Prestandaanalys och optimering â ett kraftfullt verktyg för att kontrollera prestandamatchning för kunder.
Prestandaanalys kan anvÀndas för att identifiera flaskhalsar i programmet, med en vetenskaplig ansats vid utvÀrdering av konfigurationsexperiment. Denna artikel definierar en generell metod för prestandaanalys och optimering med en webbserver skriven i Go som exempel.
Go Àr sÀrskilt vÀl lÀmpad för detta eftersom den erbjuder profilering verktyg. pprof i standardbiblioteket.

Strategi
LÄt oss skapa en sammanstÀllning för vÄr strukturella analys. Vi kommer att försöka anvÀnda viss data för beslutsfattande istÀllet för att göra Àndringar baserat pÄ intuition eller gissningar. För detta gör vi sÄ hÀr:
- Definiera optimeringsgrÀnser (krav);
- BerÀkna transaktionsbelastningen för systemet;
- Genomför test (skapa data);
- Observera;
- Analysera â uppfylls alla krav?
- Optimera vetenskapligt, formulera en hypotes;
- Genomför experiment för att verifiera denna hypotes.

Arkitektur för en enkel HTTP-server
För denna artikel kommer vi att anvÀnda en liten HTTP-server pÄ Golang. All kod frÄn denna artikel kan hittas .
Det analyserade programmet Àr en HTTP-server som frÄgar Postgresql vid varje begÀran. Dessutom finns Prometheus, node_exporter och Grafana för insamling och visning av applikations- och systemmetrik.

För enkelhetens skull antar vi att för horisontell skalning (och förenkling av berÀkningar) varje tjÀnst och databas rullas ut tillsammans:

Definiera mÄl
I det hÀr steget definierar vi oss sjÀlva med mÄlet. Vad försöker vi analysera? Hur vet vi nÀr det Àr dags att avsluta? I denna artikel förestÀller vi oss att vi har kunder och att vÄr tjÀnst ska hantera 10 000 begÀrningar per sekund.
I diskuterar detaljerat val och modellering. Vi följer samma tillvÀgagÄngssÀtt, bygger modeller:
- Fördröjning: 99% av begÀrningarna ska slutföras pÄ mindre Àn 60 ms;
- Kostnad: tjÀnsten ska förbruka den minsta summa pengar som vi anser rimligt möjlig. För detta maximerar vi genomströmningen;
- Kapacitetsplanering: det krÀvs en förstÄelse och dokumentation av hur mÄnga instanser av applikationen som behöver startas, inklusive den övergripande skalningsfunktionen, samt hur mÄnga instanser som krÀvs för att möta de initiala belastningskraven och sÀkerstÀlla .
Fördröjningen kan krÀva optimering utöver analysen, men bandbredden mÄste uppenbarligen analyseras. Vid anvÀndning av SRE-processen kommer kravet pÄ fördröjning frÄn kunden och/eller affÀren, representerade av produktÀgaren. Och vÄr tjÀnst kommer att uppfylla detta Ätagande frÄn början utan nÄgra justeringar!
Konfigurera testmiljö
Med hjÀlp av testmiljön kan vi generera en kontrollerad belastning pÄ vÄrt system. Det kommer att skapas prestandadata om webbtjÀnsten för analys.
Transaktionell belastning
Denna miljö anvÀnder för att skapa anpassningsbar HTTP-förfrÄgningsfrekvens tills det stoppas:
$ 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 reportĂvervakning
Under körningen kommer en transaktionell belastning att tillÀmpas. Utöver applikationsmetrik (antal förfrÄgningar, svarsfördröjningar) och operativsystemmetrik (minne, CPU, IOPS) kommer applikationsprofilering att genomföras för att förstÄ var det finns problem samt hur processor-tid konsumeras.
Profilering
Profilering Àr en typ av mÀtning som lÄter oss se vart processor-tid gÄr nÀr applikationen körs. Det gör det möjligt att avgöra exakt var och hur mycket processor-tid som anvÀnds:

Data kan anvÀndas under analysen för att fÄ insikt om onödigt förlorad processor-tid och onödigt utförd arbete. Go (pprof) kan generera profiler och visualisera dem i form av flame graph, med hjÀlp av standarduppsÀttningen av verktyg. Jag kommer att berÀtta om deras anvÀndning och konfigurationsguide lite lÀngre ner i artikeln.
Utförande, övervakning, analys.
Vi genomför ett experiment. Vi kommer att utföra, observera och analysera tills vi Àr nöjda med prestandan. Vi vÀljer en godtyckligt lÄg belastning för att tillÀmpa den för att fÄ resultat frÄn de första observationerna. Vid varje efterföljande steg kommer vi att öka belastningen med en viss skalningsfaktor, vald med en viss spridning. Varje körning av belastningstestningen utförs med justering av antalet förfrÄgningar: make load-test LOAD_TEST_RATE=X.
50 förfrÄgningar per sekund

