Preskočiť na hlavný obsah
OpenAI

30. júna 2026

Technika

Epidemiológia core dumpov: oprava 18-ročnej chyby

Použitie populačnej analýzy na ladenie zložitých pádov v našej dátovej infraštruktúre.

Načítava sa…

Modely a agenti OpenAI sa čoraz viac spoliehajú na škálovateľnú dátovú infraštruktúru, aby mohli pri inferencii hľadať relevantné dáta: teda keď modely premýšľajú o vašej otázke. Niektoré z týchto služieb sú napísané v C++, ktorého nízkoúrovňová kontrola nad systémom nám umožňuje maximalizovať výkon a minimalizovať využitie pamäte. Tieto výhody efektívnosti sú pri škálovaní dôležité, no chýbajúca pamäťová bezpečnosť C++ znamená, že chyby môžu spôsobiť pády zápisom na nesprávne alebo neexistujúce adresy pamäte.

Pred niekoľkými mesiacmi sme zaznamenali pády v službe Rockset, špeciálnej súčasti našej dátovej infraštruktúry ChatGPT, ktorá je kľúčová pre mnohé dátové pluginy aj vyhľadávanie v konverzáciách. Pri každom z týchto pádov sa zdalo, že bežná funkcia C++ skončila a potom sa vrátila na falošnú adresu, takže kernel program zastavil, lebo ukazovateľ inštrukcie už neukazoval na kód. Niekedy bol slot návratovej adresy v rámci zásobníka NULL. Niekedy sa zasa zdalo, že samotný CPU register ukazovateľa zásobníka je posunutý o 8 bajtov, akoby sa %rsp počas normálneho behu nejako dekrementoval. V oboch prípadoch nastal pád pri návrate.

Toto nie sú bežné režimy zlyhania aplikačného kódu. Zatúlaný zápis, ktorý trafí iba uloženú návratovú adresu, je možný, no mimoriadne nepravdepodobný. Chyba, ktorá posunie %rsp o 8 bez inline assemblera, setcontext alebo longjmp (nič z toho nepoužívame), je ešte zvláštnejšia, lebo skompilovaný kód tento register priamo upravuje iba v prológu a epilógu funkcie. Proti každej hypotéze, ktorá nám (alebo ChatGPT) napadla, hovorili silné dôkazy, takže chyba pôsobila nemožne.

To, čo sme považovali za jeden problém, sa napokon ukázalo ako dve nesúvisiace chyby, náhodou objavené v rovnakom čase. Po prvé, tichá hardvérová korupcia na jednom hostiteľovi Azure, kde CPU jednoducho nepočítal správne. Po druhé, 18 rokov stará race condition v GNU libunwind, nepovšimnutá chyba v široko používanej open source knižnici.

Tento príspevok opisuje, ako sme zdanlivo nevysvetliteľné pády identifikovali a opravili tým, že sme uvažovali ako epidemiológovia a vytvorili kvalitný súbor dát o celej populácii pádov.

Prvý pokus o ladenie: dôkladná kontrola niekoľkých core dumpov

Najprv sa pozrime hlbšie na Rockset. Je to cloud-native dátový systém na vyhľadávanie a analytiku v reálnom čase, ktorý v OpenAI používame na mnohé interné prípady použitia, napríklad synchronizačné konektory (Rockset získala OpenAI v roku 2024). Streamované aktualizácie udržiavajú aktuálny index znalostnej bázy pracovného priestoru, aby ChatGPT mohol pri odpovedaní na otázky alebo vykonávaní akcií hľadať relevantné informácie.

Exekučná vrstva Rocksetu je napísaná v C++. Jazyk C++ poskytuje nízkoúrovňový prístup k CPU, čo je dobré pre výkon a efektívnosť, no znamená to, že aplikačné chyby môžu viesť k neplatným prístupom do pamäte a segfaultom. Aby sme ich vedeli vystopovať, používame fatal signal handler z folly, ktorý pri páde zapíše stopu zásobníka, a príslušné core dumpy (snímky stavu programu pri páde) nahrávame do Azure Blob Storage na neskoršiu analýzu. Všetky listové uzly spracovania dopytov v Rocksete sú replikované, čo minimalizuje vplyv pádu na klienta. Každý segfault však zodpovedá chybe, ktorú treba opraviť, aby sme splnili naše ciele spoľahlivosti a kvality.

