Faceți calculul: scalarea aplicațiilor de microservicii cu orchestratori
Publicat: 2022-03-11Nu este chiar o surpriză faptul că arhitectura aplicațiilor pentru microservicii continuă să invadeze designul software. Este mult mai convenabil să distribuiți încărcătura, să creați implementări foarte disponibile și să gestionați upgrade-urile, ușurând în același timp dezvoltarea și managementul echipei.
Dar povestea cu siguranță nu este aceeași fără orchestratori container.
Este ușor să vrei să folosești toate caracteristicile lor cheie, în special scalarea automată. Ce binecuvântare este, urmărirea implementării containerelor care fluctuează toată ziua, dimensionate ușor pentru a face față încărcăturii curente, eliberându-ne timp pentru alte sarcini. Suntem mulțumiți cu mândrie de ceea ce arată instrumentele noastre de monitorizare a containerelor; Între timp, tocmai am configurat câteva setări - da, asta este (aproape) tot ce a fost nevoie pentru a crea magia!
Asta nu înseamnă că nu există niciun motiv să fim mândri de asta: suntem siguri că utilizatorii noștri au o experiență bună și că nu risipim bani cu infrastructura supradimensionată. Acest lucru este deja destul de considerabil!
Și, desigur, ce călătorie a fost până acolo! Pentru că, chiar dacă la sfârșit nu există atât de multe setări care trebuie configurate, este mult mai complicat decât am putea crede de obicei înainte de a începe. Număr minim/max de replici, praguri de creștere/reducere, perioade de sincronizare, întârzieri de răcire — toate aceste setări sunt foarte legate între ele. Modificarea unuia îl va afecta cel mai probabil pe altul, dar trebuie totuși să aranjați o combinație echilibrată care să se potrivească atât aplicației/implementarii, cât și infrastructurii dumneavoastră. Și totuși, nu veți găsi nicio carte de bucate sau vreo formulă magică pe Internet, deoarece depinde foarte mult de nevoile dvs.
Cei mai mulți dintre noi le setăm mai întâi la valori „aleatorie” sau implicite pe care le ajustem ulterior în funcție de ceea ce găsim în timpul monitorizării. Asta m-a făcut să mă gândesc: și dacă am fi capabili să stabilim o procedură mai „matematică” care să ne ajute să găsim combinația câștigătoare?
Calcularea parametrilor de orchestrare a containerului
Când ne gândim la microservicii de scalare automată pentru o aplicație, ne uităm de fapt la îmbunătățirea a două puncte majore:
- Asigurarea că implementarea se poate extinde rapid în cazul unei creșteri rapide a încărcăturii (pentru ca utilizatorii să nu se confrunte cu timeout-uri sau HTTP 500s)
- Scăderea costului infrastructurii (adică, împiedicarea instanțelor să fie subîncărcate)
Aceasta înseamnă, practic, optimizarea pragurilor software-ului containerului pentru extindere și reducere. (Algoritmul lui Kubernetes are un singur parametru pentru cei doi).
Voi arăta mai târziu că toți parametrii legați de instanță sunt legați de pragul de upscale. Acesta este cel mai dificil de calculat - de unde acest articol.
Notă: În ceea ce privește parametrii care sunt setați la nivel de cluster, nu am nicio procedură bună pentru aceștia, dar la sfârșitul acestui articol, voi introduce un software (o pagină web statică) care îi ține cont în timpul calculului parametrii de auto-scalare ai unei instanțe. Astfel, veți putea varia valorile acestora pentru a lua în considerare impactul lor.
Calcularea pragului de extindere
Pentru ca această metodă să funcționeze, trebuie să vă asigurați că aplicația dvs. îndeplinește următoarele cerințe:
- Sarcina trebuie să fie distribuită uniform în fiecare instanță a aplicației dvs. (într-o manieră round-robin)
- Perioadele de solicitare trebuie să fie mai scurte decât intervalul de verificare a încărcării grupului de containere .
- Trebuie să luați în considerare rularea procedurii pe un număr mare de utilizatori (definiți mai târziu).
Motivul principal pentru aceste condiții provine din faptul că algoritmul nu calculează sarcina ca fiind per utilizator , ci ca o distribuție (explicată mai târziu).
Obținerea tuturor gaussianelor
Mai întâi trebuie să formulăm o definiție pentru o creștere rapidă a sarcinii sau, cu alte cuvinte, un scenariu în cel mai rău caz. Pentru mine, o modalitate bună de a o traduce este: a avea un număr mare de utilizatori care efectuează acțiuni consumatoare de resurse într-o perioadă scurtă de timp - și există întotdeauna posibilitatea ca acest lucru să se întâmple în timp ce un alt grup de utilizatori sau servicii îndeplinesc alte sarcini. Deci, să începem de la această definiție și să încercăm să extragem niște matematici. (Pregătește-ți aspirina.)
Introducerea unor variabile:
- $N_{u}$, „numărul mare de utilizatori”
- $L_{u}(t)$, sarcina generată de un singur utilizator care efectuează „operația consumatoare de resurse” ($t=0$ indică momentul în care utilizatorul începe operația)
- $L_{tot}(t)$, încărcarea totală (generată de toți utilizatorii)
- $T_{tot}$, „perioada scurtă de timp”
În lumea matematică, vorbind despre un număr mare de utilizatori care efectuează același lucru în același timp, distribuția utilizatorilor în timp urmează o distribuție gaussiană (sau normală), a cărei formulă este:
\[G(t) = \frac{1}{\sigma \sqrt{2 \pi}} e^{\frac{-(t-\mu)^2}{2 \sigma^2}}\]Aici:
- µ este valoarea așteptată
- σ este abaterea standard
Și este reprezentat grafic după cum urmează (cu $µ=0$):
Probabil că amintește de unele cursuri pe care le-ai urmat — nimic nou. Cu toate acestea, ne confruntăm cu prima noastră problemă aici: pentru a fi precis din punct de vedere matematic, ar trebui să luăm în considerare un interval de timp de la $-\infty$ la $+\infty$, care, evident, nu poate fi calculat.
Dar privind graficul, observăm că valorile din afara intervalului $[-3σ, 3σ]$ sunt foarte apropiate de zero și nu variază mult, ceea ce înseamnă că efectul lor este cu adevărat neglijabil și poate fi lăsat deoparte. Acest lucru este mai adevărat, deoarece scopul nostru este să testăm extinderea aplicației noastre, așa că căutăm variații ale unui număr mare de utilizatori.
În plus, deoarece intervalul $[-3σ, 3σ]$ conține 99,7% dintre utilizatorii noștri, este suficient de aproape de total pentru a lucra la el și trebuie doar să înmulțim $N_{u}$ cu 1,003 pentru a compensa diferența. Selectarea acestui interval ne dă $µ=3σ$ (deoarece vom lucra de la $t=0$).
În ceea ce privește corespondența cu $T_{tot}$, alegerea acesteia să fie egală cu $6σ$ ($[-3σ, 3σ]$) nu va fi o aproximare bună, deoarece 95,4 la sută dintre utilizatori se află în intervalul $[- 2σ, 2σ]$, care durează $4σ$. Deci, dacă alegeți $T_{tot}$ să fie egal cu $6σ$, va adăuga jumătate din timp pentru doar 4,3% dintre utilizatori, ceea ce nu este cu adevărat reprezentativ. Astfel alegem să luăm $T_{tot}=4σ$ și putem deduce:
\(σ=\frac{T_{tot}}{4}\) și \(µ=\frac{3}{4} * T_{tot}\)
Au fost acele valori doar scoase dintr-o pălărie? Da. Dar acesta este scopul lor și acest lucru nu va afecta procedura matematică. Aceste constante sunt pentru noi și definesc noțiuni legate de ipoteza noastră. Acest lucru înseamnă doar că acum că le avem setate, cel mai rău scenariu al nostru poate fi tradus astfel:
Sarcina generată de 99,7% din $N{u}$, efectuând o operație consumatoare $L{u}(t)$ și unde 95,4% dintre aceștia o fac în perioada $T{tot}$.
(Acesta este ceva ce merită reținut atunci când utilizați aplicația web.)
Injectând rezultatele anterioare în funcția de distribuție a utilizatorului (Gauss), putem simplifica ecuația după cum urmează:

