Pāriet uz galveno saturu
OpenAI

2026. gada 30. jūnijs

Inženierija

Core dump epidemioloģija: 18 gadus vecas kļūdas labošana

Populācijas līmeņa analīze sarežģītu avāriju atkļūdošanai mūsu datu infrastruktūrā.

Notiek ielāde…

OpenAI modeļi un aģenti arvien vairāk paļaujas uz mērogojamu datu infrastruktūru, lai izsecināšanas laikā meklētu atbilstošus datus: brīdī, kad modeļi domā par jūsu jautājumu. Daļa šo pakalpojumu ir uzrakstīti C++, kura zemā līmeņa kontrole pār sistēmu ļauj maksimizēt veiktspēju un minimizēt atmiņas lietojumu. Šie efektivitātes ieguvumi ir svarīgi, mums mērogojot, taču C++ atmiņas drošības trūkums nozīmē, ka kļūdas var izraisīt avārijas, rakstot nepareizās vai neeksistējošās atmiņas adresēs.

Pirms dažiem mēnešiem novērojām vairākas avārijas Rockset pakalpojumā — īpaši veidotā mūsu ChatGPT datu infrastruktūras daļā, kas ir būtiska daudziem datu spraudņiem un meklēšanai sarunās. Katrā no šīm avārijām šķita, ka parasta C++ funkcija pabeidz darbu un pēc tam atgriežas uz aplamu adresi, liekot kodolam apturēt programmu, jo instrukciju rādītājs vairs nenorādīja uz kodu. Dažkārt atgriešanās adreses vieta steka kadrā bija NULL. Dažkārt pats steka rādītāja CPU reģistrs šķita nobīdīts par 8 baitiem, it kā %rsp normālas izpildes vidū kaut kā būtu samazināts. Abos gadījumos avārija notika atgriešanās brīdī.

Lietojumprogrammu kodam tie nav normāli atteices režīmi. Nejaušs ieraksts, kas trāpa tikai saglabātajā atgriešanās adresē, ir iespējams, taču ārkārtīgi maz ticams. Kļūda, kas nobīda %rsp par 8, neiesaistot inline assembler, setcontext vai longjmp (neko no tā mēs neizmantojam), ir vēl dīvaināka, jo kompilēts kods šo reģistru tieši koriģē tikai funkcijas prologā un epilogā. Pret katru hipotēzi, ko spējām izdomāt mēs (vai ChatGPT), bija spēcīgi pierādījumi, tāpēc kļūda šķita neiespējama.

Tas, ko uzskatījām par vienu problēmu, galu galā izrādījās divas nesaistītas kļūdas, kas nejauši tika atklātas vienlaikus. Pirmā — klusa aparatūras datu bojāšana vienā Azure resursdatorā, kur CPU vienkārši nepareizi veica aprēķinus. Otrā — 18 gadus vecs sacensības nosacījums GNU libunwind, nepamanīta kļūda plaši izmantotā atvērtā pirmkoda bibliotēkā.

Šis ieraksts ir par to, kā, domājot kā epidemiologi un izveidojot kvalitatīvu datu kopu par visu avāriju populāciju, mēs identificējām un izlabojām šķietami neizskaidrojamas avārijas.

Pirmais atkļūdošanas mēģinājums: rūpīgi izpētīt dažus core dump failus

Vispirms iedziļināsimies Rockset. Tā ir mākoņvidē radīta datu sistēma meklēšanai un reāllaika analītikai, ko OpenAI izmantojam daudzos iekšējos gadījumos, piemēram, sinhronizācijas savienotājiem (OpenAI iegādājās Rockset 2024. gadā). Straumēšanas atjauninājumi tiek izmantoti, lai uzturētu atjauninātu darbvietas zināšanu bāzes indeksu, lai ChatGPT, atbildot uz jautājumiem vai veicot darbības, varētu meklēt atbilstošu informāciju.

