Verbeteringen in profilering in Go 1.18 · Felix Geisendörfer

Verbeteringen in profilering in Go 1.18 · Felix Geisendörfer


Verbeteringen in profilering in Go 1.18 · Felix Geisendörfer

Gepubliceerd:

Zonder twijfel zal Go 1.18 een van de meest opwindende releases worden sinds Go 1. Je hebt waarschijnlijk gehoord van belangrijke functies zoals generieke geneesmiddelen en fuzzing, maar daar gaat dit bericht niet over. In plaats daarvan zullen we het hebben over profilering en enkele opmerkelijke verbeteringen benadrukken waar we naar uit kunnen kijken.

Een punt van persoonlijke vreugde voor mij is dat ik heb kunnen bijdragen aan verschillende verbeteringen als onderdeel van mijn werk bij Datadog en dat deze onze nieuwe Connecting Go Profiling With Tracing-functionaliteit aanzienlijk zullen verbeteren.

Als u nog niet bekend bent met Go-profilering, kunt u ook onze Gids voor Go-profilering lezen voordat u in dit bericht duikt.

Betere CPU-profileringsnauwkeurigheid

Laten we beginnen met de belangrijkste profileringswijziging in Go 1.18: de CPU-profiler voor Linux is een stuk nauwkeuriger geworden. Vooral CPU-bursts op multi-coresystemen werden vroeger onderschat, maar dat is nu opgelost.

Deze wijziging gaat terug tot eind 2019 (go1.13) toen Rhys Hiltner GH 35057 opende om te melden dat hij een Go-service zag die gebruik maakte van 20 CPU-kernen volgens topmaar alleen 2.4 kernen volgens de CPU-profiler van Go. Toen hij dieper graafde, ontdekte Rhys dat dit probleem werd veroorzaakt door het wegvallen van POSIX-signalen in de kernel.

Om het probleem te begrijpen, is het de moeite waard om te weten dat de CPU-profiler van Go 1.17 bovenop de setitimer(2) syscall is gebouwd. Hierdoor kan de Go-runtime de Linux-kernel vragen om voor elke melding een melding te ontvangen 10ms het programma draait op een CPU. Deze meldingen worden afgeleverd als SIGPROF signalen. Elke levering van een signaal zorgt ervoor dat de kernel het programma stopt en de signaalhandlerroutine van de runtime aanroept op een van de gestopte threads. De signaalhandler neemt een stacktracering van de huidige goroutine en voegt deze toe aan het CPU-profiel. Dit alles kost minder dan ~10usec waarna de kernel de uitvoering van het programma hervat.

Dus wat was er mis? Nou, gezien de programmarapportage van Rhys 20 CPU-kernen die worden gebruikt door topzou de verwachte signaalsnelheid moeten zijn 2,000 per seconde. Het resulterende profiel bevatte echter slechts een gemiddelde van 240 stapelt sporen per seconde. Uit verder onderzoek met behulp van Linux Event Tracing bleek dat de verwachte hoeveelheid signal:signal_generate Er werden gebeurtenissen waargenomen, maar niet genoeg signal:signal_deliver evenementen. Het verschil tussen het genereren en afleveren van signalen lijkt in eerste instantie misschien verwarrend, maar is te wijten aan het feit dat standaard POSIX-signalen niet in de wachtrij staan, zie signal(7). Er kan slechts één signaal tegelijk in afwachting zijn van bezorging. Als een ander signaal van dezelfde soort wordt gegenereerd terwijl er al een signaal in behandeling is, wordt het nieuwe signaal verwijderd.

Dit verklaart echter nog steeds niet waarom er tijdens precies hetzelfde zoveel signalen zouden worden gegenereerd < ~10usec signaalafhandelingsvenster zodat ze boven op elkaar terecht zouden komen. Het volgende stukje van deze puzzel is dus het feit dat de resolutie van systeemaanroepen die de CPU-tijd meten, beperkt zijn tot de softwareklok van de kernel, die de tijd meet in jiffieszie tijd(7). De duur van een handomdraai hangt af van de kernelconfiguratie, maar is standaard 4ms (250Hz). Wat dit betekent is dat de kernel niet eens in staat is om de wens van Go te honoreren om elke keer een signaal te ontvangen 10ms. In plaats daarvan moet er worden afgewisseld 8ms En 12ms zoals te zien is in het onderstaande histogram (details).