Náš pôvodný prístup bol brať tieto core dumpy ako bežný problém ladenia: veľmi dôkladne preskúmať niekoľko dumpov, tvoriť hypotézy a jednu po druhej ich vylučovať.

Väčšina pádov nastala v metóde s názvom DocumentTree::updateDocument. Pri týchto pádoch to vyzeralo, že updateDocument zavolala neznámu funkciu X, zásobník sa počas behu X poškodil a X sa potom vrátila na adresu, ktorá nebola spustiteľným kódom. V niektorých prípadoch vyzeral práve odobratý rámec X platne, až na to, že jeho uložená návratová adresa bola NULL. V iných prípadoch vyzeral nesprávne samotný ukazovateľ zásobníka, no ďalším platným rámcom sa stále zdala byť metóda updateDocument.

Nevedeli sme, kedy sa zásobník poškodzuje, čo zanechávalo obrovský priestor na hľadanie. updateDocument je veľká metóda s množstvom inliningu, takže kandidátov na X bolo príliš veľa.

Bola to chyba v našom kóde C++? Problém kompilátora alebo linkovania? Problém v niektorej z našich runtime knižníc? Chyba linuxového kernelu pri doručovaní signálov alebo prepínaní kontextu? Niečo ešte zriedkavejšie? Ak išlo o zatúlaný zápis, prečo ho nezachytilo naše stagingové prostredie s ASAN?

Pokúsili sme sa použiť aplikačné logy na identifikáciu všetkých výskytov problému, ale chyby poškodenia zásobníka sa len z logov ťažko klasifikujú, pretože zalogované stopy zásobníka sú samy poškodené alebo chýbajú. Nedokázali sme zostaviť logovací dotaz, ktorý by nemal falošne pozitívne aj falošne negatívne výsledky. Ručne sme preskúmali ďalšie core dumpy a našli sme pár nových príkladov, no tento postup bol príliš prácny na to, aby poskytol dôveryhodný súbor dát.

V tejto fáze vyšetrovania sme (nesprávne) vylúčili hardvérovú chybu, pretože pády sme videli vo viacerých regiónoch a na viacerých typoch hardvéru, takže sme stále hľadali čisto softvérové príčiny. Niekoľko dní sme išli veľmi do hĺbky pri jednom páde s nesprávne zarovnaným %rsp a rekonštruovali históriu pred pádom z obsahu zásobníka a registrov. Prinieslo to možné stopy, ale keďže sme sa nevzdali pôvodného záveru, že všetky chyby majú tú istú príčinu, nikam nás to neposunulo.

Stopy zo zásobníka

Skôr než sa dostaneme k obratu vo vyšetrovaní, je dôležité vysvetliť, aké informácie sme z core súborov získavali.

Rockset sa kompiluje s -fno-omit-frame-pointer, takže aktívny rámec zásobníka je vždy dostupný cez %rbp a volajúci tvoria z ukazovateľov rámcov spájaný zoznam.

Na Linuxe x86_64 AMD64 System V ABI rezervuje aj 128 bajtov pod %rsp ako červenú zónu. Táto oblasť je dostupná kódu v userspace a, čo je dôležité, kernel v rámci zmluvy ABI sľubuje, že ju pri doručení signálu neprepíše.

Červená zóna bola pri ladení pádu po návrate kľúčová, pretože uchováva časť informácií spred návratu. Keď sa spustí SIGSEGV, fatal signal handler z folly beží na zásobníku padajúceho vlákna. Rámce zásobníka, ktoré už nie sú aktívne (lebo ich funkcia sa vrátila), signal handler prepíše okrem posledných 128 bajtov. Preto môžeme hovoriť veci ako „práve odobratý rámec zásobníka X vyzeral platne, až na návratovú adresu NULL“. Červená zóna uchováva časť neaktívnych rámcov, niekedy len koniec jedného neaktívneho rámca.