Rockset izpildes slānis ir uzrakstīts C++. C++ valoda nodrošina zema līmeņa piekļuvi CPU, kas ir labi veiktspējai un efektivitātei, taču tas nozīmē, ka lietojumprogrammas kļūdas var novest pie nederīgas piekļuves atmiņai un segmentācijas kļūdām. Lai tās palīdzētu izsekot, mēs izmantojam folly fatālo signālu apstrādātāju, kas avārijas brīdī reģistrē steka trasējumu, un augšupielādējam attiecīgos core dump failus (programmas stāvokļa momentuzņēmumu avārijas brīdī) Azure blob krātuvē vēlākai analīzei. Visas Rockset vaicājumu apstrādes lapas ir replicētas, tādējādi mazinot avārijas ietekmi uz klientu. Tomēr katra segmentācijas kļūda atbilst kļūdai, kas jāizlabo, lai sasniegtu mūsu uzticamības un kvalitātes mērķus.

Sākotnēji uzskatījām šo core dump kā parastu atkļūdošanas problēmu: ļoti rūpīgi izskatīt dažus failus, veidot hipotēzes un vienu pēc otras tās izslēgt.

Lielākā daļa avāriju notika metodē ar nosaukumu DocumentTree::updateDocument. Šajās avārijās izskatījās, ka updateDocument bija izsaukusi kādu nezināmu funkciju X, X darbības laikā steks bija bojāts, un pēc tam X atgriezās uz adresi, kas nebija izpildāms kods. Dažos gadījumos X tikko noņemts kadrs izskatījās derīga, izņemot to, ka saglabātā atgriešanās adrese bija NULL. Citos gadījumos pats steka rādītājs izskatījās nepareizs, bet nākamais derīgais kadrs joprojām šķita, ka atrodas updateDocument.

Mēs nezinājām, kad steks tiek bojāts, un tas atstāja milzīgu meklēšanas telpu. updateDocument ir liela metode, kurā notiek daudz inline ievietošanas, tāpēc X kandidātu skaits bija nomācoši liels.

Vai tā bija kļūda mūsu C++ kodā? Kompilatora vai sasaistes problēma? Problēma kādā no mūsu izpildlaika bibliotēkām? Linux kodola kļūda signālu piegādē vai konteksta pārslēgšanā? Kaut kas vēl retāks? Ja tas bija nejaušs ieraksts, kāpēc to neatklāja mūsu ASAN priekšražošanas vide?

Mēģinājām izmantot lietojumprogrammas līmeņa žurnālus, lai identificētu visus problēmas gadījumus, taču steka bojājumu kļūdas ir grūti klasificēt tikai pēc žurnāliem, jo reģistrētie steka trasējumi paši ir bojāti vai trūkst. Mums neizdevās izveidot žurnālu vaicājumu, kurā nebūtu gan viltus pozitīvu, gan viltus negatīvu rezultātu. Manuāli izskatījām vairāk core dump un atradām vēl dažus piemērus, taču process bija pārāk darbietilpīgs, lai sniegtu uzticamu datu kopu.

Šajā izmeklēšanas posmā mēs (nepareizi) izslēdzām aparatūras kļūdu, jo redzējām avārijas vairākās zonās un vairākos aparatūras tipos, tāpēc joprojām meklējām tikai programmatūras cēloņus. Vairājas dienas ļoti dziļi pētījām vienu avāriju ar nepareizi izlīdzinātu %rsp, rekonstruējot vēsturi pirms avārijas pēc steka un reģistru satura. Tas deva dažus iespējamus pavedienus, taču, tā kā nepametām sākotnējo secinājumu, ka visām kļūdām ir viens cēlonis, tas mūs neizveda no strupceļa.

Pavedieni no steka

Pirms nonākam pie izmeklēšanas pagrieziena punkta, ir svarīgi izskaidrot, kādu informāciju ieguvām no core failiem.

Rockset tiek kompilēts ar -fno-omit-frame-pointer, tāpēc aktīvais steka kadrs vienmēr ir sasniedzams caur %rbp, un izsaucēji veido saistītu kadru rādītāju sarakstu.

Linux x86_64 vidē AMD64 System V ABI arī rezervē 128 baitus zem %rsp kā sarkano zonu. Šī zona ir pieejama lietotāja telpas kodam, un, kas svarīgi, kodols ABI līguma ietvaros sola to nepārrakstīt, piegādājot signālu.

