![Search icon](https://hackernoon.imgix.net/search-new.png?w=19&h=19)
Reziliența în software se referă la capacitatea unei aplicații de a continua să funcționeze fără probleme și în mod fiabil, chiar și în fața unor probleme sau eșecuri neașteptate. În proiectele Fintech, reziliența este de o importanță deosebită din mai multe motive. În primul rând, companiile sunt obligate să îndeplinească cerințele de reglementare, iar autoritățile de reglementare financiare pun accent pe rezistența operațională pentru a menține stabilitatea în cadrul sistemului. În plus, proliferarea instrumentelor digitale și dependența de furnizorii de servicii terți expun afacerile Fintech la amenințări sporite de securitate. Reziliența ajută, de asemenea, la atenuarea riscurilor de întrerupere cauzate de diverși factori, cum ar fi amenințările cibernetice, pandemiile sau evenimentele geopolitice, protejând operațiunile de bază ale afacerii și activele critice.
Prin modele de reziliență, înțelegem un set de bune practici și strategii concepute pentru a ne asigura că software-ul poate rezista la întreruperi și își poate menține operațiunile. Aceste modele acționează ca niște rețele de siguranță, oferind mecanisme pentru a gestiona erorile, a gestiona sarcina și a se reface după defecțiuni, asigurând astfel că aplicațiile rămân robuste și de încredere în condiții adverse.
Cele mai comune strategii de rezistență includ bulkhead, cache, fallback, reîncercare și întrerupător. În acest articol, le voi discuta mai detaliat, cu exemple de probleme pe care le pot ajuta să le rezolve.
Să aruncăm o privire la setarea de mai sus. Avem o aplicație foarte obișnuită, cu mai multe backend-uri în spate pentru a obține câteva date. Există mai mulți clienți HTTP conectați la aceste backend-uri. Se pare că toți au același pool de conexiuni! Și, de asemenea, alte resurse precum CPU și RAM.
Ce se va întâmpla, dacă unul dintre backend-uri întâmpină un fel de probleme care au ca rezultat o latență mare a solicitărilor? Datorită timpului mare de răspuns, întregul pool de conexiuni va deveni complet ocupat de solicitări care așteaptă răspunsuri de la backend1. Ca urmare, cererile destinate backend2 și backend3 sănătoși nu vor putea continua, deoarece pool-ul este epuizat. Aceasta înseamnă că o defecțiune într-unul dintre backend-urile noastre poate provoca o defecțiune în întreaga aplicație. În mod ideal, dorim ca doar funcționalitatea asociată backend-ului defectuos să sufere degradare, în timp ce restul aplicației continuă să funcționeze normal.
Ce este modelul Bulkhead?
Termenul, Bulkhead pattern, derivă din construcția de nave, implicând crearea mai multor compartimente izolate în cadrul unei nave. Dacă apare o scurgere într-un compartiment, acesta se umple cu apă, dar celelalte compartimente rămân neafectate. Această izolare previne scufundarea întregului vas din cauza unei singure breșe.
Modelul Bulkhead poate fi utilizat pentru a izola diferite tipuri de resurse într-o aplicație, prevenind o defecțiune dintr-o parte să afecteze întregul sistem. Iată cum îl putem aplica problemei noastre:
Să presupunem că sistemele noastre backend au o probabilitate scăzută de a întâmpina erori în mod individual. Cu toate acestea, atunci când o operațiune implică interogarea tuturor acestor backend-uri în paralel, fiecare poate returna independent o eroare. Deoarece aceste erori apar independent, probabilitatea globală a unei erori în aplicația noastră este mai mare decât probabilitatea de eroare a oricărui backend unic. Probabilitatea de eroare cumulată poate fi calculată folosind formula P_total=1−(1−p)^n, unde n este numărul de sisteme backend.
De exemplu, dacă avem zece backend-uri, fiecare cu o probabilitate de eroare de p=0,001 (corespunzător unui SLA de 99,9%), probabilitatea de eroare rezultată este:
P_total=1−(1−0,001)^10=0,009955
Aceasta înseamnă că SLA-ul nostru combinat scade la aproximativ 99%, ilustrând modul în care fiabilitatea generală scade atunci când interogăm mai multe backend-uri în paralel. Pentru a atenua această problemă, putem implementa un cache în memorie.
Un cache în memorie servește ca un buffer de date de mare viteză, stochând datele accesate frecvent și eliminând nevoia de a le prelua din surse potențial lente de fiecare dată. Deoarece cache-urile stocate în memorie au o șansă de eroare de 0% în comparație cu preluarea datelor prin rețea, acestea cresc semnificativ fiabilitatea aplicației noastre. Mai mult, memorarea în cache reduce traficul de rețea, scăzând și mai mult șansele de erori. În consecință, prin utilizarea unui cache în memorie, putem obține o rată de eroare și mai mică în aplicația noastră în comparație cu sistemele noastre backend. În plus, cache-urile în memorie oferă o recuperare mai rapidă a datelor decât preluarea bazată pe rețea, reducând astfel latența aplicației - un avantaj notabil.
Pentru date personalizate, cum ar fi profiluri de utilizator sau recomandări, utilizarea cache-urilor în memorie poate fi, de asemenea, foarte eficientă. Dar trebuie să ne asigurăm că toate solicitările de la un utilizator merg în mod constant către aceeași instanță de aplicație pentru a utiliza datele stocate în cache pentru ele, ceea ce necesită sesiuni sticky. Implementarea sesiunilor sticky poate fi o provocare, dar pentru acest scenariu, nu avem nevoie de mecanisme complexe. Reechilibrarea minoră a traficului este acceptabilă, așa că va fi suficient un algoritm stabil de echilibrare a sarcinii, cum ar fi hashingul consecvent.
În plus, în cazul unei defecțiuni a nodului, hashingul consecvent asigură că numai utilizatorii asociați cu nodul eșuat sunt supuși reechilibrarii, minimizând întreruperile sistemului. Această abordare simplifică gestionarea cache-urilor personalizate și îmbunătățește stabilitatea generală și performanța aplicației noastre.
Dacă datele pe care intenționăm să le stocăm în cache sunt critice și utilizate în fiecare solicitare pe care o gestionează sistemul nostru, cum ar fi politicile de acces, planurile de abonament sau alte entități vitale din domeniul nostru, sursa acestor date ar putea reprezenta un punct de eșec semnificativ în sistemul nostru. Pentru a face față acestei provocări, o abordare este replicarea completă a acestor date direct în memoria aplicației noastre.
În acest scenariu, dacă volumul de date din sursă este gestionabil, putem iniția procesul prin descărcarea unui instantaneu al acestor date la începutul aplicației noastre. Ulterior, putem primi evenimente de actualizare pentru a ne asigura că datele din cache rămân sincronizate cu sursa. Prin adoptarea acestei metode, sporim fiabilitatea accesării acestor date cruciale, deoarece fiecare preluare are loc direct din memorie cu o probabilitate de eroare de 0%. În plus, recuperarea datelor din memorie este excepțional de rapidă, optimizând astfel performanța aplicației noastre. Această strategie atenuează în mod eficient riscul asociat cu baza pe o sursă de date externă, asigurând un acces consistent și fiabil la informațiile critice pentru funcționarea aplicației noastre.
Cu toate acestea, necesitatea de a descărca date la pornirea aplicației, întârziind astfel procesul de pornire, încalcă unul dintre principiile „aplicației cu 12 factori” care pledează pentru pornirea rapidă a aplicației. Dar, nu dorim să renunțăm la beneficiile utilizării stocării în cache. Pentru a aborda această dilemă, să explorăm soluții potențiale.
Pornirea rapidă este crucială, în special pentru platforme precum Kubernetes, care se bazează pe migrarea rapidă a aplicațiilor către diferite noduri fizice. Din fericire, Kubernetes poate gestiona aplicațiile cu pornire lentă folosind funcții precum probele de pornire.
O altă provocare cu care ne putem confrunta este actualizarea configurațiilor în timp ce aplicația rulează. Adesea, ajustarea timpilor de cache sau a expirării cererilor este necesară pentru a rezolva problemele de producție. Chiar dacă putem implementa rapid fișiere de configurare actualizate în aplicația noastră, aplicarea acestor modificări necesită de obicei o repornire. Cu timpul de pornire prelungit al fiecărei aplicații, o repornire continuă poate întârzia în mod semnificativ implementarea remedierilor pentru utilizatorii noștri.
Pentru a rezolva acest lucru, o soluție este să stocați configurațiile într-o variabilă concurentă și să aveți un fir de fundal actualizat periodic. Cu toate acestea, anumiți parametri, cum ar fi expirarea solicitărilor HTTP, pot necesita reinițializarea clienților HTTP sau a bazei de date atunci când configurația corespunzătoare se modifică, ceea ce reprezintă o potențială provocare. Cu toate acestea, unii clienți, cum ar fi driverul Cassandra pentru Java, acceptă reîncărcarea automată a configurațiilor, simplificând acest proces.
Implementarea configurațiilor reîncărcabile poate atenua impactul negativ al timpilor lungi de pornire a aplicației și poate oferi beneficii suplimentare, cum ar fi facilitarea implementărilor de semnalizare a caracteristicilor. Această abordare ne permite să menținem fiabilitatea și capacitatea de răspuns a aplicațiilor, în timp ce gestionăm eficient actualizările de configurare.
Acum să aruncăm o privire la o altă problemă: în sistemul nostru, atunci când o cerere de utilizator este primită și procesată prin trimiterea unei interogări către un backend sau o bază de date, ocazional, se primește un răspuns de eroare în locul datelor așteptate. Ulterior, sistemul nostru răspunde utilizatorului cu o „eroare”.
Cu toate acestea, în multe scenarii, poate fi mai preferabil să se afișeze date ușor învechite împreună cu un mesaj care să indice că există o întârziere de reîmprospătare a datelor, mai degrabă decât să lase utilizatorul cu un mesaj de eroare roșu mare.
Pentru a rezolva această problemă și pentru a îmbunătăți comportamentul sistemului nostru, putem implementa modelul de rezervă. Conceptul din spatele acestui tipar implică existența unei surse de date secundare, care poate conține date de calitate sau prospețime mai scăzute în comparație cu sursa primară. Dacă sursa de date primară nu este disponibilă sau returnează o eroare, sistemul poate reveni la preluarea datelor din această sursă secundară, asigurându-se că o anumită formă de informații este prezentată utilizatorului în loc să afișeze un mesaj de eroare.
Dacă te uiți la imaginea de mai sus, vei observa o asemănare între problema cu care ne confruntăm acum și cea pe care am întâlnit-o cu exemplul cache.
Pentru a o rezolva, putem lua în considerare implementarea unui model cunoscut sub numele de reîncercare. În loc să se bazeze pe cache, sistemul poate fi proiectat pentru a retrimite automat cererea în cazul unei erori. Acest model de reîncercare oferă o alternativă mai simplă și poate reduce în mod eficient probabilitatea de erori în aplicația noastră. Spre deosebire de stocarea în cache, care necesită adesea mecanisme complexe de invalidare a memoriei cache pentru a gestiona modificările datelor, reîncercarea cererilor eșuate este relativ simplu de implementat. Întrucât invalidarea cache-ului este considerată una dintre cele mai dificile sarcini din ingineria software, adoptarea unei strategii de reîncercare poate simplifica gestionarea erorilor și poate îmbunătăți rezistența sistemului.
Cu toate acestea, adoptarea unei strategii de reîncercare fără a lua în considerare consecințele potențiale poate duce la complicații suplimentare.
Să ne imaginăm că unul dintre backend-urile noastre se confruntă cu un eșec. Într-un astfel de scenariu, inițierea reîncercărilor către backend-ul eșuat ar putea duce la o creștere semnificativă a volumului de trafic. Această creștere bruscă a traficului poate copleși backend-ul, exacerbând eșecul și provocând potențial un efect de cascadă în sistem.
Pentru a face față acestei provocări, este important să completați modelul de reîncercare cu modelul întreruptorului. Întrerupătorul servește ca un mecanism de siguranță care monitorizează rata de eroare a serviciilor din aval. Când rata de eroare depășește un prag predefinit, întrerupătorul întrerupe cererile către serviciul afectat pentru o durată specificată. În această perioadă, sistemul se abține de la trimiterea de solicitări suplimentare pentru a permite recuperarea timpului de serviciu eșuat. După intervalul stabilit, întrerupătorul de circuit permite cu precauție să treacă un număr limitat de solicitări, verificând dacă serviciul s-a stabilizat. Dacă serviciul a revenit, traficul normal este restabilit treptat; în caz contrar, circuitul rămâne deschis, continuând să blocheze cererile până când serviciul reia funcționarea normală. Prin integrarea modelului de întrerupător împreună cu logica de reîncercare, putem gestiona eficient situațiile de eroare și putem preveni supraîncărcarea sistemului în timpul defecțiunilor backend.
În concluzie, prin implementarea acestor modele de rezistență, ne putem consolida aplicațiile împotriva situațiilor de urgență, putem menține disponibilitatea ridicată și putem oferi utilizatorilor o experiență perfectă. În plus, aș dori să subliniez faptul că telemetria este încă un alt instrument care nu trebuie trecut cu vederea atunci când oferă rezistență proiectului. Jurnalele și valorile bune pot îmbunătăți semnificativ calitatea serviciilor și pot oferi informații valoroase asupra performanței acestora, ajutând să luați decizii informate pentru a le îmbunătăți în continuare.