Diagram zásobníka znázorňuje poškodené rámce zásobníka, ktoré môžu prepísať návratové adresy a spôsobiť pády.

Našli sme jeden pád s nesprávne zarovnaným zásobníkom, pri ktorom boli všetky zúčastnené funkcie veľmi malé. Vďaka tomu sme videli, že %rsp sa nesprávne zarovnal počas behu pomerne jednoduchej funkcie a že ďalšie volania potom ešte uspeli. Program spadol až vtedy, keď sa aktívna funkcia napokon pokúsila vrátiť. Žiadna z týchto ciest kódu nepoužívala výnimky, inline assembler, setcontext ani longjmp, takže ak sa ukazovateľ zásobníka naozaj zmenil tak, ako naznačoval core dump, problém nevysvetľovala žiadna pravdepodobná chyba v userspace kóde.

To nás nasmerovalo ku kernelu.

Rockset používa signály agresívnejšie než väčšina programov. Vykonávanie dopytov je rozdelené na množstvo ľahkých úloh, ktoré si vymieňajú dáta. Je to dôležité pre efektívne spracovanie záťaže s vysokým QPS, ale komplikuje to účtovanie CPU po jednotlivých dopytoch, keďže práca mnohých dopytov sa multiplexuje na ten istý pool vlákien.

Naším riešením je niečo, čo nazývame coarse_thread_cputime_clock; lacno aproximuje clock_gettime(CLOCK_THREAD_CPUTIME_ID, ...), takže ho možno vzorkovať na hranici každej úlohy. API timer_create možno použiť na plánovanie pravidelného doručovania signálu podľa viacerých predstáv o plynutí času vrátane akumulácie CPU času. Signál (SIGUSR2) plánujeme doručiť každých pár milisekúnd CPU času; signal handler potom aktualizuje hodnotu lokálnu pre vlákno. Aj keď mnohé úlohy počas svojho behu nevidia posun hrubých hodín, súčet všetkých rozdielov dáva neskreslený odhad skutočného CPU času dopytu.

Keďže signály doručujeme tak často, zriedkavá chyba kernelu pri prepínaní kontextu alebo doručovaní signálov pôsobila vierohodne. Venovali sme čas čítaniu hlásení chýb, zdrojového kódu kernelu aj patchov kernelu špecifických pre Azure. Skúšali sme záťažové testy. Nenašli sme nič, čo by vyzeralo súvisiace.

Vtedy sme sa rozhodli ustúpiť o krok späť a skúsiť iný prístup.

Lekár alebo epidemiológ?

Takýto problém sa dá ladiť dvoma hlavnými spôsobmi.

Jedným je správať sa trochu ako lekár: sústrediť sa na jedného pacienta, urobiť množstvo testov a pokúsiť sa diagnostikovať jeden prípad z detailných dôkazov.

Druhým je správať sa skôr ako epidemiológ: pozrieť sa na celú populáciu a pýtať sa, či existujú vzory, ktoré jeden prípad neodhalí. Začala sa chyba v konkrétnom vydaní? Koreluje s jedným hardvérovým SKU (konkrétnym modelom CPU a servera), jedným regiónom alebo jednou verziou kernelu? Neskrýva sa v tom, čo vyzerá ako jeden syndróm, viac odlišných zhlukov?

Väčšinou sme boli v režime lekára. Kľúčovou zmenou bolo rozhodnutie, že musíme nazbierať kvalitné populačné dáta.

Čistenie dát

Naše predchádzajúce pokusy automaticky nájsť všetky výskyty problému zlyhali, pretože sme sa snažili používať textové vyhľadávanie v logoch. Samotné core dumpy obsahujú oveľa viac informácií, ale ich ručná kontrola sa nedala škálovať. Rozhodli sme sa investovať úsilie do pipeline, ktorá by vedela core dumpy analyzovať automaticky.

