Analiza performanței și configurarea reprezintă un instrument puternic de verificare a conformității performanței pentru clienți.
Analiza performanței poate fi utilizată pentru a verifica sticlele de gât dintr-o aplicație, aplicând o abordare științifică în testarea experimentelor de configurare. Acest articol definește o abordare generală pentru analiza performanței și configurarea, folosind ca exemplu un server web în Go.
Go se potrivește foarte bine aici, deoarece are instrumente de profilare pprof în biblioteca standard.

Strategia
Să creăm o listă rezumată pentru analiza noastră structurală. Vom încerca să folosit date pentru a lua decizii, în loc să facem modificări bazate pe intuiție sau presupuneri. În acest scop, să procedăm astfel:
- Stabilim limitele optimizării (cerințele);
- Calculăm încărcătura tranzacțională pentru sistem;
- Executăm testul (creăm date);
- Observăm;
- Analizăm – sunt respectate toate cerințele?
- Configurăm științific, formulăm o ipoteză;
- Executăm un experiment pentru a verifica această ipoteză.

Arhitectura unui server HTTP simplu
Pentru acest articol, vom folosi un server HTTP mic în Golang. Tot codul din acest articol poate fi găsit .
Aplicația analizată este un server HTTP care interoghează Postgresql pentru fiecare cerere. În plus, există Prometheus, node_exporter și Grafana pentru colectarea și afișarea metricilor aplicației și sistemului.

Pentru simplificare, presupunem că pentru scalarea orizontală (și pentru simplificarea calculelor) fiecare serviciu și baza de date sunt desfășurate împreună:

Stabilim obiectivele
În această etapă, ne definim obiectivele. Ce încercăm să analizăm? Cum știm când este timpul să ne oprim? În acest articol, vom prezenta că avem clienți și că serviciul nostru va procesa 10.000 de cereri pe secundă.
În sunt discutate în detaliu metodele de selecție și modelare. Vom proceda în același mod, construind modele:
- Latencă: 99% dintre cereri trebuie să fie finalizate în mai puțin de 60ms;
- Cost: serviciul trebuie să consume o sumă minimă de bani, care să ni se pară rezonabil posibilă. În acest scop, maximizăm capacitatea de procesare;
- Planificarea capacităților: este necesară înțelegerea și documentarea numărului de instanțe ale aplicației care trebuie lansate, inclusiv funcționalitatea generală de scalare, precum și câte instanțe sunt necesare pentru a satisface cerințele inițiale de încărcare și asigurarea .
Întârzierea poate necesita optimizare pe lângă analiză, dar lățimea de bandă trebuie să fie analizată în mod clar. În utilizarea procesului SRE SLO, cerința de întârziere provine de la client și/sau de la afacere, reprezentate de proprietarul produsului. Iar serviciul nostru va respecta această obligație de la început fără nicio configurare!
Configurarea mediului de testare
Prin intermediul mediului de testare, vom putea genera o încărcare dozată asupra sistemului nostru. Vor fi create date de performanță ale serviciului web pentru analiză.
Încărcare tranzacțională
Acest mediu folosește pentru a crea o frecvență personalizabilă a solicitărilor HTTP, până la oprire:
$ make load-test LOAD_TEST_RATE=50
echo "POST http://localhost:8080" | vegeta attack -body tests/fixtures/age_no_match.json -rate=50 -duration=0 | tee results.bin | vegeta reportObservație
În timpul execuției, va fi aplicată o încărcare tranzacțională. Pe lângă metricile aplicației (numărul de solicitări, întârzierea la răspuns) și sistemul de operare (memorie, CPU, IOPS), va fi activat profilarea aplicației pentru a înțelege unde sunt probleme și cum este consumat timpul de procesare.
Profilare
Profilarea este un tip de măsurare care arată unde se duce timpul de procesor în timpul funcționării aplicației. Aceasta permite determinarea locului și cantității de timp de procesor consumat:

Aceste date pot fi utilizate în timpul analizei pentru a obține o idee despre timpul de procesor irosit fără rost și munca inutilă efectuată. Go (pprof) poate genera profile și le poate vizualiza sub formă de flame graph, utilizând un set standard de instrumente. Voi explica utilizarea acestora și ghidul de configurare ceva mai jos în articol.
Execuție, observație, analiză.
Vom efectua un experiment. Vom executa, observa și analiza până când performanța ne va satisface. Vom alege o sarcină aleatorie scăzută pentru a o aplica în vederea obținerii rezultatelor primelor observații. La fiecare pas următor, vom crește sarcina cu un anumit coeficient de scalare, ales cu un anumit deviere. Fiecare runda de testare a sarcinii se va face cu reglarea numărului de cereri: make load-test LOAD_TEST_RATE=X.
50 de cereri pe secundă

Observați cele două grafice din partea de sus. Graficul din stânga arată că aplicația noastră procesează 50 de cereri pe secundă (potrivit părerii sale), iar graficul din dreapta sus arată durata fiecărei cereri. Ambele parametrii ne ajută să ne uităm și să analizăm: ne încăpem în limitele performanței noastre sau nu. Linia roșie de pe grafic Latenta cererilor HTTP arată SLO-ul la 60ms. De pe linie se observă că suntem mult sub timpul maxim de răspuns.
Să ne uităm din perspectiva costului:
10000 de cereri pe secundă / la 50 de cereri pe server = 200 de servere + 1
Încă putem îmbunătăți acest indicator.
500 de cereri pe secundă
Lucruri mai interesante încep să se întâmple când sarcina devine 500 de cereri pe secundă:

Din nou, pe graficul din stânga sus se observă că aplicația înregistrează o sarcină normală. Dacă nu este așa, există o problemă pe serverul pe care este rulată aplicația. Graficul de latență a răspunsului din dreapta sus, arată că 500 de cereri pe secundă au dus la o latență de răspuns de 25-40ms. Percentila 99 încă se încadrează superb în SLO-ul de 60ms ales anterior.
Din perspectiva costului:
10000 de cereri pe secundă / la 500 de cereri pe server = 20 de servere + 1
Încă mai este loc de îmbunătățire.
1000 de cereri pe secundă

O lansare excelentă! Aplicația arată că a procesat 1000 de cereri pe secundă, totuși limita de latență a fost depășită din partea SLO. Acest lucru este vizibil pe linia p99 de pe graficul din dreapta sus. Deși linia p100 este mult mai sus, întârzierile reale depășesc maximul de 60ms. Haideți să facem un profiling pentru a descoperi ce face, de fapt, aplicația.
Profilare
Pentru profiling, punem o sarcină de 1000 de cereri pe secundă, apoi folosim pprof pentru a extrage datele, pentru a afla unde își petrece aplicația timpul procesorului. Acest lucru se poate face activând endpoint-ul HTTP pprof, după care, la încărcare, salvăm rezultatele folosind curl:
$ curl http://localhost:8080/debug/pprof/profile?seconds=29 > cpu.1000_reqs_sec_no_optimizations.profRezultatele pot fi afișate astfel:
$ go tool pprof -http=:12345 cpu.1000_reqs_sec_no_optimizations.prof
Graficul arată unde și cât timp CPU-ul este utilizat de aplicație. Conform descrierii de la :
Pe axa X se află umplerea profilului de stivă, sortată în ordine alfabetică (aceasta nu este timpul), axa Y arată adâncimea stivei, numărând de la zero în [top]. Fiecare dreptunghi reprezintă un cadru al stivei. Cu cât cadrul este mai lat, cu atât este mai frecvent în stive. Ce este deasupra lucrează pe CPU, iar mai jos sunt elementele copil. Culorile, de obicei, nu au semnificație, ci sunt alese aleatoriu pentru a distinge cadrele.
Analiza - ipoteză
Pentru ajustare, ne vom concentra pe încercarea de a găsi cheltuieli inutile de timp CPU. Vom căuta cele mai mari surse de cheltuieli inutile și le vom elimina. De asemenea, având în vedere că profilarea dezvăluie foarte precis unde își cheltuie aplicația timpul CPU, este posibil să fie necesar să facem asta de mai multe ori, iar codul sursă al aplicației va necesita modificări, repornirea testelor și observarea cum performanța se apropie de cea dorită.
Urmând recomandările lui Brendan Gregg, vom citi graficul de sus în jos. Fiecare linie afișează un cadru al stivei (apel de funcție). Prima linie este punctul de intrare în program, părintele tuturor celorlalte apeluri (în alte cuvinte, toate celelalte apeluri vor avea aceasta în stiva lor). Linia următoare este deja diferită:
![]()
Dacă treci cursorul peste numele funcției de pe grafic - va fi afișat timpul total cât a fost ea în stivă în timpul depanării. Funcția HTTPServe a fost acolo 65% din timp, iar celelalte funcții runtime, runtime.mcall, mstart și gc, au ocupat restul timpului. Un fapt interesant: 5% din timpul total a fost cheltuit pe solicitările DNS:

Adresele pe care programul le caută aparțin Postgresql. Facem clic pe FindByAge:

Interesant, programul arată că sunt, în principiu, trei surse principale care adaugă întârzieri: deschiderea și închiderea conexiunilor, solicitarea datelor și conexiunea la baza de date. Pe grafic se vede că solicitările DNS, deschiderea și închiderea conexiunilor ocupă aproximativ 13% din timpul total de execuție.
Ipoteză: Reutilizarea conexiunilor prin intermediul unui pool ar trebui să scadă timpul unei solicitări HTTP, permițând o lățime de bandă mai mare și întârzieri mai mici..
Configurarea aplicației - experiment.
Actualizăm codul sursă, încercând să eliminăm conexiunea cu Postgresql la fiecare solicitare. Prima variantă - utilizarea la nivel de aplicație. În acest experiment, ne pool-ul de conexiuni folosind driverul sql pentru go:
db, err := sql.Open("postgres", dbConnectionString)
db.SetMaxOpenConns(8)
if err != nil {
return nil, err
}Executare, observație, analiză.
După restartarea testului cu 1000 de solicitări pe secundă, se observă că p99 pentru întârzieri a revenit la normal cu SLO de 60ms!
Ce costuri sunt implicate?
10000 de solicitări pe secundă / la 1000 de solicitări pe server = 10 servere + 1.
Să facem și mai bine!
2000 de solicitări pe secundă.

Dublarea sarcinii arată același lucru, graficul din colțul stânga sus arată că aplicația reușește să gestioneze 2000 de solicitări pe secundă, p100 sub 60ms, p99 care respectă SLO.
Din perspectiva costului:
10000 de solicitări pe secundă / la 2000 de solicitări pe server = 5 servere + 1.
3000 de solicitări pe secundă.

Aici, aplicația poate gestiona 3000 de solicitări cu o întârziere p99 mai mică de 60ms. SLO nu este încălcat, iar costul acceptat este:
10000 de solicitări pe secundă / la 3000 de solicitări pe server = 4 servere + 1. (autorul a rotunjit la cel mai apropiat număr superior, nota traducătorului)
Să încercăm un alt ciclu de analiză.
Analiza - ipoteză
Colectăm și afișăm rezultatele debugging-ului aplicației la 3000 de solicitări pe secundă:

Încă 6% din timp se cheltuie pe stabilirea conexiunilor. Configurarea pool-ului a îmbunătățit performanța, dar se observă în continuare că aplicația continuă să creeze conexiuni noi cu baza de date.
Ipoteză: Conexiunile, în ciuda existenței pool-ului, sunt încă pierdute și eliberate, așadar aplicația trebuie să le reinstaleze. Setarea numărului de conexiuni așteptate la dimensiunea pool-ului ar trebui să ajute la reducerea întârzierii prin minimizarea timpului pe care aplicația îl cheltuie pentru a crea o conexiune..
Configurarea aplicației - experiment.
Încercăm să setăm egal cu dimensiunea pool-ului (de asemenea, descrisă ):
db, err := sql.Open("postgres", dbConnectionString)
db.SetMaxOpenConns(8)
db.SetMaxIdleConns(8)
if err != nil {
return nil, err
}Executare, observație, analiză.
3000 de solicitări pe secundă.

p99 sub 60ms cu p100 semnificativ mai mic!

Verificarea flame graph arată că stabilirea conexiunii nu mai este vizibilă! Să verificăm în detaliu. pg(*conn).query — de asemenea, nu observăm stabilirea conexiunii aici.

Concluzie
Analiza performanței este esențială pentru a înțelege dacă așteptările clienților și cerințele non-funcționale sunt satisfăcute. Analiza prin compararea observațiilor cu așteptările clienților poate ajuta la definirea a ceea ce este acceptabil și ceea ce nu este. Go oferă instrumente eficiente, integrate în biblioteca standard, care fac analiza simplă și accesibilă.
Sursa: habr.com