Sarkanā zona bija centrāla mūsu atkļūdošanā avārijai pēc atgriešanās, jo tā saglabā daļu informācijas no brīža pirms atgriešanās. Kad tiek aktivizēts SIGSEGV, folly fatālo signālu apstrādātājs darbojas uz avarējošā pavediena steka. Steka kadri, kas vairs nav aktīvi (jo to funkcija ir atgriezusies), tiks pārrakstīti ar signāla apstrādātāju, izņemot pēdējos 128 baitus. Tāpēc varam teikt, piemēram: „X tikko noņemtais steka kadrs izskatījās derīgs, izņemot NULL atgriešanās adresi.” Sarkanā zona saglabā daļu neaktīvo kadru vai reizēm tikai viena neaktīva kadra asti.

Steka diagramma, kurā parādīti bojāti steka kadri, kas var pārrakstīt atgriešanās adreses un izraisīt avārijas.

Atradām vienu avāriju ar nepareizi izlīdzinātu steku, kurā visas iesaistītās funkcijas bija ļoti mazas. Tas ļāva redzēt, ka %rsp bija kļuvis nepareizi izlīdzināts samērā vienkāršas funkcijas izpildes laikā un ka pēc tam vēl vairāki izsaukumi bija izdevušies. Programma avarēja tikai tad, kad aktīvā funkcija beidzot mēģināja atgriezties. Nevienā no šiem koda ceļiem netika izmantoti izņēmumi, inline assembler, setcontext vai longjmp, tāpēc, ja steka rādītājs tiešām mainījās tā, kā liecināja core fails, neviena ticama lietotāja telpas koda kļūda to neizskaidroja.

Tas mūs virzīja kodola virzienā.

Rockset signālus izmanto agresīvāk nekā lielākā daļa programmu. Vaicājumu izpilde ir sadalīta daudzos vieglos uzdevumos, kas apmainās ar datiem. Tas ir svarīgi, lai efektīvi apstrādātu lielas QPS slodzes, taču apgrūtina CPU uzskaiti katram vaicājumam, jo daudzu vaicājumu darbs tiek apvienots vienā un tajā pašā pavedienu kopā.

Mūsu risinājums ir tas, ko saucam par coarse_thread_cputime_clock; tas pietiekami lēti aptuveni atdarina clock_gettime(CLOCK_THREAD_CPUTIME_ID, ...), lai varētu ņemt paraugus pie katras uzdevuma robežas. timer_create API var izmantot, lai ieplānotu periodisku signāla piegādi pēc vairākiem laika ritējuma jēdzieniem, tostarp pēc uzkrātā CPU laika. Mēs ieplānojam signālu (SIGUSR2), kas tiek piegādāts ik pēc dažām CPU laika milisekundēm; tad signāla apstrādātājs atjaunina pavedienam lokālu vērtību. Lai gan daudzi uzdevumi izpildes laikā neredz raupjā pulksteņa virzību, visu deltu summa dod nenovirzītu faktiskā vaicājuma CPU laika novērtējumu.

Tā kā signālus piegādājam tik bieži, reta kodola kļūda konteksta pārslēgšanā vai signālu piegādē šķita ticama. Veltījām laiku kļūdu ziņojumu, kodola pirmkoda un Azure specifisko kodola ielāpu lasīšanai. Mēģinājām slodzes testus. Neizdevās atrast neko, kas šķistu saistīts.

Tajā brīdī nolēmām atkāpties soli atpakaļ un izmēģināt citu pieeju.

Ārsts vai epidemiologs?

Šādu problēmu var atkļūdot divos plašos veidos.

Viens ir rīkoties kā ārstam: koncentrēties uz vienu pacientu, veikt daudz testu un mēģināt diagnosticēt vienu gadījumu pēc detalizētiem pierādījumiem.

Otrs — rīkoties vairāk kā epidemiologam: skatīties uz visu populāciju un jautāt, vai pastāv raksti, ko viens atsevišķs gadījums nevar atklāt. Vai kļūda sākās konkrētā laidienā? Vai tā korelē ar vienu aparatūras SKU (konkrētu CPU un servera modeli), vienu reģionu vai vienu kodola versiju? Vai tajā, kas izskatās pēc viena sindroma, slēpjas vairāki atšķirīgi klasteri?

Mēs lielākoties bijām ārsta režīmā. Galvenā pārmaiņa bija lēmums, ka mums jāsavāc kvalitatīvi populācijas dati.

Datu attīrīšana