Nechali sme ChatGPT napísať skript, ktorý stiahol prefix každého core súboru, extrahoval registre, pomocou logov odfiltroval známe falošné pozitíva a automaticky označil pád ako návrat na NULL, nesprávne zarovnaný zásobník alebo iné. Potom sme tento skript paralelne spustili nad všetkými produkčnými core dumpmi Rocksetu z predchádzajúceho roka.

To bol bod obratu.

Keď sme mali čistý súbor dát, korelácie sa objavili okamžite. To, čo sme považovali za jednu čudnú chybu, boli v skutočnosti dve samostatné populácie pádov.

Core dumpy s návratom na NULL boli rozptýlené naprieč mnohými klastrami a geografickými regiónmi. Ich frekvencia nedávno stúpla, no nemali jasný dátum začiatku ani čistú infraštruktúrnu hranicu.

Pády s nesprávne zarovnaným zásobníkom vyzerali úplne inak. Všetky pochádzali z jedného regiónu, mali jasný dátum začiatku a nikdy sa nestali na uzloch, ktoré bežali dlhý čas. Hoci zahŕňali viacero VM v Azure (virtuálnych strojov hostovaných v cloude), vzor vyzeral ako jeden fyzický stroj s chybným hardvérom, ktorý spôsoboval problémy tej VM, ktorá na ňom práve pristála.

Bodový graf mier pádov podľa klastrov v čase ukazuje, že väčšina pádov sa sústreďuje v klastroch 2, 3 a 6, so špičkou v klastri 1 ku koncu obdobia.

Vtedy sme si uvedomili, že sme si v hlave spájali dve chyby do jednej. Keďže sme miešali protipríklady z oboch chýb, nedokázali sme nájsť jedno súvislé vysvetlenie.

Chyba č. 1: zlý hostiteľ

S čistým zoznamom uzlov Kubernetes a časovými pečiatkami sme dokázali vystopovať pády s nesprávne zarovnaným zásobníkom k jedinému fyzickému hostiteľovi, ktorého bolo jednoduché pridať na denylist.

Poškodenie registrov sa nám na tomto hostiteľovi nepodarilo reprodukovať v kontrolovanom prostredí ani po niekoľkých týždňoch záťažového testovania. Keď však problematický hostiteľ vyradili z prevádzky, pády s nesprávne zarovnaným zásobníkom zmizli.

Odstránenie zlého hostiteľa nie je trvalé riešenie v tom zmysle, že nezabráni novému výskytu rovnakého problému. Môžeme však zmeniť softvér tak, aby sa podobný problém pri opakovaní dal ľahko zistiť a zvládnuť. Vylepšili sme náš fatal signal handler tak, aby obsahoval stav registrov, vďaka čomu vieme opakovanie zistiť iba z logov (bez core dumpu). Zmenili sme control plane tak, aby sa VM zvyčajne opätovne používali namiesto recyklovania, čo na našej úrovni infraštruktúrneho stacku výrazne uľahčuje detekciu zlých uzlov. Aktualizovali sme aj runbooky (a mentálne modely nášho tímu), aby s touto možnosťou počítali.

Keď sme oddelili pády zlého hostiteľa, o zostávajúcich core dumpoch s návratom na NULL sa uvažovalo oveľa ľahšie. Predtým sme vylúčili odvíjanie výnimiek, lebo sme si mysleli, že máme protipríklady: pády v cestách kódu, kde sa výnimky určite nepoužívali. Všetky tieto protipríklady však pochádzali zo zhluku hardvérovej korupcie.

Keď sme s týmto vedomím znovu preskúmali zostávajúce core dumpy, zistili sme, že tento záver bol presne opačný: všetky pády nastávali počas odvíjania výnimiek.

Spracovanie výnimiek je dynamický prenos riadenia

Keď C++ vyhodí výnimku, runtime musí zistiť, ktorý catch blok ju má prijať a ktoré deštruktory alebo cleanup handlery sa majú cestou spustiť. Kompilátor tieto metadáta vygeneruje, ale samotné párovanie prebieha dynamicky za behu.

