ABONAMENTE VIDEO REDACȚIA
RO
EN
NOU
Numărul 150
Numărul 149 Numărul 148 Numărul 147 Numărul 146 Numărul 145 Numărul 144 Numărul 143 Numărul 142 Numărul 141 Numărul 140 Numărul 139 Numărul 138 Numărul 137 Numărul 136 Numărul 135 Numărul 134 Numărul 133 Numărul 132 Numărul 131 Numărul 130 Numărul 129 Numărul 128 Numărul 127 Numărul 126 Numărul 125 Numărul 124 Numărul 123 Numărul 122 Numărul 121 Numărul 120 Numărul 119 Numărul 118 Numărul 117 Numărul 116 Numărul 115 Numărul 114 Numărul 113 Numărul 112 Numărul 111 Numărul 110 Numărul 109 Numărul 108 Numărul 107 Numărul 106 Numărul 105 Numărul 104 Numărul 103 Numărul 102 Numărul 101 Numărul 100 Numărul 99 Numărul 98 Numărul 97 Numărul 96 Numărul 95 Numărul 94 Numărul 93 Numărul 92 Numărul 91 Numărul 90 Numărul 89 Numărul 88 Numărul 87 Numărul 86 Numărul 85 Numărul 84 Numărul 83 Numărul 82 Numărul 81 Numărul 80 Numărul 79 Numărul 78 Numărul 77 Numărul 76 Numărul 75 Numărul 74 Numărul 73 Numărul 72 Numărul 71 Numărul 70 Numărul 69 Numărul 68 Numărul 67 Numărul 66 Numărul 65 Numărul 64 Numărul 63 Numărul 62 Numărul 61 Numărul 60 Numărul 59 Numărul 58 Numărul 57 Numărul 56 Numărul 55 Numărul 54 Numărul 53 Numărul 52 Numărul 51 Numărul 50 Numărul 49 Numărul 48 Numărul 47 Numărul 46 Numărul 45 Numărul 44 Numărul 43 Numărul 42 Numărul 41 Numărul 40 Numărul 39 Numărul 38 Numărul 37 Numărul 36 Numărul 35 Numărul 34 Numărul 33 Numărul 32 Numărul 31 Numărul 30 Numărul 29 Numărul 28 Numărul 27 Numărul 26 Numărul 25 Numărul 24 Numărul 23 Numărul 22 Numărul 21 Numărul 20 Numărul 19 Numărul 18 Numărul 17 Numărul 16 Numărul 15 Numărul 14 Numărul 13 Numărul 12 Numărul 11 Numărul 10 Numărul 9 Numărul 8 Numărul 7 Numărul 6 Numărul 5 Numărul 4 Numărul 3 Numărul 2 Numărul 1
×
▼ LISTĂ EDIȚII ▼
Numărul 71
Abonament PDF

Resilience

Lucian Condescu
Java Developer @ NTT DATA Romania



PROGRAMARE

Caracteristica principală a unui sistem distribuit o reprezintă numărul mare de componente independente care colaborează în vederea obținerii unor funcționalități comune. Comunicarea între ele se realizează, în general, prin intermediul rețelei (network). Pe lângă numeroasele avantaje, o astfel de abordare aduce de la sine și o serie de probleme. Cea mai importantă dintre ele și cea pe care o voi analiza în acest articol, este gestionarea eficientă a situațiilor în care una sau mai multe componente distribuite nu mai funcționează sau funcționează deficitar.

ntt data

Altfel spus, voi prezenta câteva patternuri care fac ca un sistem software să fie cu adevărat rezistent (resilient) la diferitele tipuri de eșecuri (failures) ce pot apărea și să rămână în picioare până în repriza a 12-a, asemenea unui pugilist.

Referință imagine

Într-un sistem distribuit, cel mai des întâlnite probleme sunt problemele cauzate de rețea. Ele pot apărea în orice moment și pot fi determinate de defecțiuni hardware/software, atacuri de securitate, configurarea greșită a device-urilor precum și multe alte cauze. Există însă și alte moduri în care o componentă distribuită poate eșua. Fie că vorbim despre eșecuri cauzate de timpi foarte mari de răspuns, fie despre răspunsuri eronate sau invalide, din perspectiva clientului unei componente sau al unui serviciu, lucrurile nu sunt roz.