Iepriekšējie mēģinājumi automātiski atrast visus problēmas gadījumus bija neveiksmīgi, jo centāmies izmantot teksta meklēšanu žurnālos. Pašos core dump failos ir daudz vairāk informācijas, taču manuāla to skatīšana nebija mērogojama. Nolēmām ieguldīt darbu konveijera izveidē, kas varētu automātiski analizēt core dump failus.

Mēs likām ChatGPT uzrakstīt skriptu, kas lejupielādēja katra core faila prefiksu, izguva reģistrus, ar žurnālu palīdzību izfiltrēja zināmos viltus pozitīvos gadījumus un automātiski marķēja avāriju kā atgriešanos uz nulli, nepareizi izlīdzinātu steku vai citu. Pēc tam palaidām šo skriptu paralēli pār visiem ražošanas Rockset core dump failiem no iepriekšējā gada.

Tas bija pagrieziena punkts.

Kad mums bija tīra datu kopa, korelācijas parādījās uzreiz. Tas, ko bijām uzskatījuši par vienu dīvainu kļūdu, patiesībā bija divas atsevišķas avāriju populācijas.

Atgriešanās uz nulli core faili bija izkliedēti pa daudziem klasteriem un ģeogrāfiskajām zonām. To biežums nesen bija pieaudzis, taču nebija skaidra sākuma datuma un tīras infrastruktūras robežas.

Avārijas ar nepareizi izlīdzinātu steku izskatījās pilnīgi citādi. Tās visas nāca no vienas zonas, tām bija skaidrs sākuma datums, un tās nekad nenotika mezglos, kas bija darbojušies ilgu laiku. Lai gan tās skāra vairākas Azure VM (mākonī mitinātas virtuālās mašīnas), raksts izskatījās pēc vienas fiziskas mašīnas ar bojātu aparatūru, kas rada problēmas jebkurai VM, kura nejauši uz tās nonāk.

Punktu diagramma ar avāriju biežumu pa klasteriem laika gaitā; lielākā daļa avāriju koncentrēta 2., 3. un 6. klasterī, bet perioda beigās redzams pīķis 1. klasterī.

Tajā brīdī sapratām, ka domās esam sapludinājuši divas kļūdas. Tā kā bijām jaukuši kopā pretpiemērus no abām kļūdām, nevarējām atrast vienu saskanīgu skaidrojumu.

Kļūda Nr. 1: sliktais resursdators

Ar tīru Kubernetes mezglu sarakstu un laikspiedogiem varējām izsekot nepareizi izlīdzinātā steka avārijas līdz vienam fiziskam resursdatoram, ko bija viegli iekļaut liegumu sarakstā.

Mums neizdevās kontrolētā vidē reproducēt reģistru bojāšanu šajā resursdatorā pat pēc vairākām stresa testēšanas nedēļām. Tomēr, tiklīdz problemātiskais resursdators tika izņemts no ekspluatācijas, nepareizi izlīdzinātā steka avārijas pazuda.

Sliktā resursdatora noņemšana nav pastāvīgs risinājums tādā nozīmē, ka tā nenovērš jaunu tādas pašas problēmas gadījumu. Tomēr varam mainīt programmatūru tā, lai līdzīgas problēmas atkārtošanās gadījumā to būtu viegli atklāt un apstrādāt. Uzlabojām fatālo signālu apstrādātāju, iekļaujot reģistru stāvokli, lai atkārtošanos varētu noteikt tikai pēc žurnāliem (core dump nav vajadzīgs). Mainījām vadības plakni tā, lai VM parasti tiktu atkārtoti izmantotas, nevis pārstrādātas, kas mūsu infrastruktūras steka līmenī ievērojami atvieglo sliktu mezglu noteikšanu. Atjauninājām arī savas ekspluatācijas instrukcijas (un komandas mentālos modeļus), lai iekļautu šo iespēju.

Kad sliktā resursdatora avārijas tika nodalītas, par atlikušajiem atgriešanās uz nulli core failiem kļuva daudz vieglāk spriest. Iepriekš bijām izslēguši izņēmumu attīšanu, jo domājām, ka mums ir pretpiemēri: avārijas koda ceļos, kuros izņēmumi noteikti netika izmantoti. Taču visi šie pretpiemēri bija no aparatūras bojāšanas klastera.