Odvíjanie výnimky v skutočnosti nevykonáva funkcia, ktorá vyvolá throw, ale pomocné funkcie volané výsledným skompilovaným kódom. Tieto runtime rutiny skúmajú zásobník, načítajú metadáta o funkciách nájdených na zásobníku, dynamicky hľadajú cleanup handlery a catch bloky a potom prenesú riadenie na jedno z týchto miest. Prenos riadenia zahŕňa odvinutie všetkých medziľahlých rámcov zásobníka (vrátane rámcov pomocných funkcií).

Prevádzkovo je to oveľa bližšie k longjmp alebo prepnutiu fiberu než k bežnému volaniu a návratu. Treba obnoviť registre ukladané volanou funkciou aj registre rámca zásobníka %rbp a %rsp.

Náš binárny súbor linkuje dve knižnice, ktoré obsahujú implementácie funkcií vykonávajúcich odvíjanie výnimiek C++: libgcc a GNU libunwind. Dynamický linker si vybral definície z GNU libunwind. Prekvapilo nás to; očakávali sme, že vyhrá implementácia libgcc kvôli pravidlám verziovania symbolov, no kontrola bežiacich binárnych súborov ukázala opak.

Zrušenie posledného predpokladu

V tejto chvíli sa naša pracovná hypotéza zmenila, keď sme uvoľnili ďalší predpoklad, ktorý sme si vytvorili v čase, keď sme si mysleli, že ide iba o jednu chybu.

Možno sme nevideli obyčajný návrat funkcie na NULL. Možno sme videli unwind transfer — v podstate obnovu registrov v štýle setcontext — kde sa cieľový ukazovateľ inštrukcie stal NULL ešte pred prenosom riadenia. Inými slovami, nesprávne dáta z unwind knižnice, nie nesprávny slot návratovej adresy na zásobníku.

To problém dramaticky zúžilo. Buď GNU libunwind počítal nesprávny cieľový stav, alebo počítal správny stav a niečo ho poškodilo skôr, než sa dal použiť.

Prečítali sme zdrojový kód GNU libunwind a zistili sme, že na zásobníku syntetizuje ucontext_t, vyplní požadovaný stav registrov pre rámec cleanup handlera a potom odovzdá ukazovateľ na túto štruktúru internej assemblerovej rutine: _Ux86_64_setcontext.

V tejto chvíli sme mali všetky dieliky.

Syntetizovaný ucontext_t žije v jednom z rámcov zásobníka, ktoré _Ux86_64_setcontext počas behu tejto funkcie odvíja. Čítal _Ux86_64_setcontext zo štruktúry aj po zmene %rsp, keď už štruktúra nebola súčasťou aktívneho zásobníka? Tým by sa stala zraniteľnou voči prepísaniu pri doručení signálu, napríklad nášho častého SIGUSR2.

Chyba č. 2: chyba v libunwind

Odpoveď bola áno.

Tu je posledných šesť inštrukcií _Ux86_64_setcontext vo verzii GNU libunwind, ktorú sme používali; pozostávajú prevažne z inštrukcií mov, ktoré načítavajú z pamäte do cieľového registra:

Obyčajný text

1
74: mov UC_MCONTEXT_GREGS_RSP(%rdi),%rsp
2
75:
3
76: /* push the return address on the stack */
4
77: mov UC_MCONTEXT_GREGS_RIP(%rdi),%rcx
5
78: push %rcx
6
79:
7
80: mov UC_MCONTEXT_GREGS_RCX(%rdi),%rcx
8
81: mov UC_MCONTEXT_GREGS_RDI(%rdi),%rdi
9
82: retq

(%rdi ukazuje na ucontext_t alokovaný na zásobníku a makrá UC_MCONTEXT_* sa len rozvinú na pevný offset, na ktorom je uložený konkrétny register.)