În consecință, ne punem întrebarea cum am putea dezvolta o aplicație într-un mod în care aceasta să fie rezistentă la multitudinea de situații excepționale ce pot apărea, fără ca utilizatorul final să sesizeze (ideal) sau fără a-i degrada în mod dramatic experiența. Mi-am propus să vorbesc în continuare despre câteva mijloace de protecție și de răspuns care ne ajută în tratarea comportamentelor neprevăzute dintr-un sistem distribuit și care oferă aplicației noastre un mai mare grad de rezistență (resilience) și de stabilitate.

Timeout

Poate cel mai simplu și totodată cel mai neglijat mecanism de protecție este folosirea timeout-urilor. Un serviciu nu ar trebui să aștepte la nesfârșit rezultatul unei operații ce implică apeluri de rețea, în schimb ar trebui să renunțe la request după o perioadă rezonabilă de timp. Întrebarea firească care se pune este: ce ar trebui să se întâmple în continuare ? O variantă ar fi integrarea unui mecanism de retry care să repete operația precedentă. Din păcate, în cazul problemelor de rețea mai sus menționate, aceasta nu ajută, pentru că de cele mai multe ori acestea nu se rezolvă de la sine, ci necesită timp sau intervenții. O altă variantă ar putea fi adăugarea operației într-o coadă de așteptare pentru o procesare ulterioară și notificarea utilizatorului în acest sens. Cum probabil v-ați dat seama, o astfel de abordare funcționează doar pentru anumite tipuri de servicii, spre exemplu tranzacții bancare sau servicii de mesagerie. Pentru altele, cea mai bună variantă este întoarcerea unui comportament de fallback.

Fallbackul a fost introdus ca o alternativă la mecanismele de retry și queue, acolo unde nici unul din cele două nu se pretează. Poate fi văzut ca un comportament default folosit atunci când un serviciu eșuează. Trebuie să fim însă foarte atenți la alegerea fallbackului potrivit pentru un serviciu. Există situații în care singurul comportament rezonabil ar putea fi întoarcerea unui mesaj de eroare către utilizator (real-time systems) dar și situații în care un fallback potrivit ar putea trece neobservat din perspectiva utilizatorului final. Spre exemplu, în cazul unei aplicații de comerț online, un fallback potrivit pentru serviciul de autentificare ar putea fi întoarcerea un utilizator guest, ceea ce va face ca experiența clientului să nu aibă prea mult de suferit, acesta putând în continuare să facă cumpărături.

Fail Fast

Ce poate fi mai frustrant decât așteptarea îndelungată după un serviciu care întârzie să furnizeze datele solicitate ? Așteptarea îndelungată cumulată cu funcționarea improprie sau cu aflarea că date introduse de noi sunt incorecte. E ca și cum am aștepta foarte mult timp la un ghișeu pentru a descoperi că ne lipsește un formular. Nu putea să ne spună cineva asta, înainte să ne punem la coadă ?

Pe același principiu funcționează și patternul Fail Fast. Ideal ar fi că dacă un serviciu urmează să eșueze într-un mod sau altul, ar trebui să o facă cât mai repede cu putință, evitând folosirea inutilă a resurselor sistemului și evitând răspunsurile lente. Pentru cele mai multe din cazuri, Fail Fast poate fi implementat adăugând un nivel de validare a datelor de intrare în fața operațiilor costisitoare din punct de vedere al consumului de resurse (uzual, apeluri de servicii externe). Aceasta va crește probabilitatea ca operația să reușească și să nu fie executată în zadar. Dar ce se întâmplă dacă dintr-un motiv sau altul, serviciul apelat nu funcționează sau funcționează deficitar ? La un moment dat, cineva ar trebui să oprească apelurile inutile și să eșueze rapid. Aici intervine circuit breakerul.

Circuit Breaker

