Analiza e performancës dhe optimizimi — një mjet i fuqishëm për verifikimin e përputhshmërisë së performancës për klientët.
Analiza e performancës mund të përdoret për të identifikuar ngushticat në program, duke përdorur një qasje shkencore për të vlerësuar eksperimentet e optimizimit. Ky artikull përcakton një qasje të përgjithshme për analizën e performancës dhe optimizimin duke përdorur një server web në Go si shembull.
Go është veçanërisht i përshtatshëm këtu, pasi ka mjete profilizimi. pprof në bibliotekën standarde.

Strategjia
Le të krijojmë një listë të përmbledhur për analizën tonë strukturore. Do të përpiqemi të përdorim disa të dhëna për të marrë vendime në vend që të bëjmë ndryshime të bazuara në intuitë ose supozime. Për këtë, do të bëjmë kështu:
- Përcaktojmë kufijtë e optimizimit (kërkesat);
- Llogarisim ngarkesën transaksionale për sistemin;
- Kryejmë test (krijojmë të dhëna);
- Vëzhgojmë;
- Analizojmë — a përmbushen të gjitha kërkesat?
- Optimizojmë shkencërisht, formulojmë një hipotezë;
- Kryejmë eksperimentin për të testuar këtë hipotezë.

Arkitektura e një serveri HTTP të thjeshtë
Për këtë artikull do të përdorim një server të vogël HTTP në Golang. Të gjithë kodin nga ky artikull mund ta gjeni .
Aplikacioni që analizojmë është një server HTTP, i cili pyet Postgresql për çdo kërkesë. Përveç kësaj, kemi Prometheus, node_exporter dhe Grafana për të mbledhur dhe vizualizuar metrikat e aplikacionit dhe sistemit.

Për thjeshtësinë, le të supozojmë se për shkallëzimin horizontal (dhe për thjeshtësimin e llogaritjeve) çdo shërbim dhe bazë të dhënash vendoset së bashku:

Përcaktojmë qëllimet
Në këtë hap, përcaktojmë qëllimin. Çfarë po përpiqemi të analizojmë? Si do ta dimë kur është koha për të përfunduar? Në këtë artikull do të supozojmë se kemi klientë dhe se shërbimi ynë do të përpunojë 10,000 kërkesa në sekondë.
Në shpjegon në detaje metodat e zgjedhjes dhe modelimit. Do të veprojmë njësoj, do të ndërtojmë modele:
- Mungesa: 99% e kërkesave duhet të përfundojnë për më pak se 60ms;
- Kostoja: shërbimi duhet të konsumojë një shumë minimale parash, e cila na duket e arsyeshme. Për këtë, maksimizojmë kapacitetin.
- Planifikimi i kapaciteteve: kërkohet të kuptohet dhe të dokumentohet se sa instanca të aplikacionit do të nevojiten për të funksionuar, duke përfshirë funksionalitetin e përgjithshëm të shtrirjes, si dhe sa instanca do të kërkohen për të përmbushur kërkesat fillestare për ngarkesën dhe për të siguruar .
Kërkesa për vonesë mund të kërkojë optimizim përveç analizës, por kapaciteti sigurimisht duhet të analizohet. Kur përdoret procesi SRE SLO, kërkesa për vonesë vjen nga klienti ose biznesi, të pasqyruara nga pronari i produktit. Shërbimi ynë do ta plotësojë këtë angazhim që në fillim pa cilësime të tjera!
Konfigurojmë mjedisin e testit
Me anë të mjedisit të testit do të mund të ofrojmë një ngarkesë të matur në sistemin tonë. Do të krijohen të dhëna për analizimin e performancës së shërbimit web.
Ngarkesa transaksionale
Ky mjedis përdor për të krijuar një frekuencë të personalizueshme të kërkesave për HTTP, derisa të ndalet:
$ 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 reportVëzhgimi
Gjatë ekzekutimit do të aplikohet ngarkesa transaksionale. Së bashku me metrikat e aplikacionit (numri i kërkesave, vonesat në përgjigje) dhe të sistemit operativ (memoria, CPU, IOPS) do të niset profilizimi i aplikacionit, për të kuptuar ku ka probleme dhe si konsumohen koha e procesorit.
Profilizimi
Profilizimi është një lloj matjeje që lejon të shikohet ku shkon koha e procesorit gjatë funksionimit të aplikacionit. Ai lejon të përcaktohet ku dhe sa kohë e procesorit po përdoret:

Këto të dhëna mund të përdoren gjatë analizës për të marrë një ide mbi kohën e procesorit të shpenzuar kot dhe punën e panevojshme që po kryhet. Go (pprof) mund të gjenerojë profile dhe t'i vizualizojë ato si grafikë flakë, duke përdorur një grup standard mjetesh. Do t'ju flas për përdorimin e tyre dhe udhëzimin për konfigurim pak më poshtë në artikull.
Ekzekutimi, vëzhgimi, analiza.
Do të kryejmë një eksperiment. Ne do të realizojmë, vëmë në dukje dhe analizojmë derisa performanca të jetë e kënaqshme. Do të zgjedhim një ngarkesë të ulët rastësisht, për ta aplikuar atë për të marrë rezultatet e vrojtimeve të para. Në çdo hap të ardhshëm do të rrisim ngarkesën me një koeficient shkallëzimi të zgjedhur me një farë shpërndarjeje. Çdo ekzekutim i testit të ngarkesës bëhet me rregullimin e numrit të kërkesave: bëni load-test LOAD_TEST_RATE=X.
50 kërkesa në sekondë

Vini re dy grafikat e sipërme. Grafik i majtë i sipërm tregon se aplikacioni ynë përpunon 50 kërkesa në sekondë (në mendimin e tij), ndërsa grafik i djathtë i sipërm tregon kohën e çdo kërkese. Të dy parametrat na ndihmojnë të shikojmë dhe analizojmë: a po përputhemi me kufijtë tanë të performancës apo jo. Vijën e kuqe në grafik HTTP Kërkesa Latencë tregon SLO në 60ms. Nga vija, është e qartë se jemi dukshëm më poshtë kohës maksimale të përgjigjes tonë.
Le të shohim nga këndvështrimi i kostos:
10000 kërkesa në sekondë / për 50 kërkesa për server = 200 servera + 1
Ne ende mund ta përmirësojmë këtë tregues.
500 kërkesa në sekondë
Gjërat bëhen më interesante kur ngarkesa arrin 500 kërkesa në sekondë:

Gjithashtu në grafikun e majtë të sipërm mund të shihni që aplikacioni regjistron ngarkesën normale. Nëse nuk është kështu - ka një problem me serverin ku është e vendosur aplikacioni. Grafik me latencën e përgjigjes pozicionohet lart djathtas, tregon se 500 kërkesa në sekondë çuan në një vonesë përgjigjeje prej 25-40ms. Percentili 99 ende është brenda SLO 60ms, i zgjedhur më sipër.
Nga këndvështrimi i kostos:
10000 kërkesa në sekondë / për 500 kërkesa për server = 20 servera + 1
Akoma mund të përmirësojmë.
1000 kërkesa në sekondë

Një nisje e shkëlqyer! Aplikacioni tregon se ka përpunuar 1000 kërkesa në sekondë, megjithatë kufiri i vonesës u shkel nga ana e SLO-s. Kjo duket nga vija p99 në grafikun e djathtë të sipërm. Edhe pse vija p100 është shumë më lart, vonesat reale janë mbi maksimumin prej 60ms. Le të thellohemi në profilizim për të kuptuar se çfarë bën vërtet aplikacioni.
Profilizimi
Për profilizimin vendosim ngarkesën në 1000 kërkesa në sekondë, pastaj përdorim pprof për të tërhiqur të dhënat, për të kuptuar se ku aplikacioni po përton kohën e procesorit. Kjo mund të bëhet duke aktivizuar HTTP endpoint pprof, pastaj, nën ngarkesë, ruani rezultatet me curl:
$ curl http://localhost:8080/debug/pprof/profile?seconds=29 > cpu.1000_reqs_sec_no_optimizations.profRezultatet mund të paraqiten kështu:
$ go tool pprof -http=:12345 cpu.1000_reqs_sec_no_optimizations.prof
Grafiku tregon se ku dhe sa kohë procesori konsumon aplikacioni. Nga përshkrimi i :
Në boshtin X - mbushja e profilit të këtij staku, e renditur në rend alfabetik (kjo nuk është koha), boshti Y tregon thellësinë e stack-ut, duke filluar nga zero në [top]. Çdo drejtkëndësh është një kornizë e stack-ut. Sa më e gjerë të jetë korniza, aq më shpesh ajo është e pranishme në stack-e. Ajo që është lart punon me CPU-në, ndërsa më poshtë janë elementet fëmijë. Ngjyrat zakonisht nuk kanë asnjë domethënie dhe thjesht zgjidhen rastësisht për të dalluar kornizat.
Analiza - hipotezë
Për konfigurimin do të përqendrohemi në përpjekjen për të gjetur shpenzime të pavlera të kohës së procesorit. Do të kërkojmë burimet më të mëdha të shpenzimeve të pavlera dhe do t'i heqim ato. Dhe, duke pasur parasysh se profilizimi zbulon me saktësi se ku aplikacioni e shpenzon kohën e tij në procesor, ndoshta duhet ta bëjmë këtë disa herë, si dhe do të ketë nevojë të ndryshojmë kodin burimor të aplikacionit, të rinisnim testet dhe të shikonim se si performanca po afron me atë që është planifikuar.
Duke ndjekur rekomandimet e Brendan Gregg do të lexojmë grafikun nga lart poshtë. Çdo rresht paraqet një kornizë të stack-ut (thirrje funksioni). Rreshti i parë është pika e hyrjes në program, prindi i të gjitha thirrjeve të tjera (në terma të tjerë, të gjitha thirrjet e tjera do ta kenë atë në stack-un e tyre). Rreshti tjetër tashmë është ndryshe:
![]()
Nëse vendosni kursorin mbi emrin e funksionit në grafik, do të shfaqet koha totale që ka qenë në stack gjatë debugging. Funksioni HTTPServe ka qenë aty 65% të kohës, funksionet e tjera runtime, runtime.mcall, mstart dhe gc, morën kohën e mbetur. Një fakt interesant: 5% e kohës totale është shpenzuar për kërkesa në DNS:

Adresat që programi e kërkon i përkasin Postgresql. Klikoni mbi FindByAge:

Është interesante, programi tregon se në parim ka tre burime kryesore që shtojnë vonesa: hapja dhe mbyllja e lidhjeve, kërkesa për të dhëna dhe lidhja me bazën e të dhënave. Në grafik duket se kërkesat në DNS, hapja dhe mbyllja e lidhjeve zënë rreth 13% të gjithë kohës së ekzekutimit.
Hipopeza: Rimëkëmbja e lidhjeve me një grup duhet të reduktojë kohën e një kërkese HTTP, duke lejuar kapacitet më të lartë dhe vonesa më të ulëta..
Konfigurimi i aplikacionit - eksperimenti
Po azhurnojmë kodin burimor, duke provuar të heqim lidhjen me Postgresql për çdo kërkesë. Opsioni i parë - përdorimi në nivelin e aplikacionit. Në këtë eksperiment ne grupin e lidhjeve me ndihmën e drejtuesit sql për go:
db, err := sql.Open("postgres", dbConnectionString)
db.SetMaxOpenConns(8)
if err != nil {
return nil, err
}Ekzekutimi, vëzhgimi, analiza
Pasi rifilluam testin me 1000 kërkesa në sekondë, u pa se p99 për vonesat arriti në normë me SLO 60ms!
Sa kushton?
10000 kërkesa në sekondë / për 1000 kërkesa për server = 10 serverë + 1
Le të bëjmë edhe më mirë!
2000 kërkesa në sekondë

Dyfishimi i ngarkesës tregon të njëjtën gjë, grafiku në këndin e majtë të sipërm tregon se aplikacioni është në gjendje të përpunojë 2000 kërkesa në sekondë, p100 më i ulët se 60ms, p99 i plotëson SLO.
Nga këndvështrimi i kostos:
10000 kërkesa në sekondë / për 2000 kërkesa për server = 5 serverë + 1
3000 kërkesa në sekondë

Këtu aplikacioni mund të përpunojë 3000 kërkesa me vonesë p99 më pak se 60ms. SLO nuk është shkelur, dhe kostoja pranohet si:
10000 kërkesa në sekondë / për 3000 kërkesa për server = 4 serverë + 1 (autori e rroundi në të madhën, shën. e përkthyesit)
Le të provojmë një raund tjetër analize.
Analiza - hipotezë
Mblidhen dhe shfaqen rezultatet e debuggingut të aplikacionit me 3000 kërkesa në sekondë:

Ende 6% e kohës shpenzohet në vendosjen e lidhjeve. Konfigurimi i grupit përmirësoi performancën, por është akoma e qartë se aplikacioni vazhdon të punojë për të krijuar lidhje të reja me bazën e të dhënave.
Hipopeza: Lidhjet, pavarësisht se ka një grup, vazhdojnë të hidhen poshtë dhe të çlirohen, prandaj aplikacioni duhet t'i ribëjë ato. Vendosja e numrit të lidhjeve në pritje në madhësinë e grupit duhet të ndihmojë me vonesën duke minimizuar kohën që aplikacioni shpenzon për të krijuar një lidhje..
Konfigurimi i aplikacionit - eksperimenti
Po provojmë të vendosim të barabartë me madhësinë e grupit (të përshkruar gjithashtu ):
db, err := sql.Open("postgres", dbConnectionString)
db.SetMaxOpenConns(8)
db.SetMaxIdleConns(8)
if err != nil {
return nil, err
}Ekzekutimi, vëzhgimi, analiza
3000 kërkesa në sekondë

p99 më pak se 60ms me një p100 të konsiderueshëm më të ulët!

Kontrolli i grafikut të flakës tregon se vendosja e lidhjes nuk është më e dukshme! Le të kontrollojmë më hollësisht pg(*conn).query — gjithashtu nuk e vërejmë vendosjen e lidhjes këtu.

Përfundim
Analiza e performancës është thelbësore për të kuptuar nëse pritshmëritë e klientëve dhe kërkesat jo funksionale janë plotësuar. Analiza e bërë duke krahasuar vëzhgimet me pritshmëritë e klientëve mund të ndihmojë në përcaktimin e asaj që është e pranueshme dhe asaj që nuk është. Go ofron mjete efikase, të integruara në bibliotekën standarde, me anë të të cilave analiza bëhet e thjeshtë dhe e aksesueshme.
Burimi: habr.com
