Core dump -ilmiön epidemiologia: 18 v vanhan bugin korjaaminen
Väestötason analyysin hyödyntäminen datainfrastruktuurimme hankalien kaatumisten vianmäärityksessä.
OpenAI:n mallit ja agentit tukeutuvat yhä enemmän skaalautuvaan datainfrastruktuuriin löytääkseen olennaista dataa päättelyn aikana: silloin kun mallit pohtivat kysymystäsi. Osa näistä palveluista on kirjoitettu C++:lla, jonka matalan tason hallinta auttaa maksimoimaan suorituskyvyn ja minimoimaan muistinkäytön. Tehokkuushyödyt ovat skaalatessa tärkeitä, mutta C++:n muistiturvan puute tarkoittaa, että bugit voivat kaataa ohjelman kirjoittamalla vääriin tai olemattomiin muistiosoitteisiin.
Muutama kuukausi sitten havaitsimme kaatumisia Rockset-palvelun sisältä. Se on ChatGPT:n datainfrastruktuurin räätälöity osa, tärkeä monille datalaajennuksille ja keskustelujen haulle. Näissä kaatumisissa tavallinen C++-funktio näytti päättyvän ja palaavan sitten virheelliseen osoitteeseen, jolloin ydin pysäytti ohjelman, koska käskyosoitin ei enää osoittanut koodiin. Joskus pinokehyksen paluuosoitepaikka oli NULL. Joskus pinon osoittimen CPU-rekisteri itse näytti olevan 8 tavua pielessä, aivan kuin %rsp olisi jotenkin pienentynyt normaalin suorituksen keskellä. Molemmissa tapauksissa kaatuminen tapahtui palatessa.
Nämä eivät ole sovelluskoodille tavallisia vikatiloja. Harhalaukauskirjoitus, joka osuu vain tallennettuun paluuosoitteeseen, on mahdollinen mutta äärimmäisen epätodennäköinen. Bugi, joka kohdistaa %rsp:n 8 tavua väärin ilman inline-kokoonpanoa, setcontextia tai longjmpia (emme käytä mitään niistä), on vielä oudompi, koska käännetty koodi säätää rekisteriä suoraan vain funktion prologissa ja epilogissa. Jokaista hypoteesia, jonka me (tai ChatGPT) pystyimme keksimään, vastasi vahva todiste, joten virhe vaikutti mahdottomalta.
Se, minkä oletimme yhdeksi ongelmaksi, osoittautui lopulta kahdeksi riippumattomaksi bugiksi, jotka sattuivat paljastumaan samaan aikaan. Ensimmäinen oli hiljainen laitteistokorruptio yhdessä Azure-isännässä, jossa CPU ei vain laskenut oikein. Toinen oli 18 vuotta vanha kilpailutilanne GNU libunwindissa, huomaamaton bugi laajasti käytetyssä avoimen lähdekoodin kirjastossa.
Tässä kerromme, miten tunnistimme ja korjasimme näennäisesti selittämättömät kaatumiset ajattelemalla epidemiologin tavoin ja rakentamalla laadukkaan aineiston koko kaatumispopulaatiosta.
Aloitetaan syventymällä Rocksetiin. Se on pilvinatiivi datajärjestelmä hakuun ja reaaliaikaiseen analytiikkaan. Käytämme sitä OpenAI:ssa moniin sisäisiin tarpeisiin, kuten synkronointiliittimiin (OpenAI osti Rocksetin vuonna 2024). Suoratoistopäivityksillä pidetään työtilan tietopohjan indeksi ajan tasalla, jotta ChatGPT voi etsiä olennaista tietoa vastatessaan kysymyksiin tai tehdessään toimia.
Rocksetin suorituskerros on kirjoitettu C++:lla. C++ antaa matalan tason pääsyn CPU:hun, mikä parantaa suorituskykyä ja tehokkuutta, mutta sovellusbugit voivat johtaa virheellisiin muistiviittauksiin ja segfault-kaatumisiin. Näiden jäljittämiseksi käytämme follyn fatal signal handleria kirjaamaan pinonjäljityksen kaatumisen tapahtuessa ja lataamme vastaavat core dumpit (tilannekuvat ohjelman tilasta kaatumishetkellä) Azure Blob Storageen myöhempää analyysia varten. Kaikki Rocksetin kyselynkäsittelyn leaf-solmut replikoidaan, mikä minimoi kaatumisen vaikutuksen asiakkaaseen. Jokainen segfault vastaa kuitenkin bugia, joka on korjattava luotettavuus- ja laatutavoitteidemme saavuttamiseksi.
Aluksi käsittelimme näitä ytimiä perinteisenä debuggausongelmana: tutkimme muutamaa core dumpia tarkasti, muodostimme hypoteeseja ja suljimme niitä pois yksi kerrallaan.
Useimmat kaatumiset tapahtuivat metodissa DocumentTree::updateDocument. Niissä näytti siltä, että updateDocument oli kutsunut tuntematonta funktiota X, pino oli korruptoitunut X:n ollessa aktiivinen, ja X oli palannut osoitteeseen, joka ei ollut suoritettavaa koodia. Joissakin tapauksissa X:n juuri poistettu kehys näytti kelvolliselta paitsi, että sen tallennettu paluuosoite oli NULL. Muissa tapauksissa pinon osoitin näytti väärältä, mutta seuraava kelvollinen kehys vaikutti silti olevan updateDocument.
Emme tienneet, milloin pino korruptoitui, joten hakutila oli valtava. updateDocument on suuri metodi, jossa suoritetaan paljon inline-käsittelyä, joten X:n ehdokkaiden määrä oli valtava.
Oliko tämä bugi C++-koodissamme? Kääntäjä- tai linkitysongelma? Ongelma jossakin ajonaikaisessa kirjastossamme? Linux-ytimen bugi signaalien toimituksessa tai kontekstinvaihdossa? Jotain vielä harvinaisempaa? Jos kyse oli harhakirjoituksesta, miksei ASAN-staging-ympäristömme havainnut sitä?
Yritimme tunnistaa kaikki ongelman esiintymät sovellustason lokeista, mutta pinokorruptiobugeja on vaikea luokitella pelkistä lokeista, koska kirjatut pinonjäljitykset ovat itsekin korruptoituneita tai puuttuvat. Emme saaneet tehtyä lokikyselyä, jossa ei olisi ollut sekä vääriä positiivisia että vääriä negatiivisia. Tutkimme käsin lisää ytimiä ja löysimme lisäesimerkkejä, mutta työ oli liian raskasta luotettavan aineiston rakentamiseen.
Tässä vaiheessa suljimme (virheellisesti) laitteistobugin pois, koska kaatumisia näkyi useilla alueilla ja laitteistotyypeillä. Etsimme siis yhä pelkkiä ohjelmistosyitä. Muutaman päivän ajan syvennyimme yhteen väärin kohdistuneen %rsp:n kaatumiseen ja rekonstruoimme historiaa pinon ja rekisterien sisällöstä. Se tuotti vihjeitä, mutta koska pidimme kiinni alkuoletuksesta, että kaikilla bugeilla oli sama syy, emme päässeet eteenpäin.
Ennen tutkimuksen käännekohtaa on tärkeää selittää, millaista tietoa poimimme core-tiedostoista.
Rockset käännetään asetuksella -fno-omit-frame-pointer, joten aktiivinen pinokehys löytyy aina %rbp:n kautta, ja kutsujat muodostavat kehysosoittimien linkitetyn listan.
Linuxin x86_64-alustalla AMD64 System V ABI varaa myös 128 tavua %rsp:n alapuolelta red zoneksi. Tämä alue on käyttäjätilan koodin käytettävissä, ja tärkeää kyllä ydin lupaa ABI-sopimuksen osana olla sotkematta sitä signaalia toimittaessaan.
Red zone oli keskeinen paluun jälkeisen kaatumisen debuggaamisessa, koska se säilyttää tietoa ennen paluuta. Kun SIGSEGV laukeaa, follyn fatal signal handler suoritetaan kaatuvan säikeen pinossa. Pinokehykset, jotka eivät enää ole aktiivisia, koska niiden funktio on palannut, sotkeutuvat signaalinkäsittelijän takia viimeisiä 128 tavua lukuun ottamatta. Siksi voimme sanoa esimerkiksi: ”X:n juuri poistettu pinokehys näytti kelvolliselta, paitsi että paluuosoite oli NULL.” Red zone säilyttää osan ei-aktiivisista kehyksistä tai joskus vain yhden kehyksen hännän.
Löysimme yhden väärin kohdistuneen pinon kaatumisen, jossa kaikki mukana olleet funktiot olivat hyvin pieniä. Sen ansiosta näimme, että %rsp oli mennyt vinoon suhteellisen yksinkertaisen funktion suorituksen aikana ja että sen jälkeen oli vielä onnistunut kutsuja. Ohjelma kaatui vasta, kun aktiivinen funktio lopulta yritti palata. Yksikään näistä koodipoluista ei käyttänyt poikkeuksia, inline-kokoonpanoa, setcontextia tai longjmpia, joten jos pinon osoitin todella muuttui ytimen vihjaamalla tavalla, mikään uskottava käyttäjätilan bugi ei selittänyt sitä.
Se työnsi meitä ytimen suuntaan.
Rockset käyttää signaaleja aggressiivisemmin kuin useimmat ohjelmat. Kyselyjen suoritus jaetaan moniin kevyisiin tehtäviin, jotka vaihtavat dataa. Tämä on tärkeää suurten QPS-työmäärien tehokkaassa käsittelyssä, mutta se vaikeuttaa kyselykohtaista CPU-käytön seurantaa, koska monien kyselyjen käsittely on yhdistetty samaan säikeistöpooliin.
Ratkaisumme on coarse_thread_cputime_clock, joka arvioi clock_gettime(CLOCK_THREAD_CPUTIME_ID, ...) -kutsua tarpeeksi edullisesti, jotta sitä voi mitata jokaisella tehtävärajalla. timer_create-API:lla voi ajastaa säännöllisen signaalin useiden ajan kulun käsitteiden perusteella, myös kertyneen CPU-ajan mukaan. Ajastamme signaalin (SIGUSR2) toimitettavaksi muutaman millisekunnin CPU-ajan välein, jolloin signaalinkäsittelijä päivittää säiekohtaisen arvon. Vaikka monet tehtävät eivät huomaa karkean kellon etenemistä suorituksen aikana, kaikkien erojen summaaminen tuottaa puolueettoman arvion kyselyn todellisesta CPU-ajasta.
Koska toimitamme signaaleja niin usein, harvinainen ytimen bugi kontekstinvaihdossa tai signaalien toimituksessa vaikutti uskottavalta. Käytimme aikaa bugiraporttien, ytimen lähdekoodin ja Azure-kohtaisten ydinpaikkausten lukemiseen. Kokeilimme stressitestejä. Emme löytäneet mitään asiaan liittyvältä vaikuttavaa.
Silloin päätimme ottaa askeleen taaksepäin ja kokeilla toista lähestymistapaa.
Tällaisen ongelman selvittämiseen on kaksi yleistä tapaa.
Yksi on toimia kuin lääkäri: keskittyä yhteen potilaaseen, tehdä paljon testejä ja yrittää diagnosoida yksittäinen tapaus yksityiskohtaisesta näytöstä.
Toinen on toimia enemmän epidemiologin tavoin: katsoa koko populaatiota ja kysyä, onko siinä kuvioita, joita yksittäinen tapaus ei voi paljastaa. Alkoiko bugi tietyssä julkaisussa? Korreloiko se yhden laitteisto-SKU:n (tietyn CPU- ja palvelinmallin), alueen tai ydinversion kanssa? Piileekö yhden oireyhtymän sisällä useita erillisiä klustereita?
Olimme olleet enimmäkseen lääkäritilassa. Ratkaiseva muutos oli päätös kerätä laadukasta populaatiodataa.
Aiemmat yrityksemme löytää kaikki ongelman esiintymät automaattisesti epäonnistuivat, koska yritimme käyttää lokien tekstihakuja. Core dumpit sisältävät paljon enemmän tietoa, mutta niiden manuaalinen katselu ei skaalautunut. Päätimme rakentaa putken, joka analysoisi core dumpit automaattisesti.
Pyysimme ChatGPT:tä kirjoittamaan skriptin, joka latasi kunkin core-tiedoston prefiksin, poimi rekisterit, suodatti tunnetut väärät positiiviset lokien avulla ja merkitsi kaatumisen automaattisesti luokkiin return-to-null, misaligned-stack tai muu. Ajoimme skriptin rinnakkain kaikille edellisen vuoden tuotannon Rockset-core dumpeille.
Tämä oli käännekohta.
Kun meillä oli puhdas aineisto, korrelaatiot näkyivät heti. Se, mitä olimme pitäneet yhtenä outona bugina, olikin kaksi erillistä kaatumispopulaatiota.
Return-to-null-coret jakautuivat moniin klustereihin ja maantieteellisiin alueisiin. Niiden taajuus oli kasvanut viime aikoina, mutta selkeää aloituspäivää tai puhdasta infrastruktuurirajaa ei ollut.
Väärin kohdistuneen pinon kaatumiset näyttivät aivan erilaisilta. Ne tulivat kaikki yhdeltä alueelta, niillä oli selvä aloituspäivä, eikä niitä koskaan tapahtunut solmuissa, jotka olivat olleet käynnissä pitkään. Vaikka niissä oli mukana useita Azuren VM:iä (pilvessä isännöityjä virtuaalikoneita), kuvio näytti siltä, että yksi viallinen fyysinen kone aiheutti ongelmia mille tahansa VM:lle, joka sattui päätymään sille.
Silloin tajusimme yhdistäneemme mielessämme kaksi bugia. Koska sekoitimme molempien bugien vastaesimerkkejä, emme löytäneet yhtä johdonmukaista selitystä.
Kun meillä oli puhdas lista Kubernetes-solmuista ja aikaleimoista, pystyimme jäljittämään väärin kohdistuneen pinon kaatumiset yhteen fyysiseen isäntään, joka oli helppo estolistata.
Emme pystyneet toistamaan rekisterikorruptiota kyseisellä isännällä hallitussa ympäristössä edes useiden viikkojen stressitestauksen jälkeen. Kun ongelmallinen isäntä poistettiin käytöstä, väärin kohdistuneen pinon kaatumiset kuitenkin katosivat.
Viallisen isännän poistaminen ei ole pysyvä ratkaisu siinä mielessä, ettei se estä saman ongelman uutta esiintymää. Voimme kuitenkin muuttaa ohjelmistoa niin, että vastaava ongelma havaitaan ja käsitellään helposti, jos se toistuu. Paransimme fatal signal handleriamme lisäämällä rekisteritilan, jotta toistuma voidaan havaita pelkistä lokeista ilman core dumpia. Muutimme ohjaustasoa niin, että VM:t yleensä käytetään uudelleen eikä kierrätetä, mikä helpottaa viallisten solmujen havaitsemista meidän infrastruktuuritasollamme. Päivitimme myös runbookimme ja tiimimme mentaalimallit huomioimaan tämän mahdollisuuden.
Kun viallisten isäntien kaatumiset erotettiin, jäljelle jääneitä return-to-null-coreja oli paljon helpompi ymmärtää. Aiemmin olimme sulkeneet poikkeusten purun pois, koska luulimme meillä olevan vastaesimerkkejä: kaatumisia koodipoluissa, joissa poikkeuksia ei varmasti käytetty. Mutta kaikki nuo vastaesimerkit olivat laitteistokorruptioklusterista.
Kun palasimme jäljellä oleviin ytimiin tämä mielessä, huomasimme päätelmän olleen täysin nurinpäin: kaikki kaatumiset tapahtuivat poikkeusten purun aikana.
Kun C++ heittää poikkeuksen, ajonaikaisen ympäristön on selvitettävä, mille catch-lohkolle se kuuluu ja mitkä destruktorit tai siivouskäsittelijät ajetaan matkalla. Kääntäjä tuottaa metadatan, mutta varsinainen täsmäytys tapahtuu dynaamisesti ajon aikana.
Poikkeuksen purkua ei itse asiassa tee throw-kutsun tekevä funktio, vaan syntyvän käännetyn koodin kutsumat apufunktiot. Nämä ajonaikaiset rutiinit tutkivat pinoa, hakevat metadatan pinosta löytyneistä funktioista, etsivät dynaamisesti siivouskäsittelijöitä ja catch-lohkoja ja siirtävät sitten ohjauksen johonkin niistä. Ohjauksen siirtoon kuuluu kaikkien välissä olevien pinokehysten purkaminen, myös apufunktioiden kehysten.
Käytännössä tämä muistuttaa paljon enemmän longjmpia tai fiber-vaihtoa kuin normaalia kutsua ja paluuta. Kutsutun funktion tallennetut rekisterit on palautettava, samoin kuin pino-kehyksen rekisterit %rbp ja %rsp.
Binäärimme linkittyy kahteen kirjastoon, joissa on toteutukset C++-poikkeusten purkua tekeville funktioille: libgcc:hen ja GNU libunwindiin. Dynaaminen linkkeri valitsi GNU libunwindin määritelmät. Se yllätti meidät; odotimme libgcc-toteutuksen voittavan symboliversiointisääntöjen vuoksi, mutta ajavien binäärien tarkastus osoitti muuta.
Tässä vaiheessa työhypoteesimme muuttui, kun höllensimme toista oletusta, jonka olimme tehneet luullessamme, että bugeja oli vain yksi.
Ehkä emme nähneet tavallista funktion paluuta NULLiin. Ehkä näimme unwind-siirron – käytännössä setcontext-tyylisen rekisterien palautuksen – jossa kohdekäskyosoitin oli muuttunut NULLiksi ennen ohjauksen siirtoa. Toisin sanoen kyse oli unwind-kirjaston virheellisestä datasta eikä pinon virheellisestä paluuosoitepaikasta.
Se rajasi ongelmaa dramaattisesti. Joko GNU libunwind laski väärän kohdetilan, tai se laski oikean tilan ja jokin korruptoi sen ennen käyttöä.
Luimme GNU libunwindin lähdekoodia ja havaitsimme, että se syntetisoi pinoon ucontext_t:n, täyttää siihen halutun rekisteritilan siivouskäsittelijän kehykselle ja antaa sitten osoittimen tähän rakenteeseen sisäiselle kokoonpanorutiinille: _Ux86_64_setcontext.
Tässä vaiheessa meillä oli kaikki palaset.
Syntetisoitu ucontext_t sijaitsee yhdessä pinokehyksistä, jotka _Ux86_64_setcontext purkaa oman suorituksensa aikana. Lukiko _Ux86_64_setcontext rakenteesta sen jälkeen, kun se oli muuttanut %rsp:tä, jolloin rakenne ei enää ollut osa aktiivista pinoa? Se tekisi siitä alttiin signaalin toimituksen aiheuttamalle ylikirjoitukselle, kuten usein saapuvalle SIGUSR2:llemme.
Vastaus oli kyllä.
Tässä ovat käyttämämme GNU libunwind -version _Ux86_64_setcontextin viimeiset kuusi käskyä. Ne ovat enimmäkseen mov-käskyjä, jotka lataavat muistista kohdereksiteriin:
(%rdi osoittaa pinoon varattuun ucontext_t:hen, ja UC_MCONTEXT_*-makrot laajenevat vain kiinteäksi siirtymäksi, johon tietty rekisteri on tallennettu.)
Ensimmäinen käsky aloittaa kilpailuikkunan. Se päivittää %rsp:n osoittamaan aktiivisen pinon uuteen pohjaan. Heti kun näin tapahtuu, %rdi:n osoittama rakenne ei enää ole osa aktiivista pinoa tai red zonea eikä siten enää ytimen koskemattomissa.
Yleensä tämä ei aiheuta ongelmia, mutta jos signaali saapuu täsmälleen oikealla (väärällä?) hetkellä, ydin rakentaa signaalikehyksen kohtaan %rsp-128. Se voi ylikirjoittaa muistin, johon %rdi osoittaa.
Jos näin tapahtuu ennen kuin seuraava käsky lukee UC_MCONTEXT_GREGS_RIP(%rdi):n, palautettava käskyosoitin voi korruptoitua. Meidän kaatumisissamme siitä tuli NULL.
Se on bugi.
Tämä kokoonpano selittää myös yhden meitä hämmentäneen havainnon: miksi funktiolla X oli NULL edeltävän pinokehyksen paluuosoitepaikassa.
setcontext kirjoitettiin palauttamaan kaikki rekisterit, myös %rdi, joten se ei voi käyttää sitä rekisteriä lukemaan UC_MCONTEXT_GREGS_RIP(%rdi)iä ohjauksen siirron viime hetkellä. Sen sijaan se lukee arvon aiemmin, tallentaa sen pinoon, palauttaa vielä muutaman rekisterin ja käyttää sitten retq:ta tallennetun arvon lukemiseen ja ohjauksen siirtämiseen.
Se, mikä ytimissä näytti siltä kuin ”funktio palasi NULLiin”, olikin todellisuudessa sitä, että ”unwinder synteettisesti loi kohdepaluuosoitteen pinoon, mutta kohde korruptoitui ennen siirron valmistumista”. Oletimme, että paluuosoitepaikan korruptio tapahtuu paikallaan, koska emme tienneet paikkoja, joissa korruptoituvaa dataa kirjoitettaisiin paluuosoitepaikkaan tarkoituksella.
Bugi tuntuu absurdilta, koska kilpailuikkuna on niin kapea. Tällaisessa kilpailutilanteessa ulkoisen tapahtuman eli signaalin täytyy osua kahden toisen säikeen tekemän vaiheen väliin. Mitä lähempänä vaiheet ovat toisiaan, sitä epätodennäköisempi kilpailutilanne on.
Tässä tapauksessa haavoittuva ikkuna on kirjaimellisesti yhden käskyn levyinen! Signaalin on saavuttava sen jälkeen, kun %rsp on muutettu, mutta ennen kuin seuraava käsky lataa %rip:n. Nykyaikainen superskalaarinen out-of-order-CPU voi suorittaa useita tällaisia yksinkertaisia käskyjä sykliä kohti, joten kilpailuikkuna on noin sata pikosekuntia.
Kun löysimme tämän kilpailutilanteen, ensimmäinen ajatuksemme oli, että sen täytyy olla liian harvinainen selittämään havaittua kaatumisastetta. Havaitsimme koko laivastossa yli tusina ”return-to-null”-kaatumista päivässä. Voisiko yksi käsky, joka aiheuttaa kilpailutilanteen poikkeuksen puhdistuksen aikana, todella selittää tämän?
Turvauduimme Fermat-arviointiin. Jos haavoittuva ikkuna on suuruusluokkaa sekuntia ja SIGUSR2 saapuu joka CPU-aikasekunti, jokaisella poikkeuksen siivouskäsittelijällä tai catch-lohkolla on noin todennäköisyys hävitä kilpailu.
Rockset käyttää poikkeuksia osana sisäistä syöttörajoitusmekanismiaan. Yksi ylikuormitettu isäntäkone voi heittää suuruusluokkaa poikkeusta sekunnissa. Tämä tarkoittaa, että syöttörajoitusta käyttävän isäntäkoneen keskimääräinen vikaantumisväli on sekuntia, eli yksi kaatuminen muutaman tunnin välein. Koko valikoiman mittakaavassa tämä riittää mainiosti selittämään havaitun kaatumistiheyden.
GNU libunwindin bugi on vanha: yli 18 vuotta vanha, mukana ensimmäisessä C++-poikkeusten purkua tukeneessa x86_64-versiossa.
Miksi se siis näkyi vasta nyt?
Kaatumistiheys on suunnilleen verrannollinen heitettyjen poikkeusten ja toimitettujen signaalien määrään. Se riippuu myös siitä, paljonko pinoa signaalinkäsittelijä kuluttaa.
Rockset on epätavallinen kaikilla kolmella akselilla. Heitämme poikkeuksia suuria määriä osana normaalia ylikuormituksen hallintaa; toimitamme SIGUSR2-signaaleja poikkeuksellisen usein coarse_thread_cputime_clockin takia; ja aiemmin tänä vuonna lisäsimme SIGUSR2-käsittelijän pinonkulutusta lisäämällä kutsun timer_getoverruniin, jotta voimme huomioida yhdistyneet signaalit.
Viimeinen muutos näyttää olleen tärkeä. Jos käsittelijä käyttää tarpeeksi vähän pinoa, se ei ehkä yllä ylikirjoittamaan vanhentunutta ucontext_t-muistia. Ennen muutosta emme havainneet näitä kaatumisia lainkaan. Muutoksen jälkeen tahti pysyi matalana, kunnes nostimme kuormaa joissakin käyttötapauksissa, jotka rasittivat vastapainemekanismia.
Toisin sanoen libunwind-bugi oli aina ollut olemassa, mutta poikkeustahtimme, signaalitahtimme ja käsittelijän pinonkulutuksen tulo ylitti vasta hiljattain rajan, jossa se näkyi tuotannossa.
Tämä mekanismi selittää myös sattuman, että sekä laitteistobugi että libunwind-bugi kaatuivat enimmäkseen DocumentTree::updateDocumentin sisällä. libunwindin kaatumiset painottuivat vahvasti tähän metodiin, koska se on aina aktiivinen, kun heitämme poikkeuksen ingest-vastapaineen käyttämiseksi. Se valikoitui vahvasti myös %rsp-väärinkohdistuskaatumisiin, koska viallinen laitteistosolmu oli SKU:ta, jota käytämme massatiedonsyöttöön, ja se viettää suurimman osan CPU-ajastaan tässä metodissa.
Välitön lievennyksemme oli vaihtaa GNU libunwind libgcc:n unwinderiin. Se oli jo itsessään hyvä vaihto: libgcc:n toteutus on hyötynyt suuresta työstä lukkokilpailun vähentämiseksi, mikä on tärkeää suuria VM:iä skaalattaessa.
Lähetimme GNU libunwindiin myös itsenäisen toistajan ja korjauksen(avautuu uudessa ikkunassa) sekä varmistimme, ettei muissa unwindereissa ole vastaavaa ongelmaa.
Tämä vianetsintämatka opetti meille paljon dynaamisen linkityksen, DWARF-unwind-metadatan, Linuxin signaalien toimituksen, System V ABI:n ja C++-poikkeusmekanismin yksityiskohdista. Pääopetus oli silti kaikkea sitä yksinkertaisempi.
Tärkein askel ei ollut nokkela kokoonpanon lukeminen tai syvä yksityiskohtien tuntemus. Se oli laadukkaan aineiston rakentaminen. Ilman tätä aineistoa sekoitimme kaksi erillistä ilmiötä yhdeksi tarinaksi ja yritimme päätellä tiemme ulos sekavuudesta. Kun meillä oli tarkka ja kattava populaatiodata, ongelman rakenne kävi ilmeiseksi: yksi kaatumispopulaatio kuului vialliselle isännälle ja toinen libunwindin kilpailulle. Kun data parani, virheenkorjaus helpottui.
Rocksetin kaltaisissa infrastruktuurijärjestelmissä sillä on suuri merkitys. Tutkimus vahvisti sitoutumistamme syvään instrumentointiin, automatisoituihin tutkimuksiin ja operatiivisten työkalujemme jatkuvaan parantamiseen. Luotettavuus ei ole vain bugien korjaamista jälkikäteen, vaan datan, työnkulkujen ja taitojen rakentamista niin, että mahdottomilta näyttävistä ongelmista tulee diagnosoitavia ja ratkaistavia.
Tekijät
By Nathan Bronson ja Member of Technical Staff