Conceptul de circuit breaker provine din ingineria electrică, modul de funcționare fiind relativ simplu: în momentul în care apare o problemă în rețeaua electrică (supra-sarcini sau scurtcircuite), circuit breakerul întrerupe curentul prin respectivul circuit, evitând compromiterea întregii rețele electrice sau a consumatorilor finali.

Într-un sistem software, circuit breakerul are rolul de a monitoriza operațiile ce implică apeluri de rețea și de a interveni acolo unde este cazul. De îndată ce un serviciu extern a atins un anumit număr de apelări considerate eșuate, circuit breakerul ajunge la concluzia că mai mult ca sigur serviciul nu mai este funcțional și oprește apelurile ulterioare, întorcând un comportament de fallback. În unele situații, aceasta ajută și serviciul respectiv să se recupereze, neîncărcându-l cu requesturi suplimentare, sortite eșecului. Dar avantajul major e dat de faptul că performanța generală a aplicației noastre, care altfel ar fi consumat inutil resurse, nu va avea de suferit.

Un circuit breaker are trei stări posibile: Închis, Deschis și Parțial-Deschis.

În practică, se recomandă folosirea a câte unui circuit breaker pentru toate interacțiunile între componentele unui sistem distribuit ce implică rețeaua. Citirea dintr-o bază de date, apelarea unui serviciu REST, chiar și apelarea unui alt serviciu dezvoltat tot de noi, toate acestea sunt situații în care un circuit breaker ne-ar putea salva aplicația de la o prăbușire generală.

Bulkhead

Iar dacă tot vorbim de lucruri ce ne-ar putea salva de la o prăbușire în cascadă a componentelor dintr-un sistem distribuit, merită să menționăm și patternul Bulkhead. Conceptul de bulkhead provine din ingineria navală și presupune împărțirea navelor/bărcilor în mai multe zone independente, astfel încât o eventuală fisură să nu compromită întreaga navă, ci doar un anumit compartiment, nava fiind încă funcțională (într-o oarecare măsură).

Extrapolând la software, folosirea bulkheadurilor ne ajută să compartimentăm o aplicație într-un mod în care un serviciu ce încetează să mai funcționeze corespunzător, să nu influențeze funcționalitatea restului de servicii. Cu alte cuvinte, dorim ca un eșec al unui anumit serviciu să nu se propage în întreg sistemul și să producă comportamente neașteptate ale altor servicii dar și a aplicației ca întreg.

În mod uzual, bulkheadul se implementează creând pentru fiecare serviciu câte un thread pool separat. Dacă o funcționalitate e supusă unui număr foarte mare de requesturi sau din diferite motive devine indisponibilă, doar thread poolul asociat va fi afectat, restul serviciilor funcționând la parametri normali. Presupunând că un serviciu depinde de alte două servicii iar unul din ele începe să funcționeze mai greoi, utilizarea unui singur thread pool va produce în cele din urmă și o degradare a rapidității funcționalităților puse la dispoziție de celălalt.

Concluzie

În contextul în care software-ul din ziua de azi se bazează din ce în ce mai mult pe componente distribuite, patternurile și bunele practici prezentate mai sus, devin extrem de importante. Aplicate, ele ne pot oferi siguranța că aplicația noastră va continua să funcționeze chiar și în condiții diferite față de cele ideale.

Bibliografie:

  1. https://www.slideshare.net/ufried/patterns-of-resilience

  2. Michael T. Nygard, Release It!

  3. https://martinfowler.com/bliki/CircuitBreaker.html

  4. https://docs.microsoft.com/en-us/azure/architecture/patterns/bulkhead

  5. https://www.javaworld.com/article/2824163/application-performance/stability-patterns-applied-in-a-restful-architecture.html?page=2

NUMĂRUL 149 - Development with AI

Sponsori

  • Accenture
  • BT Code Crafters
  • Accesa
  • Bosch
  • Betfair
  • MHP
  • BoatyardX
  • .msg systems
  • P3 group
  • Ing Hubs
  • Cognizant Softvision
  • Colors in projects