Kad, to paturot prātā, vēlreiz izskatījām atlikušos core failus, atklājām, ka secinājums bija tieši pretējs: visas avārijas notika izņēmumu attīšanas laikā.

Izņēmumu apstrāde ir dinamiska kontroles nodošana

Kad C++ izmet izņēmumu, izpildlaikam jānoskaidro, kuram catch blokam tas jāsaņem un kuri destruktori vai tīrīšanas apstrādātāji jāizpilda pa ceļam. Kompilators izvada šos metadatus, bet faktiskā saskaņošana notiek dinamiski izpildlaikā.

Izņēmumu attīšanu patiesībā neveic funkcija, kas izsauc throw, bet gan palīgfunkcijas, kuras izsauc iegūtais kompilētais kods. Šīs izpildlaika rutīnas pārbauda steku, iegūst metadatus par stekā atrastajām funkcijām, dinamiski meklē tīrīšanas apstrādātājus un catch blokus un pēc tam nodod kontroli vienai no šīm vietām. Kontroles nodošana ietver visu starpā esošo steka kadru attīšanu (tostarp palīgfunkciju kadrus).

Praktiski tas ir daudz tuvāk longjmp vai šķiedras pārslēgšanai nekā parastam izsaukumam un atgriešanai. Jāatjauno callee-save reģistri, kā arī steka kadru reģistri %rbp un %rsp.

Mūsu binārais fails sasaistās ar divām bibliotēkām, kurās ir C++ izņēmumu attīšanas funkciju ieviešanas: libgcc un GNU libunwind. Dinamiskais sasaistītājs izvēlējās GNU libunwind definīcijas. Tas mūs pārsteidza; bijām gaidījuši, ka simbolu versiju noteikšanas noteikumu dēļ uzvarēs libgcc ieviešana, taču strādājošu bināro failu pārbaude parādīja, ka tā nav.

Atmetam vēl vienu pēdējo pieņēmumu

Šajā brīdī mūsu darba hipotēze mainījās, jo atlaidām vēl vienu pieņēmumu, ko bijām izdarījuši, domājot, ka ir tikai viena kļūda.

Varbūt mēs neredzējām parastu funkcijas atgriešanos uz NULL. Varbūt redzējām attīšanas nodošanu — faktiski setcontext stila reģistru atjaunošanu —, kur mērķa instrukciju rādītājs pirms kontroles nodošanas bija kļuvis NULL. Citiem vārdiem, nepareizi dati no attīšanas bibliotēkas, nevis nepareiza atgriešanās adreses vieta stekā.

Tas problēmu dramatiski sašaurināja. Vai nu GNU libunwind aprēķināja nepareizu mērķa stāvokli, vai arī aprēķināja pareizo stāvokli, bet kaut kas to sabojāja, pirms to varēja piemērot.

Lasījām GNU libunwind pirmkodu un atklājām, ka tā stekā sintezē ucontext_t, aizpilda vajadzīgo reģistru stāvokli tīrīšanas apstrādātāja kadram un pēc tam nodod rādītāju uz šo struktūru iekšējai assembler rutīnai: _Ux86_64_setcontext.

Šajā brīdī mums bija visas puzles daļas.

Sintezētais ucontext_t atrodas vienā no steka kadriem, ko _Ux86_64_setcontext attin šīs funkcijas izpildes laikā. Vai _Ux86_64_setcontext lasīja no struktūras pēc tam, kad bija mainījis %rsp, brīdī, kad struktūra vairs nebija aktīvā steka daļa? Tas padarītu to ievainojamu pret pārrakstīšanu signāla piegādes laikā, piemēram, mūsu biežā SIGUSR2 gadījumā.

Kļūda Nr. 2: libunwind kļūda

Atbilde bija jā.

Lūk, pēdējās sešas instrukcijas _Ux86_64_setcontext tajā GNU libunwind versijā, ko izmantojām; tās galvenokārt sastāv no mov instrukcijām, kas ielādē no atmiņas mērķa reģistrā:

Vienkāršs teksts

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 norāda uz stekā piešķirto ucontext_t, un UC_MCONTEXT_* makrosi vienkārši izvēršas par fiksētu nobīdi, kurā glabājas konkrēts reģistrs.)