Sinds afwisselend 8ms En 12ms lukt nog steeds 10ms gemiddeld genomen is het op zichzelf geen probleem. Wat echter een probleem is, is wat er gebeurt als bijv 10 threads draaien op verschillende CPU-kernen, waardoor ze worden gebruikt 40ms van CPU-tijd binnen een 4ms even raam. Dus wanneer de kernel zijn controles uitvoert, moet hij genereren 4 SIGPROF signalen tegelijk, waardoor ze op één na allemaal worden weggelaten in plaats van afgeleverd. Of met andere woorden, setitimer(2) – de draagbare API van de kernel die wordt geadverteerd voor profilering – blijkt ongeschikt te zijn voor het profileren van drukke multi-core systemen :(.

Echter, gezien het feit dat de man-pagina’s de korte beperkingen van CPU-tijdgerelateerde syscalls en de semantiek van procesgestuurde signalen uitleggen, is het niet helemaal duidelijk of dit door de kernelbeheerders als een bug zou worden herkend. Maar zelfs als setitimer(2) opgelost zou zijn, zou het lang duren voordat mensen hun kernels zouden upgraden, dus bracht Rhys het idee van gebruik naar voren timer_create(2) die boekhouding per thread van in behandeling zijnde signalen biedt.

Helaas kreeg het idee geen feedback van de Go-beheerders, dus het probleem bleef sluimeren totdat Rhys en ik er in mei 2021 opnieuw over begonnen te praten. op twitter. We besloten samen te werken, waarbij Rhys werkte aan de kritieke wijzigingen in de CPU-profiler, en ik aan een betere testdekking voor cgo en andere randgevallen, weergegeven in het onderstaande diagram.

Een andere bijzaak van mij was het creëren van een zelfstandig C-programma genaamd proftest om het signaalgeneratie- en leveringsgedrag van te observeren setitimer(2) En timer_create(2) wat een ander bias-probleem uit 2016 bevestigde: setitimer(2)De procesgerichte signalen zijn niet eerlijk verdeeld over CPU-consumerende threads. In plaats van één draad heeft de neiging om vastgeschroefd te raken en ontvangt minder signalen dan alle andere threads. Nu, om eerlijk te zijn, signal(7) stelt dat de kernel een willekeurige thread kiest waaraan procesgerichte signalen moeten worden afgegeven wanneer meerdere threads in aanmerking komen. Maar aan de andere kant vertoont macOS dit soort vooroordelen niet, en ik denk dat dit het punt is waarop ik moet stoppen met het maken van excuses namens de Linux-kernel :).

Maar het goede nieuws is dat, afgezien van de snelle resolutie, timer_create(2) lijkt niet te lijden aan een van de kwalen waar hij last van heeft setitimer(2). Dankzij signaalaccounting per thread levert het op betrouwbare wijze de juiste hoeveelheid signalen en vertoont het geen voorkeur voor bepaalde threads. Het enige probleem is dat timer_create(2) vereist dat de CPU-profiler op de hoogte is van alle threads. Dit is gemakkelijk voor threads die zijn gemaakt door de Go-runtime, maar wordt lastig wanneer cgo-code zijn eigen threads voortbrengt. Rhys-patch lost dit op door te combineren timer_create(2) En setitimer(2). Wanneer een signaal wordt ontvangen, controleert de signaalbehandelaar de oorsprong van het signaal en verwijdert dit als dit niet de beste signaalbron is die beschikbaar is voor de huidige thread. Bovendien heeft de patch ook een aantal slimme stukjes om vooringenomenheid tegen kortstondige threads te vermijden en is hij in het algemeen gewoon een geweldig stukje techniek.

Hoe dan ook, dankzij veel beoordelingen en feedback van de Go-beheerders is de Rhys-patch samengevoegd en geleverd met Go 1.18, waarmee GH 35057 en GH 14434 zijn hersteld. Mijn testcases hebben het niet gehaald, deels omdat het moeilijk is om niet-vliegende beweringen te doen over profileringsgegevens, maar vooral omdat we niet het risico wilden lopen dat die patches de aandacht van upstream zouden afleiden van de belangrijkste veranderingen zelf. Niettemin was deze open source community-samenwerking een van mijn favoriete computerervaringen van 2021!

Bugfixes voor Profiler-labels

Met Profiler-labels (ook wel pprof-labels of -tags genoemd) kunnen gebruikers willekeurige sleutel/waarde-paren associëren met de momenteel actieve goroutine, die worden overgenomen door onderliggende goroutines. Deze labels komen ook terecht in CPU- en Goroutine-profielen, waardoor deze profielen kunnen worden opgedeeld op basis van labelwaarden.