Prvá inštrukcia je začiatkom race window. Aktualizuje %rsp tak, aby ukazoval na nové dno aktívneho zásobníka. Hneď ako sa to stane, štruktúra, na ktorú ukazuje %rdi, už nie je súčasťou aktívneho zásobníka (ani červenej zóny) a kernelu už nie je zakázané na ňu siahať.

Zvyčajne to nespôsobí problémy, ale ak signál príde presne v správnom (nesprávnom?) okamihu, kernel vytvorí rámec signálu na %rsp-128. To môže prepísať pamäť, na ktorú ukazuje %rdi.

Ak sa to stane skôr, než ďalšia inštrukcia prečíta UC_MCONTEXT_GREGS_RIP(%rdi), obnovený ukazovateľ inštrukcie sa môže poškodiť. Pri našich pádoch sa stal NULL.

To je tá chyba.

Prečo jadrá maskované ako bežné chybné návraty

Táto asambláž tiež vysvetľuje jedno z pozorovaní, ktoré nás mýlilo: prečo mala funkcia X v slote návratovej adresy v predchádzajúcom zásobníkovom rámci hodnotu NULL.

setcontext bol napísaný tak, aby obnovil všetky registre vrátane %rdi, takže v poslednom momente prenosu riadenia nemôže použiť tento register na načítanie UC_MCONTEXT_GREGS_RIP(%rdi). Namiesto toho hodnotu načíta skôr, uloží ju na zásobník, obnoví ešte pár registrov a potom použije retq, aby načítal uloženú hodnotu a odovzdal riadenie.

To, čo v core dump-och vyzeralo ako „funkcia sa vrátila do NULL“, bolo v skutočnosti „unwinder syntetizoval cieľovú návratovú adresu na zásobníku, ale tento cieľ bol skôr, než sa prenos dokončil, poškodený“. Predpokladali sme, že poškodenie slotu návratovej adresy sa musí diať priamo na mieste, pretože sme nevedeli o žiadnych miestach, kde by sa (poškoditeľné) dáta zámerne zapisovali do slotu návratovej adresy.

Pretekové okno na jednu inštrukciu

To, čo robí tento bug absurdným, je, aké úzke je toto okno preteku. V tomto type pretekovej podmienky sa externá udalosť (signál) musí stať medzi dvoma krokmi, ktoré vykoná iné vlákno. Čím bližšie sú tieto kroky pri sebe, tým menej pravdepodobné je, že sa preteková podmienka stane.

V tomto prípade je zraniteľné okno doslova široké jednu inštrukciu! Signál musí byť doručený po tom, čo sa zmení %rsp, ale ešte predtým, než ďalšia inštrukcia načíta %rip. Na modernom superskalárnom out-of-order CPU sa dá za cyklus spustiť viacero takýchto jednoduchých inštrukcií, takže okno pre race je približne sto pikosekúnd.

Keď sme na tento race narazili, naša prvá reakcia bola, že musí byť príliš zriedkavý na to, aby vysvetlil pozorovanú mieru pádov. V celej flotile sme videli viac než tucet return-to-null pádov denne. Mohol by naozaj race na úrovni jednej inštrukcie počas čistenia po výnimke za to?

Siahli sme po Fermatovom odhade. Ak je zraniteľné okno rádovo 101010^{-10} sekúnd a SIGUSR2 prichádza každých 10210^{-2} sekúnd CPU času, potom má každý handler čistenia výnimky alebo catch blok približne 10810^{-8} pravdepodobnosť, že pretek prehrá.

Rockset používa výnimky ako súčasť svojho interného mechanizmu backpressure pri ingestovaní. Jediný preťažený host môže vyhodiť rádovo 10410^{4} výnimiek za sekundu. To znamená, že stredný čas medzi zlyhaniami hosta, ktorý používa backpressure, je 10410^{4} sekúnd, teda jeden pád každých pár hodín. V mierke celej flotily je to viac než dosť na vysvetlenie pozorovanej frekvencie pádov.

Prečo sa bug v libunwind objavil až teraz?