Pirmā instrukcija ir sacensības loga sākums. Tā atjaunina %rsp, lai tas norādītu uz jauno aktīvā steka apakšu. Tiklīdz tas notiek, struktūra, uz kuru norāda %rdi, vairs nav aktīvā steka (vai sarkanās zonas) daļa un vairs nav kodolam aizliegta.

Parasti tas nerada problēmas, bet, ja signāls pienāk tieši pareizajā (nepareizajā?) brīdī, kodols izveidos signāla kadru pie %rsp-128. Tas var pārrakstīt atmiņu, uz kuru norāda %rdi.

Ja tas notiek, pirms nākamā instrukcija nolasa UC_MCONTEXT_GREGS_RIP(%rdi), atjaunotais instrukciju rādītājs var tikt sabojāts. Mūsu avārijās tas kļuva par NULL.

Tā ir šī kļūda.

Kāpēc core faili maskējās kā parastas sliktas atgriešanās

Šis assembler kods arī izskaidro vienu no novērojumiem, kas mūs mulsināja: kāpēc funkcijai X iepriekšējā steka kadra atgriešanās adreses vietā bija NULL.

setcontext tika rakstīts, lai atjaunotu visus reģistrus, tostarp %rdi, tāpēc kontroles nodošanas pēdējā brīdī tas nevar izmantot šo reģistru, lai lasītu UC_MCONTEXT_GREGS_RIP(%rdi). Tā vietā tas nolasa vērtību agrāk, saglabā to stekā, atjauno vēl dažus reģistrus un tad izmanto retq, lai nolasītu saglabāto vērtību un nodotu kontroli.

Tas, kas core failos izskatījās kā „funkcija atgriezās uz NULL”, patiesībā bija „unwinder stekā sintezēja mērķa atgriešanās adresi, bet šis mērķis tika sabojāts, pirms nodošana pabeidzās”. Mēs uzskatījām, ka atgriešanās adreses vietas bojāšanai jānotiek uz vietas, jo nezinājām nevienu vietu, kur (sabojājami) dati ar nolūku tiktu ierakstīti atgriešanās adreses vietā.

Vienas instrukcijas sacensību logs

Šo kļūdu šķietami absurdu padara tas, cik šaurs ir sacensības logs. Šāda veida sacensības nosacījumā ārējam notikumam (signālam) jānotiek starp diviem cita pavediena veiktajiem soļiem. Jo tuvāk viens otram ir šie soļi, jo mazāka iespēja, ka sacensības nosacījums notiks.

Šajā gadījumā ievainojamais logs burtiski ir vienas instrukcijas plats! Signāls jāpiegādā pēc tam, kad %rsp ir mainīts, bet pirms nākamā instrukcija ielādē %rip. Mūsdienu superskalārs ārpus kārtas CPU vienā ciklā var izpildīt vairākas šādas vienkāršas instrukcijas, tāpēc sacensības logs ir aptuveni simt pikosekunžu.

Kad atradām šo sacensību, pirmā reakcija bija, ka tā noteikti ir pārāk reta, lai izskaidrotu novēroto avāriju biežumu. Visā flotē redzējām vairāk nekā duci atgriešanās uz nulli avāriju dienā. Vai vienas instrukcijas sacensība izņēmumu tīrīšanas laikā tiešām varēja to izskaidrot?

Mēs pievērsāmies Fermī novērtējumam. Ja ievainojamais logs ir apmēram 101010^{-10} sekundes un SIGUSR2 pienāk ik pēc 10210^{-2} CPU laika sekundēm, tad katram izņēmuma tīrīšanas apstrādātājam vai catch blokam ir aptuveni 10810^{-8} varbūtība zaudēt sacensībā.

Rockset izmanto izņēmumus kā daļu no iekšējā datu ielādes pretspiediena mehānisma. Viens pārslogots resursdators var izmest apmēram 10410^{4} izņēmumus sekundē. Tas nozīmē, ka vidējais laiks starp atteicēm resursdatoram, kas izmanto pretspiedienu, ir 10410^{4} sekundes jeb viena avārija ik pēc dažām stundām. Flotes mērogā ar to ir vairāk nekā pietiekami, lai izskaidrotu novēroto avāriju biežumu.

Kāpēc libunwind kļūda parādījās tieši tagad?

GNU libunwind kļūda ir sena — vairāk nekā 18 gadus veca, klātesoša jau pirmajā x86_64 versijā, kas atbalstīja C++ izņēmumu attīšanu.

