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 137
Abonament PDF

O poveste despre un microserviciu și problemele sale de performanță

Adnana Brişan
Mid Software Engineer @ Zenitech



PROGRAMARE

Tendințele tehnologice din ultimii ani s-au îndreptat tot mai mult înspre dezvoltarea unor servicii cât mai mici, cât mai rapide și care să consume cât mai puține resurse. Astfel, s-a ajuns la "spargerea" aplicațiilor vechi, greoaie, de tip monolit, în bucățele numite microservicii, care deservesc o singură funcționalitate a unei platforme. Ne-am întors, așadar, la filozofia de tip divide et impera.

Ce se poate face, totuși, atunci când, orice s-ar încerca, performanțele microserviciului nu se apropie de cele scontate? Ba, mai mult, chiar influențează negativ mediul în care rulează?

Acest articol prezintă încercări practice de îmbunătățire a performanțelor unui microserviciu real, care au fost lansate în producție împreună cu avantajele și dezavantajele aferente. Sperăm ca strategiile noastre să fie aplicabile și în alte contexte sau dacă nu, măcar să aprindă scânteia inspirației pentru cei care se confruntă cu situații asemănătoare.

Cu ce ne confruntăm?

Așa cum am învățat de mici, orice problemă trebuie să aibă ipoteză și concluzie. Deși în titlu am promis o poveste, ne iertați dacă vom îmbina literatura cu matematica. Ipoteza noastră sună cam așa:

A fost odată un microserviciu (să-i spunem Micron) trecut de prima tinerețe, scris în Java 11, care expunea un API de tip REST, dispunea de un mecanism de caching, funcționând în producție pe 6 VM-uri (virtual machines). Pentru stocarea datelor se folosea un singur nod al unei baze de date de tip MySQL, care era, de asemenea, instalată în mașină virtuală. La nivel de platformă, acest microserviciu era cel mai solicitat de către utilizatori (primind, în medie, un milion de cereri pe oră), fiind esențial navigării acestora în cadrul site-ului. Totodată, requesturile tuturor clienților de pe mapamond erau redirecționate din diferite environment-uri de tip spoke către environmentul principal, de tip hub, deci către cele 6 VM-uri disponibile. Distribuția cererilor înspre VM-uri se făcea printr-o "poartă", un proxy, care funcționa după un algoritm de tip least connections, al cărui ultim update de software data din 2017. În intervalele de timp în care platforma avea cele mai multe accesări, Micron avea performanțe foarte slabe, timpi de răspuns crescuți, numărul de erori returnate era semnificativ, iar nivelul de utilizare al CPU pentru baza de date se apropia, uneori chiar atingând, maximul alocat. Un alt microserviciu, reactiv de data aceasta - Micron 2.0 - care îndeplinește aceleași funcționalități și va veni ca un înlocuitor pentru Micron era în curs de dezvoltare. Dar soluția la problema supraîncărcării trebuia să se găsească urgent.

Pentru o mai bună experiență de utilizare a platformei atât la ore de vârf, cât și în afara acestora, timpii de răspuns ai lui Micron trebuiau neapărat îmbunătățiți, numărul erorilor redus cât mai mult sau chiar complet, iar gradul de folosire a bazei de date trebuia minimizat, cu scopul de a preveni posibile incidente care ar implica pierderea datelor stocate.

Ce e de făcut?

Analiza textului ne duce cu gândul la un Făt-Frumos la ananghie. Precum Harap-Alb a avut nevoie să treacă prin diverse aventuri alături de prietenii săi pentru a se descoperi pe sine, Micron are nevoie de o reînnoire pentru a-și îmbunătăți forma, dar, deși este eroul principal, are nevoie și de ajutor din partea celorlalte personaje.