De acum înainte, având $σ$ și $µ$ definite, vom lucra la intervalul $t \in [0, \frac{3}{2}T_{tot}]$ (cu durată $6σ$).
Care este sarcina totală de utilizatori?
Al doilea pas în microservicii de scalare automată este calcularea $L_{tot}(t)$.
Deoarece $G(t)$ este o distribuție , pentru a prelua numărul de utilizatori la un anumit moment în timp, trebuie să calculăm integrala acesteia (sau să folosim funcția de distribuție cumulată). Dar din moment ce nu toți utilizatorii își încep operațiunile în același timp, ar fi o adevărată mizerie să încerci să introduci $L_{u}(t)$ și să reducă ecuația la o formulă utilizabilă.
Deci, pentru a face acest lucru mai ușor, vom folosi o sumă Riemann, care este o modalitate matematică de a aproxima o integrală folosind o sumă finită de forme mici (vom folosi dreptunghiuri aici). Cu cât sunt mai multe forme (subdiviziuni), cu atât rezultatul este mai precis. Un alt beneficiu al utilizării subdiviziunilor vine din faptul că putem considera toți utilizatorii dintr-o subdiviziune ca și-au început operațiunile în același timp.
Revenind la suma Riemann, are următoarea proprietate care se conectează cu integralele:
\[\int_{a}^{b} f( x )dx = \lim_{n \rightarrow \infty } \sum_{k=1}^{n} ( x_{k} - x_{k-1} ) f( x_{k} )\]Cu $x_k$ definit după cum urmează:
\[x_{ k } = a + k\frac{ b - a }{ n }, 0 \leq k \leq n\]Acest lucru este adevărat acolo unde:
- $n$ este numărul de subdiviziuni.
- $a$ este limita inferioară, aici 0.
- $b$ este limita superioară, aici $\frac{3}{2}*T_{tot}$.
- $f$ este funcția—aici $G$—pentru a-și aproxima aria.
Notă: numărul de utilizatori prezenți într-o subdiviziune nu este un număr întreg. Acesta este motivul pentru două dintre condițiile prealabile: a avea un număr mare de utilizatori (deci partea zecimală nu are prea mult impact) și necesitatea ca sarcina să fie distribuită uniform în fiecare instanță.
De asemenea, rețineți că putem vedea forma dreptunghiulară a subdiviziunii în partea dreaptă a definiției sumei Riemann.
Acum că avem formula sumei Riemann, putem spune că valoarea de încărcare la momentul $t$ este suma numărului de utilizatori din fiecare subdiviziune înmulțită cu funcția de încărcare a utilizatorului la momentul corespunzător . Aceasta poate fi scrisă ca:
\[L_{ tot }( t ) = \lim_{n \rightarrow \infty} \sum_{ k=1 }^{ n } ( x_{k} - x_{k-1} )G( x_{k} ) L_{ u }( t - x_{k} )\]După înlocuirea variabilelor și simplificarea formulei, aceasta devine:
\[L_{ tot }( t ) = \frac{6 N_{u}}{\sqrt{2 \pi}} \lim_{n \rightarrow \infty} \sum_{ k=1 }^{ n } (\ frac{1}{n}) e^{-{(\frac{6k}{n} - 3)^{2}}} L_{ u }( t - k \frac{3 T_{tot}}{2n } )\]Și voila ! Am creat funcția de încărcare!
Găsirea pragului de extindere
Pentru a finaliza, trebuie doar să rulăm un algoritm de dihotomie care variază pragul pentru a găsi cea mai mare valoare în care încărcarea per instanță nu depășește niciodată limita maximă în întreaga funcție de încărcare. (Aceasta este ceea ce face aplicația.)
Deducerea altor parametri de orchestrare
De îndată ce ați găsit pragul de extindere ($S_{up}$), alți parametri sunt destul de ușor de calculat.
De la $S_{up}$ veți ști numărul maxim de instanțe. (De asemenea, puteți căuta sarcina maximă a funcției de încărcare și puteți împărți la sarcina maximă pe caz, rotunjită în sus.)
Numărul minim ($N_{min}$) de instanțe trebuie să fie definit în funcție de infrastructura dvs. (Aș recomanda să aveți minimum o replică per AZ.) Dar trebuie să țineți cont și de funcția de încărcare: deoarece o funcție Gaussiană crește destul de rapid, distribuția de încărcare este mai intensă (pe replică) la început, așa că poate doriți să creșteți numărul minim de replici pentru a amortiza acest efect. (Acest lucru va crește cel mai probabil $S_{up}$.)
În cele din urmă, odată ce ați definit numărul minim de replici, puteți calcula pragul de reducere ($S_{down}$) ținând cont de următoarele: Deoarece reducerea unei singure replici nu are mai mult efect asupra altor instanțe decât atunci când reduceți de la $N_{min}+1$ până la $N_{min}$, trebuie să ne asigurăm că pragul de extindere nu va fi declanșat imediat după reducere. Dacă este permis, acest lucru va avea un efect yo-yo. Cu alte cuvinte:
\[( N_{ min } + 1) S_{ jos } < N_{ min }S_{ sus }\]Sau:
\[S_{ jos } < \frac{N_{ min }}{N_{min}+1}S_{ sus }\]De asemenea, putem admite că, cu cât clusterul dvs. este configurat să aștepte mai mult înainte de a reduce scalarea, cu atât este mai sigur să setați $S_{down}$ mai aproape de limita superioară. Încă o dată, va trebui să găsești un echilibru care ți se potrivește.
Rețineți că, atunci când utilizați sistemul de orchestrare Mesosphere Marathon cu autoscalerul său, numărul maxim de instanțe care pot fi eliminate o dată de la reducere este legat de AS_AUTOSCALE_MULTIPLIER
($A_{mult}$), ceea ce implică:
Dar funcția de încărcare utilizator?
Da, aceasta este o problemă puțin și nu cea mai ușor de rezolvat matematic, dacă este chiar posibil.
Pentru a rezolva această problemă, ideea este să rulați o singură instanță a aplicației dvs. și să creșteți numărul de utilizatori care efectuează aceeași sarcină în mod repetat, până când sarcina serverului atinge maximul atribuit (dar nu peste). Apoi împărțiți la numărul de utilizatori și calculați timpul mediu al solicitării. Repetați această procedură cu fiecare acțiune pe care doriți să o integrați în funcția dvs. de încărcare a utilizatorului, adăugați ceva timp și gata.
Sunt conștient de faptul că această procedură implică luarea în considerare a faptului că fiecare cerere de utilizator are o încărcare constantă asupra procesării sale (ceea ce este evident incorect), dar masa de utilizatori va crea acest efect deoarece fiecare dintre ei nu se află în același pas de procesare în același timp . Deci, cred că aceasta este o aproximare acceptabilă, dar încă o dată insinuează că aveți de-a face cu un număr mare de utilizatori.
Puteți încerca și cu alte metode, cum ar fi graficele cu flacără CPU. Dar cred că va fi foarte dificil să creezi o formulă exactă care să lege acțiunile utilizatorilor de consumul de resurse.
Vă prezentăm app-autoscaling-calculator
Și acum, pentru aplicația web mică menționată de-a lungul: ia ca intrare funcția de încărcare, configurația orchestratorului containerului și alți parametri generali și returnează pragul de extindere și alte cifre legate de instanță.
Proiectul este găzduit pe GitHub, dar are și o versiune live disponibilă.
Iată rezultatul dat de aplicația web, rulat pe baza datelor de testare (pe Kubernetes):
Scalarea microserviciilor: gata de bătaie în întuneric
Când vine vorba de arhitecturi de aplicații pentru microservicii, implementarea containerelor devine un punct central al întregii infrastructuri. Și cu cât orchestratorul și containerele sunt mai bine configurate, cu atât timpul de rulare va fi mai fluid.
Aceia dintre noi din domeniul serviciilor DevOps cautam mereu modalitati mai bune de reglare a parametrilor de orchestrare pentru aplicatiile noastre. Să adoptăm o abordare mai matematică a microserviciilor cu scalare automată!