Kāpēc tā parādījās tieši tagad?

Avāriju biežums aptuveni ir proporcionāls tam, cik daudz izņēmumu tiek izmesti un cik daudz signālu tiek piegādāti. Tas ir atkarīgs arī no tā, cik daudz steka patērē signāla apstrādātājs.

Rockset ir neparasts visās trīs asīs. Izņēmumus izmetam augstā biežumā kā normālas pārslodzes kontroles daļu; SIGUSR2 piegādājam neparasti bieži coarse_thread_cputime_clock dēļ; un šā gada sākumā likām SIGUSR2 apstrādātājam izmantot vairāk steka, pievienojot izsaukumu uz timer_getoverrun, lai varētu uzskaitīt apvienotus signālus.

Šķiet, pēdējā izmaiņa bija svarīga. Ja apstrādātājs izmanto pietiekami maz steka, tas var neaizsniegt un nepārrakstīt novecojušo ucontext_t atmiņu. Pirms šīs izmaiņas šādas avārijas vispār nenovērojām. Pēc izmaiņas biežums palika zems, līdz palielinājām slodzi dažiem lietojuma gadījumiem, kas noslogoja pretspiediena mehānismu.

Citiem vārdiem, libunwind kļūda tur ir bijusi vienmēr, taču mūsu izņēmumu biežuma, signālu biežuma un apstrādātāja steka lietojuma reizinājums tikai nesen pārsniedza slieksni, kurā tā kļuva redzama ekspluatācijā.

Šis mehānisms izskaidro arī sakritību, ka gan aparatūras kļūda, gan libunwind kļūda pārsvarā avarēja DocumentTree::updateDocument iekšienē. libunwind avārijas bija ļoti tendētas uz šo metodi, jo tā vienmēr ir aktīva brīdī, kad izmetam izņēmumu, lai piemērotu datu ielādes pretspiedienu. Tā bija arī spēcīgi atlasīta %rsp nepareizas izlīdzināšanas avārijām, jo bojātais aparatūras mezgls bija SKU, ko izmantojam masveida datu ielādei, un lielāko daļu CPU laika pavada šajā metodē.

Mūsu tūlītējais mazināšanas pasākums bija pāriet no GNU libunwind uz libgcc attinēju. Tas pats par sevi bija labs kompromiss: libgcc ieviešana ir guvusi labumu no liela darba, kas samazina bloķēšanas konkurenci, un tas ir svarīgi, mērogojoties uz lielām VM.

Mēs arī nosūtījām patstāvīgu pašpietiekamu reproduktoru un labojumu(atveras jaunā logā) GNU libunwind, kā arī pārbaudījām, ka citiem attinējiem nav līdzīgas problēmas.

Populācijas līmeņa diagnozes spēks

Šis atkļūdošanas ceļš mums daudz iemācīja par dinamiskās sasaistes, DWARF attīšanas metadatu, Linux signālu piegādes, System V ABI un C++ izņēmumu mehānikas konkrētajām detaļām. Taču galvenā mācība bija vienkāršāka par visu to.

Svarīgākais solis nebija gudra assembler lasīšana vai dziļas zināšanas par detaļām. Tas bija kvalitatīvas datu kopas izveidošana. Bez šīs datu kopas mēs jaucām divas atšķirīgas parādības vienā stāstā un mēģinājām ar spriešanu izkļūt no apjukuma. Kad mums bija precīzi un pilnīgi populācijas dati, problēmas struktūra kļuva acīmredzama: viena avāriju populācija piederēja sliktam resursdatoram, bet otra — sacensībai libunwind. Kad dati kļuva labāki, atkļūdošana kļuva vieglāka.

Tādām infrastruktūras sistēmām kā Rockset tas ir ļoti svarīgi. Šī izmeklēšana nostiprināja mūsu apņemšanos veikt dziļu instrumentēšanu, automatizētas izmeklēšanas un pastāvīgi uzlabot operacionālos rīkus. Uzticamība nav tikai kļūdu labošana pēc to notikšanas — tā ir datu, darbplūsmu un prasmju veidošana, kas neiespējamas problēmas pārvērš diagnosticējamās un atrisināmās.

Autori

By Nathan Bronson un Member of Technical Staff