La un prim brainstorming, se observă clar că problema are o componentă prioritară: reducerea numărului de interogări pe baza de date, în același timp țintind către creșterea vitezei serviciului și scăderea procentului de răspunsuri eronate. Există diverse modalități prin care aceste rezultate ar putea fi obținute. Dintre acestea menționăm:

  1. Transformarea serviciului într-unul reactiv, asincron. Astfel, s-ar permite execuția mai multor fire în paralel, scăzând timpul de răspuns. Totuși, această idee este invalidată, deoarece inițiativa pentru Micron 2.0 este deja în curs, iar efortul suplimentar ar fi în van.

  2. Limitarea numărului de conexiuni deschise. Deși pare o soluție fezabilă pentru a asigura corectitudinea informațiilor returnate și a scădea rata de interogări trimise spre baza de date, nu ne permitem să riscăm înrăutățirea drastică a timpului de răspuns, tot comportamentul platformei depinzând de cererile către Micron.

  3. Rescrierea tuturor query-urilor SQL pentru a evita problema N+1. Aceasta ar reprezenta soluția uneia dintre cele mai întâlnite cauze ale scăderii performanțelor serviciilor care folosesc baze de date relaționale. Practic, să ne imaginăm că într-un tabel avem o listă cu mașini, iar într-un alt tabel o listă cu roți, între cele două existând o relație de tip One-to-Many (mai multe roți corespund unei mașini). Pentru a afla anumite informații despre roți, serviciul va efectua o interogare spre baza de date care va returna lista completă a mașinilor, iar pentru fiecare dintre cele N mașini, va crea un alt query ca să obțină lista de roți corespunzătoare, pe care mai apoi o va prelucra. În total, se efectuează N+1 căutări, care ar putea fi reduse la una singură: lista completă a roților, obținută direct din tabelul corespunzător, care să fie salvată în memorie și mai apoi prelucrată.

  4. Scalarea serviciului. Cu cât numărul de VM-uri este mai mare, cu atât raportul requesturi/mașină virtuală este mai scăzut. Totuși, nu putem să creștem numărul de mașini virtuale la fel de ușor cum am crește numărul de poduri în Kubernetes, pentru că procesul este unul costisitor pentru companie, atât din punct de vedere financiar, cât și din perspectiva muncii depuse de către mai multe echipe, rămânând posibilitatea ca performanțele să nu se ridice la nivelul așteptărilor. Cum spun englezii, penny wise, but pound foolish. De asemenea, această inițiativă nu ar atinge scopul principal, reducerea efortului depus de baza de date.

  5. Folosirea tuturor nodurilor bazei de date, în loc de unul singur. Poate cel mai sigur mod de a minimiza utilizarea CPU. Astfel, interogările ar merge către trei noduri, în loc de unul singur, fără a mai cauza suprasolicitare. Problema se pune, însă, în ceea ce privește implementarea, ținând cont de mai multe aspecte, precum topologia bazei de date sau algoritmul de repartizare a VM-urilor la noduri.

Ce alegem?

Așa cum am menționat în ipoteza noastră, Micron a ajuns la o vârstă înaintată, când nu mai acceptă cu ușurință schimbări majore. Drept urmare, s-a recurs la variantele care ar avea cele mai bune rezultate în contextul actual: rescrierea manuală a interogărilor, pentru a reduce numărul de interacțiuni cu baza de date și implementarea unui mecanism de conectare a VM-urilor la toate cele trei noduri MySQL disponibile.

Pe de o parte, rezolvarea aspectului celor N+1 query-uri este o muncă de durată raportându-ne la complexitatea serviciului, pentru care avem la dispoziție toate informațiile necesare. Cu alte cuvinte, vorbim de un proces care presupune modificări la nivelul claselor din proiect care se ocupă direct cu solicitarea informațiilor. Așa trebuia și Harap-Alb să separe nisipul de mac...

Pe de altă parte, conectarea unui microserviciu la toate nodurile unei baze de date ridică mai multe probleme:

Mai mult ca sigur, există și alți factori pe care nu i-am luat în calcul sau nu i-am detaliat, dar asta nu face altceva decât să denote lipsa importanței acestora. Chestiunile arzătoare au fost mai mult ca sigur întoarse pe toate părțile, fiți fără grijă.

Așadar, luând în calcul ecosistemul în care rulează Micron și opțiunile de care dispunem, vom recurge la modificări atât în codul microserviciului - se implementează un mecanism de retry pentru a face posibilă reîncercarea executării tuturor operațiilor de scriere în baza de date în cazul în care se produc conflicte - cât și în afara acestuia. Cu toate că riscăm ca, prin logica adițională, să înrăutățim viteza sistemului, se consideră că situațiile în care apar conflicte vor fi rare, iar integritatea datelor și lipsa erorilor sunt mai importante decât atingerea timpului de răspuns minim posibil.

Cu privire la problema folosirii tuturor nodurilor MySQL disponibile, există posibilitatea de a suprascrie, pentru fiecare VM, proprietățile referitoare la URL-ul sursei de date. Astfel, știind că avem 6 mașini virtuale și 3 noduri disponibile, intenționăm ca un nod să fie folosit de către două VM-uri. Deși această soluție nu garantează stabilitate permanentă sau balansarea perfectă a numărului de cereri trimise spre fiecare nod, este un pas important în direcția potrivită și nu prezintă aceeași complexitate pe care ar avea-o, spre exemplu, crearea unui algoritm de decizie la nivel de codebase.

Pentru a aborda cel de-al treilea aspect problematic, se optează pentru scoaterea din uz a proxy-ului actual și crearea unuia nou, de tip NGINX, care va rula în Kubernetes și se va comporta ca un demultiplexor. Acesta va asigura existența sesiunilor "lipicioase" atât între client și unul dintre pod-urile sale (sticky session cookies la nivel de Ingress), cât și mai departe, între sine și unul dintre VM-uri. Algoritmul de distribuție folosit este de tip hashing - se bazează pe IP-ul atașat cererii, împreună cu alte date relevante - și redirecționează cererile care vin de la client către una dintre mașinile virtuale în funcție de codul rezultat. Drept urmare, cererile care provin din același loc vor merge spre aceeași destinație în cadrul aceleiași sesiuni. Odată cu alegerea utilizării unui asemenea software, se ocolește riscul pe care l-ar fi presupus schimbarea algoritmului de distribuție în cadrul proxy-ului existent, adică necesitatea actualizării acestuia, după șase ani în care a fost ignorat. Nu s-a dorit trezirea ursului, ca să putem ajunge la salățile prețioase din grădina lui.