Observera de tvÄ övre graferna. Den övre vÀnstra visar att vÄr applikation hanterar 50 förfrÄgningar per sekund (enligt vad den tror), och den övre högra visar varaktigheten för varje förfrÄgan. BÄda parametrarna hjÀlper oss att se och analysera: gÄr vi inom vÄra prestandagrÀnser eller inte. Den röda linjen i grafen HTTP Request Latency visar SLO pÄ 60 ms. Enligt linjen ligger vi lÄngt under vÄr maximala svarstid.
LÄt oss titta pÄ kostnadssidan:
10000 förfrÄgningar per sekund / pÄ 50 förfrÄgningar per server = 200 servrar + 1
Vi kan fortfarande förbÀttra detta vÀrde.
500 förfrÄgningar per sekund
Mer intressanta saker börjar hÀnda nÀr belastningen Àr 500 förfrÄgningar per sekund:

à terigen i den vÀnstra övre grafen ser vi att applikationen registrerar en normal belastning. Om inte, finns det ett problem pÄ servern dÀr applikationen Àr igÄng. Grafen med svarsfördröjning finns högst upp till höger och visar att 500 förfrÄgningar per sekund ledde till svarsfördröjningar pÄ 25-40 ms. 99:e percentilen faller fortfarande inom SLO pÄ 60 ms, som valdes ovan.
FrÄn kostnadssynpunkt:
10000 förfrÄgningar per sekund / pÄ 500 förfrÄgningar per server = 20 servrar + 1
Det finns fortfarande utrymme för förbÀttring.
1000 förfrÄgningar per sekund

En fantastisk körning! Applikationen visar att den har hanterat 1000 förfrÄgningar per sekund, men grÀnsen för fördröjning har överskridits enligt SLO. Detta kan ses pÄ p99-linjen i den övre högra grafen. Trots att p100-linjen Àr mycket högre, Àr de faktiska fördröjningarna över maximivÀrdet pÄ 60 ms. LÄt oss dyka ner i profileringen för att ta reda pÄ vad applikationen faktiskt gör.
Profilering
För profilering sÀtter vi belastningen pÄ 1000 förfrÄgningar per sekund, och sedan anvÀnder vi pprof för att samla in data för att ta reda pÄ var applikationen spenderar processor tid. Detta kan göras genom att aktivera HTTP-endpointen. pprof, efter vilket du kan spara resultaten under belastning med curl:
$ curl http://localhost:8080/debug/pprof/profile?seconds=29 > cpu.1000_reqs_sec_no_optimizations.profResultaten kan visas sÄ hÀr:
$ go tool pprof -http=:12345 cpu.1000_reqs_sec_no_optimizations.prof
Grafen visar var och hur mycket CPU-tid applikationen anvÀnder. Enligt beskrivningen av :
X-axeln representerar stapelfyllnaden, sorterad alfabetiskt (detta Àr inte tid), Y-axeln visar stapeldjupet, rÀknat frÄn noll i [top]. Varje rektangel Àr en stapelram. Ju bredare ramen Àr, desto oftare förekommer den i stacker. Det som Àr högst upp arbetar pÄ CPU, medan det lÀngre ner Àr underordnade element. FÀrgerna har oftast ingen betydelse utan vÀljs slumpmÀssigt för att sÀrskilja ramar.
Analys - hypotes
För instÀllningen kommer vi att fokusera pÄ att försöka hitta meningslösa CPU-ÄtgÄngar. Vi ska leta efter de största kÀllorna till meningslös ÄtgÄng och ta bort dem. Med tanke pÄ att profileringen tydligt visar var applikationen anvÀnder sin CPU-tid kan det hÀnda att detta behöver göras flera gÄnger, och det kan krÀvas att Àndra applikationens kÀllkod, starta om tester och observera att prestandan nÀrmar sig planen.
Följande Brendan Greggs rekommendationer kommer vi att lÀsa grafen uppifrÄn och ner. Varje rad visar en stapelram (funktionsanrop). Första raden Àr intrÀdespunkten i programmet, förÀlder till alla andra anrop (med andra ord kommer alla andra anrop att ha den i sin stack). NÀsta rad Àr redan annorlunda:
![]()
Om du för musen över funktionsnamnet i grafen visas den totala tiden som den var i stacken under felsökningen. Funktionen HTTPServe spenderade dÀr 65% av tiden, andra funktioner runtime.mcall, mstart och gc, tog upp resterande tid. En intressant detalj: 5% av den totala tiden spenderades pÄ DNS-förfrÄgningar:

Adresserna som programmet söker tillhör Postgresql. Vi klickar pÄ FindByAge:

Intressant nog visar programmet att det finns tre huvudsakliga kÀllor som bidrar till latensen: öppning/stÀngning av anslutningar, datainhÀmtning och anslutning till databasen. Grafen visar att DNS-förfrÄgningar, öppning och stÀngning av anslutningar tar cirka 13% av den totala körningstiden.
Hypotes: à teranvÀndning av anslutningar med hjÀlp av en pool bör minska tiden för en enskild HTTP-förfrÄgan, vilket möjliggör högre kapacitet och lÀgre latens..
AppinstĂ€llning â experiment.
Vi uppdaterar kĂ€llkoden, testar att ta bort anslutningen till Postgresql för varje begĂ€ran. Första alternativet â anvĂ€ndning av pĂ„ applikationsnivĂ„. I detta experiment kommer vi anslutningspoolen med hjĂ€lp av sql-drivrutinen för Go:
db, err := sql.Open("postgres", dbConnectionString)
db.SetMaxOpenConns(8)
if err != nil {
return nil, err
}Utförande, övervakning, analys.
Efter omstart av testet med 1000 förfrÄgningar per sekund Àr det tydligt att p99 av latenserna har kommit i norm med SLO 60 ms!
Vad kostar det?
10000 förfrÄgningar per sekund / pÄ 1000 förfrÄgningar per server = 10 servrar + 1.
LÄt oss göra det Ànnu bÀttre!
2000 förfrÄgningar per sekund.

En dubblering av belastningen visar samma sak, den övre vÀnstra grafen visar att applikationen lyckas hantera 2000 förfrÄgningar per sekund, p100 Àr under 60 ms, p99 uppfyller SLO.
FrÄn kostnadssynpunkt:
10000 förfrÄgningar per sekund / pÄ 2000 förfrÄgningar per server = 5 servrar + 1.
3000 förfrÄgningar per sekund.

HÀr kan applikationen hantera 3000 förfrÄgningar med en p99-latens under 60 ms. SLO bryts inte, och kostnaden tas som:
10000 förfrÄgningar per sekund / pÄ 3000 förfrÄgningar per server = 4 servrar + 1. (författaren avrundade uppÄt, översÀttarens anm.)
LÄt oss göra ett ytterligare analysomgÄng.
Analys - hypotes
Vi samlar in och visar resultaten frÄn avkodningen av applikationen pÄ 3000 förfrÄgningar per sekund:

Ănnu 6% av tiden spenderas pĂ„ att etablera anslutningar. InstĂ€llningen av poolen har förbĂ€ttrat prestandan, men det Ă€r fortfarande tydligt att applikationen fortsĂ€tter att skapa nya anslutningar till databasen.
Hypotes: Anslutningar, trots existensen av en pool, avvisas och stÀdas fortfarande, sÄ applikationen mÄste Äteretablera dem. InstÀllningen av antalet vÀntande anslutningar till poolens storlek bör hjÀlpa till med latensen genom att minimera tiden som applikationen spenderar pÄ att skapa en anslutning..
AppinstĂ€llning â experiment.
Vi försöker stÀlla in lika med poolens storlek (som Àven beskrivs i ):
db, err := sql.Open("postgres", dbConnectionString)
db.SetMaxOpenConns(8)
db.SetMaxIdleConns(8)
if err != nil {
return nil, err
}Utförande, övervakning, analys.
3000 förfrÄgningar per sekund.

p99 under 60 ms med betydligt lÀgre p100!

Kontrollen av flame graph visar att anslutningen inte lĂ€ngre Ă€r mĂ€rkbar! LĂ„t oss undersöka nĂ€rmare. pg(*conn).query â vi mĂ€rker inte att anslutningen etableras hĂ€r heller.

Slutsats
Prestandaanalys Àr avgörande för att förstÄ om kundernas förvÀntningar och icke-funktionella krav uppfylls. Genom att jÀmföra observationer med kundernas förvÀntningar kan man avgöra vad som Àr acceptabelt och vad som inte Àr det. Go erbjuder effektiva verktyg, inbyggda i standardbiblioteket, som gör analysen enkel och tillgÀnglig.
KĂ€lla: habr.com