Bij Datadog gebruiken we profilerlabels voor Connecting Go Profiling With Tracing en als onderdeel van het werken aan deze functie heb ik er veel op getest. Dit zorgde ervoor dat ik stapelsporen in mijn profielen waarnam waaraan labels moesten zijn bevestigd, maar dat was niet het geval. Ik ging verder door het probleem te melden als GH 48577 en de oorzaak nader te onderzoeken.

Gelukkig bleek het probleem heel simpel. De CPU-profiler keek soms naar de verkeerde goroutine bij het toevoegen van het label en vereiste in wezen slechts een oplossing van één regel:

-cpuprof.add(gp, stk(:n))
+cpuprof.add(gp.m.curg, stk(:n))

Dit kan in het begin een beetje verwarrend zijn gp is de huidige goroutine en wijst meestal naar dezelfde plaats als gp.m.curg (de huidige goroutine van de draad gp loopt door). Deze twee zijn echter niet hetzelfde wanneer Go van stapel moet wisselen (bijvoorbeeld tijdens het verwerken van signalen of het wijzigen van de grootte van de stapel van de huidige goroutine), dus als het signaal op een ongelukkig moment arriveert, gp verwijst naar een puur interne goroutine die namens de gebruiker wordt uitgevoerd, maar waarbij de labels ontbreken die bij de huidige stacktracering horen stk. Blijkbaar is dit een veel voorkomend probleem, dus het is zelfs gedocumenteerd in de runtime/HACKING.md-handleiding.

Gezien de eenvoud van het probleem heb ik er een patch van één regel voor ingediend. De patch werd echter begroet met het klassieke OSS-antwoord: “Kun je er alsjeblieft een test voor schrijven?” – geleverd door Michael Pratt. Ik twijfelde aanvankelijk aan de ROI van zo’n testcase, maar ik had het niet méér mis kunnen hebben. Het was absoluut heel moeilijk om een ​​niet-flaky test te schrijven om het probleem aan te tonen, en daarom moest de oorspronkelijke versie van de patch worden teruggedraaid. Het proces van het implementeren en verbeteren van de testcase hielp Michael echter om twee extra problemen met betrekking tot labels te identificeren, die hij uiteindelijk allebei oploste:

CL 369741 heeft een fout opgelost die één-voor-één optrad bij het coderen van de eerste batch pprof-monsters, waardoor een klein aantal monsters werd getagd met de verkeerde labels.

CL 369983 heeft een probleem opgelost waardoor systeemgoroutines (bijvoorbeeld voor GC) labels overnamen wanneer ze voortkwamen uit gebruikersgoroutines.

Omdat alle drie de problemen zijn opgelost, worden profilerlabels een stuk nauwkeuriger in Go 1.18. De grootste impact zal zijn voor programma’s die veel cgo gebruiken, aangezien C-aanroepen op een aparte stapel draaien gp != gp.m.curg. Maar zelfs reguliere programma’s ervaren 3-4% ontbrekende labels in Go 1.17 zullen profiteren van deze verandering en volledige nauwkeurigheid bereiken.

Bugfix voor Stack Trace

Een andere profileringsgerelateerde bug die we bij Datadog hebben ontdekt, is GH 49171. Het probleem manifesteerde zich in delta-allocatieprofielen met negatieve allocatietellingen. Toen we dieper gingen graven, kwamen we niet-deterministische programmateller-symbolisatie (pc) tegen als hoofdoorzaak. Wat dit betekent is dat exact dezelfde stacktracering (dezelfde programmatellers) verschillende symbolen (bijvoorbeeld bestandsnamen) zou bevatten in profielen die op een ander tijdstip zijn gemaakt, wat onmogelijk zou moeten zijn.

De bug was ook heel moeilijk zelfstandig te reproduceren, en het kostte me bijna een week voordat het lukte en het upstream rapporteerde. Uiteindelijk bleek het een compiler-regressie te zijn met inline sluitingen in Go 1.17, die werd opgelost door Than McIntosh.

Epiloog

Zoals u kunt zien, bevat Go 1.18 niet alleen boordevol opwindende nieuwe functies, maar ook een aantal belangrijke verbeteringen in de profilering. Laat het me weten als ik iets heb gemist of als je vragen of feedback hebt.

Bedankt voor het lezen en dank aan mijn collega Nick Ripley voor het beoordelen van dit bericht.

— Felix Geisendörfer


Abonneer u op deze blog via RSS of e-mail of ontvang kleine updates van mij via Twitteren.





Source link

Postagens Similares

Deixe um comentário

O seu endereço de email não será publicado. Campos obrigatórios marcados com *