Ce am învățat?

Ordinea în care s-au adus modificări serviciului nu a fost una întâmplătoare. Mai întâi, s-au implementat schimbările în codul lui Micron, cele legate de problema N+1 și de mecanismul de retry. În a doua fază, s-a creat noul proxy, iar cea de-a treia etapă a fost reprezentată de redirecționarea VM-urilor microserviciului către toate nodurile bazei de date. S-au efectuat teste de performanță la sfârșitul fiecărei etape, urmărindu-se comportamentul sistemului în fiecare situație. Continuând paralela cu Harap-Alb, Micron a trebuit să treacă prin diverse probe de foc până la atingerea finalului fericit...

Datele rezultate în urma fiecărui test de anduranță (rulat pe parcursul a 8 ore, cu un număr de 3810 utilizatori în total) se pot regăsi în tabelul de mai jos.

Software testat Timp mediu de răspuns (ms) Procentaj + număr total de erori de tip 500 Număr total de cereri (mil)
Micron - varianta inițială + proxy 67.66 0.00001% (924) 54.29
inițial + 1 nod MySQL
Micron - varianta finală + proxy 39.84 0% (0) 54.32
inițial + 1 nod MySQL
Micron - varianta finală + NGINX + 1 38.26 0% (0) 54.32
nod MySQL
Micron - varianta finală + NGINX + 3 30.51 0% (0) 54.34
noduri MySQL

Se observă cu ușurință faptul că îmbunătățirea cea mai mare a venit prin intermediul ajustărilor din codebase, în prima fază de testare. Pe lângă scăderea timpului mediu de răspuns cu un răsunător 41%, s-a reușit anularea totală a erorilor de server. Cu toate acestea, o comparație mai detaliată a relevat faptul că unele API-uri, mai ales cele care execută operații de scriere în baza de date, au avut un timp de răspuns mai slab decât cel obținut în testul inițial. Acest fapt denotă caracterul de "rău necesar" al mecanismului de retry, compromisul făcut în detrimentul vitezei, pentru a scădea procentajul de erori.

A doua tură de teste, rulate, de această dată, împreună cu noul demultiplexor a avut rezultate asemănătoare celor din cazul precedent. Totuși, efectul scontat a fost atins, testul dovedind capacitatea proxy-ului de a-și îndeplini cu brio atribuțiile, fără a produce vreo defecțiune. Deși distribuția requesturilor către VM-uri nu a fost perfect balansată din cauza faptului că motoarele de testare au IP-uri apropiate care se încadrează, de cele mai multe ori, în aceeași categorie, fiecare dintre cele șase mașini virtuale pe care rulează microserviciul a funcționat la o capacitate de maximum 50%. Astfel, se poate spune cu încredere că, în situații reale de folosire a platformei, nu se va ajunge la suprasolicitarea serviciului pe niciuna dintre instanțe.

Ultima rundă de teste de anduranță la care a fost supus Micron a fost "cireașa de pe tort", combinând toate eforturile depuse până atunci. Prin distribuirea încărcăturii pe toate cele trei noduri de MySQL, s-a obținut un timp de răspuns cu 54.9% (!!!) mai bun decât în primul test și cu 20.5% mai bun decât în cea mai recentă etapă de testare. Cea mai mare realizare a fost, însă, reducerea nivelului de utilizare a procesorului alocat bazei de date, de la 100% pe singurul nod folosit în situația inițială, la aproximativ 30% pe fiecare dintre cele trei noduri.

Cum se termină povestea?

Prințul s-a căsătorit cu prințesa, unind astfel cele două regate, și au trăit fericiți până la adânci bătrâneți. Sau, cel puțin, o variantă relativ asemănătoare, care se poate aplica în domeniul tehnologiei:

Ca urmare a testelor de performanță cu rezultate excelente, s-a decis lansarea noii variante a serviciului, alături de toate configurările adiționale. Chiar dacă în release s-a mai împotmolit pe alocuri și a dat emoții celor prezenți, Micron a rezistat intemperiilor și a primit cu succes un update de versiune. De atunci și până în ziua de astăzi, nu au existat incidente legate de suprasolicitarea bazei de date sau de pierderea integrității datelor. Din partea clienților nu au mai existat plângeri, iar platforma nu mai necesită refreshuri ocazionale.

Și-am încălecat pe-o șa, și v-am spus povestea așa. Și-am încălecat pe-o căpșună, și v-am spus o mare minciună...

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