Bug v GNU libunwind je starý, viac než 18 rokov, a je prítomný už v prvej verzii x86_64, ktorá podporovala odvíjanie C++ výnimiek.

Tak prečo sa to objavilo až teraz?

Miera pádov je približne úmerná tomu, koľko výnimiek sa vyhodí a koľko signálov sa doručí. Závisí aj od toho, koľko zásobníka spotrebuje handler signálu.

Rockset je nezvyčajný vo všetkých troch osiach. Výnimky hádžeme vo vysokých mierach ako súčasť bežného riadenia preťaženia. SIGUSR2 doručujeme nezvyčajne často kvôli coarse_thread_cputime_clock. A začiatkom tohto roka sme upravili handler SIGUSR2 tak, aby používal viac zásobníka, keď sme pridali volanie timer_getoverrun, aby sme vedeli započítať zlúčené signály.

Tá posledná zmena sa zdá byť dôležitá. Ak handler používa dosť málo zásobníka, nemusí sa dostať až k zastaranej pamäti ucontext_t a prepísať ju. Pred touto zmenou tieto pády vôbec nepozorujeme. Po zmene zostala miera nízka, kým sme nezvýšili záťaž pre niektoré prípady použitia, ktoré zaťažili mechanizmus backpressure.

Inými slovami, bug v libunwind tam bol vždy, ale súčin našej miery výnimiek, miery signálov a využitia zásobníka v handleri len nedávno prekročil prah, keď sa to stalo prevádzkovo viditeľné.

Tento mechanizmus tiež vysvetľuje zhodu okolností, že hardvérový bug aj bug v libunwind padali väčšinou vnútri DocumentTree::updateDocument. Pády z libunwind boli výrazne vychýlené smerom k tejto metóde, pretože je vždy aktívna v momente, keď vyhodíme výnimku, aby sme uplatnili backpressure pri ingeste. Pri pádoch kvôli nesprávnemu zarovnaniu %rspsa tiež výrazne uprednostňovala, pretože chybný hardvérový uzol bol zo SKU, ktoré používame na hromadný ingest, a ten trávi väčšinu CPU času v tejto metóde.

Našou okamžitou mitigáciou bolo prepnúť z GNU libunwind na unwinder z libgcc. To bol dobrý kompromis aj sám o sebe: implementácia v libgcc profitovala z veľa práce na znížení lock contention, čo je dôležité pri škálovaní na veľké VM.

Zároveň sme do upstreamu poslali samostatný reproduktor a opravu(otvorí sa v novom okne) pre GNU libunwind a overili sme, že ostatné unwindery nemajú podobný problém.

Sila diagnostiky na úrovni populácie

Táto cesta ladenia nás naučila veľa o konkrétnych detailoch dynamického linkovania, unwind metadát DWARF, doručovania signálov v Linuxe, ABI System V a mechaniky C++ výnimiek. Ale hlavné ponaučenie bolo jednoduchšie než čokoľvek z toho.

Najdôležitejší krok nebolo šikovné čítanie assembleru ani hlboká znalosť detailov. Bolo to vybudovanie kvalitného dátového súboru. Bez tohto dátového súboru sme miešali dva odlišné javy do jedného príbehu a snažili sa z toho zmätku vyargumentovať. Keď sme mali presné a kompletné dáta o celej populácii, štruktúra problému bola zrazu jasná: jedna populácia pádov patrila chybnému hostu a druhá patrila race condition v libunwind. Keď boli dáta lepšie, debugovanie bolo jednoduchšie.

Pre infraštruktúrne systémy ako Rockset na tom veľmi záleží. Toto skúmanie posilnilo náš záväzok k hlbokej instrumentácii, automatizovaným vyšetrovaniam a neustálemu zlepšovaniu našich prevádzkových nástrojov. Spoľahlivosť nie je len o opravovaní chýb po tom, čo sa stanú. Je to o budovaní dát, pracovných postupov a zručností, ktoré z nemožných problémov spravia také, ktoré sa dajú diagnostikovať a vyriešiť.

Autori

By Nathan Bronson a Member of Technical Staff