Atminties išklotinių epidemiologija: taisome 18 metų klaidą
Populiacijos lygmens analizė sudėtingoms strigtims mūsų duomenų infrastruktūroje derinti.
OpenAI modeliai ir agentai vis labiau remiasi plečiama duomenų infrastruktūra, kad išvedimo metu, kai modeliai svarsto jūsų klausimą, rastų aktualius duomenis. Kai kurios iš šių paslaugų parašytos C++ kalba; jos žemo lygio sistemos valdymas leidžia maksimaliai didinti našumą ir mažinti atminties naudojimą. Plečiantis mastui šis efektyvumas svarbus, bet dėl C++ atminties saugos stokos klaidos gali sukelti strigtis rašant į neteisingus ar neegzistuojančius atminties adresus.
Prieš kelis mėnesius pastebėjome strigčių Rockset paslaugoje – specialioje mūsų ChatGPT duomenų infrastruktūros dalyje, svarbioje daugeliui duomenų papildinių ir paieškai pokalbiuose. Kiekvienoje iš šių strigčių įprasta C++ funkcija, atrodė, baigdavo darbą ir grįždavo į netikrą adresą, todėl branduolys sustabdydavo programą, nes instrukcijų rodyklė neberodė į kodą. Kartais grįžimo adreso vieta dėklo kadre buvo NULL. Kartais pats dėklo rodyklės CPU registras, atrodė, buvo pasislinkęs 8 baitais, tarsi %rsp kažkaip būtų sumažintas įprasto vykdymo metu. Abiem atvejais strigtis įvykdavo grįžtant.
Programų kodui tai nėra įprasti gedimo režimai. Atsitiktinis įrašymas, pataikantis tik į išsaugotą grįžimo adresą, įmanomas, bet itin mažai tikėtinas. Klaida, kuri paslenka %rsp 8 baitais be įterptinės asemblerio kalbos, setcontext ar longjmp (jų nenaudojame), dar keistesnė, nes sukompiliuotas kodas šį registrą tiesiogiai keičia tik funkcijos prologe ir epiloge. Prieš kiekvieną hipotezę, kurią sugalvojome mes (ar ChatGPT), buvo rimtų įrodymų, tad klaida atrodė neįmanoma.
Tai, ką laikėme viena problema, galiausiai pasirodė esančios dvi nesusijusios klaidos, atsitiktinai aptiktos tuo pačiu metu. Pirma – tyli aparatinės įrangos korupcija viename Azure serveryje, kur CPU tiesiog neteisingai skaičiavo. Antra – 18 metų senumo lenktyniavimo sąlyga GNU libunwind, nepastebėta plačiai naudojamos atvirojo kodo bibliotekos klaida.
Šiame įraše pasakojame, kaip identifikavome ir pataisėme, regis, nepaaiškinamas strigtis mąstydami kaip epidemiologai ir sudarydami kokybišką visos strigčių populiacijos duomenų rinkinį.
Pirmiausia pažvelkime į Rockset giliau. Tai debesijai pritaikyta duomenų sistema paieškai ir realiojo laiko analitikai, kurią OpenAI naudojame daugeliui vidinių paskirčių, pvz., sinchronizavimo jungtims (Rockset OpenAI įsigijo 2024 m.). Srautiniai atnaujinimai palaiko naują darbo erdvės žinių bazės indeksą, kad ChatGPT galėtų ieškoti aktualios informacijos atsakydamas į klausimus ar atlikdamas veiksmus.
Rockset vykdymo sluoksnis parašytas C++. C++ kalba suteikia žemo lygio prieigą prie CPU, o tai naudinga našumui ir efektyvumui, bet reiškia, kad programos klaidos gali lemti neteisingas atminties prieigas ir segmentavimo klaidas. Kad jas atsektume, naudojame folly fatal signal handler, kuris strigties metu įrašo dėklo trasą, o atitinkamas atminties išklotines (programos būsenos momentinę kopiją strigties metu) įkeliame į Azure blob saugyklą vėlesnei analizei. Visi Rockset užklausų apdorojimo lapai replikuojami, todėl strigties poveikis klientui sumažinamas. Vis dėlto kiekviena segmentavimo klaida reiškia klaidą, kurią reikia taisyti, kad pasiektume patikimumo ir kokybės tikslus.
Iš pradžių šias išklotines traktavome kaip įprastą derinimo uždavinį: labai atidžiai nagrinėti kelias atminties išklotines, kelti hipotezes ir jas paeiliui atmesti.
Dauguma strigčių įvyko metode DocumentTree::updateDocument. Šiose strigtyse atrodė, kad updateDocument iškvietė nežinomą funkciją X, dėklas buvo sugadintas, kol X veikė, o tada X grįžo į adresą, kuris nebuvo vykdomasis kodas. Kai kuriais atvejais ką tik pašalintas X kadras atrodė teisingas, išskyrus tai, kad jo išsaugotas grįžimo adresas buvo NULL. Kitais atvejais pats dėklo rodyklė atrodė neteisinga, bet kitas galiojantis kadras vis tiek, regis, buvo updateDocument.
Nežinojome, kada dėklas sugadinamas, todėl paieškos erdvė buvo milžiniška. updateDocument yra didelis metodas, kuriame daug įterpimo, tad kandidatų į X buvo pribloškiamai daug.
Ar tai mūsų C++ kodo klaida? Kompiliatoriaus ar susiejimo problema? Problema vienoje iš vykdymo bibliotekų? Linux branduolio klaida, susijusi su signalų pateikimu ar konteksto perjungimu? Kažkas dar retesnio? Jei tai buvo atsitiktinis įrašymas, kodėl jo nepagavo mūsų ASAN testinė aplinka?
Bandėme naudoti programos lygio žurnalus visiems problemos atvejams rasti, bet dėklo korupcijos klaidas sunku klasifikuoti vien iš žurnalų, nes pačios įrašytos dėklo trasos būna sugadintos arba jų nėra. Nepavyko sudaryti žurnalo užklausos, kurioje nebūtų ir klaidingų teigiamų, ir klaidingų neigiamų rezultatų. Rankiniu būdu peržiūrėjome daugiau išklotinių ir radome papildomų pavyzdžių, bet toks procesas buvo per daug imlus darbui, kad duotų patikimą duomenų rinkinį.
Šiame tyrimo etape (neteisingai) atmetėme aparatinės įrangos klaidą, nes matėme strigtis keliuose regionuose ir kelių tipų aparatinėje įrangoje, todėl vis dar ieškojome vien programinių priežasčių. Kelias dienas labai giliai nagrinėjome vieną neteisingai sulygiuoto %rsp strigtį, atkurdami istoriją iki strigties iš dėklo ir registrų turinio. Tai davė galimų užuominų, bet neatsisakėme pradinės išvados, kad visos klaidos turi tą pačią priežastį, todėl iš aklavietės neišėjome.
Prieš pereinant prie tyrimo lūžio taško, svarbu paaiškinti, kokią informaciją išgaudavome iš atminties išklotinių failų.
Rockset kompiliuojamas su -fno-omit-frame-pointer, todėl aktyvus dėklo kadras visada pasiekiamas per %rbp, o kvietėjai sudaro susietą kadrų rodyklių sąrašą.
Linux x86_64 sistemoje AMD64 System V ABI taip pat rezervuoja 128 baitus žemiau %rsp kaip raudonąją zoną. Ši sritis prieinama naudotojo erdvės kodui ir, svarbu, branduolys pagal ABI sutartį pažada jos nepaliesti pateikdamas signalą.
Raudonoji zona buvo esminė derinant strigtį po grįžimo, nes joje išlieka dalis informacijos iš laiko prieš grįžimą. Kai suveikia SIGSEGV, folly lemtingo signalo tvarkytuvas veikia stringančios gijos dėkle. Dėklo kadrai, kurie nebėra aktyvūs (nes jų funkcija grįžo), bus perrašyti signalo tvarkytuvo, išskyrus paskutinius 128 baitus. Todėl galime sakyti tokius dalykus kaip „ką tik pašalintas X dėklo kadras atrodė teisingas, išskyrus NULL grįžimo adresą“. Raudonoji zona išsaugo dalį neaktyvių kadrų, o kartais tik vieno neaktyvaus kadro pabaigą.
Radome vieną neteisingai sulygiuoto dėklo strigtį, kurioje visos susijusios funkcijos buvo labai mažos. Tai leido pamatyti, kad %rsp tapo neteisingai sulygiuotas vykdant gana paprastą funkciją, o po to dar pavyko atlikti daugiau kvietimų. Programa sugedo tik tada, kai aktyvi funkcija galiausiai pabandė grįžti. Nė vienas iš tų kodo kelių nenaudojo išimčių, įterptinės asemblerio kalbos, setcontext ar longjmp, tad jei dėklo rodyklė tikrai pasikeitė taip, kaip rodė išklotinė, jokia tikėtina naudotojo erdvės kodo klaida to nepaaiškino.
Tai pastūmėjo mus link branduolio.
Rockset signalus naudoja agresyviau nei dauguma programų. Užklausų vykdymas suskaidytas į daug lengvų užduočių, kurios keičiasi duomenimis. Tai svarbu efektyviai apdorojant didelio QPS apkrovas, bet apsunkina CPU apskaitą pagal užklausą, nes daugelio užklausų darbas multipleksuojamas tame pačiame gijų telkinyje.
Mūsų sprendimas yra tai, ką vadiname coarse_thread_cputime_clock: jis pakankamai pigiai aproksimuoja clock_gettime(CLOCK_THREAD_CPUTIME_ID, ...), kad būtų galima imti mėginį ties kiekviena užduoties riba. timer_create API galima naudoti periodiniam signalo pateikimui suplanuoti pagal kelias laiko tėkmės sampratas, įskaitant CPU laiko kaupimąsi. Suplanuojame, kad signalas (SIGUSR2) būtų pateikiamas kas kelias CPU laiko milisekundes; tada signalo tvarkytuvas atnaujina gijos vietinę reikšmę. Nors daug užduočių vykdymo metu nemato, kad grubus laikrodis pajudėjo, visų deltų suma duoda nešališką faktinio užklausos CPU laiko įvertį.
Kadangi signalus pateikiame taip dažnai, reta branduolio klaida konteksto perjungime ar signalų pateikime atrodė tikėtina. Skyrėme laiko klaidų ataskaitoms, branduolio šaltinio kodui ir Azure specifiniams branduolio pataisymams skaityti. Bandėme apkrovos testus. Nepavyko rasti nieko, kas atrodytų susiję.
Tada nusprendėme atsitraukti ir išbandyti kitą požiūrį.
Yra du pagrindiniai būdai derinti tokią problemą.
Vienas – elgtis tarsi gydytojui: sutelkti dėmesį į vieną pacientą, atlikti daug tyrimų ir bandyti diagnozuoti atvejį iš detalių įrodymų.
Kitas – veikti labiau kaip epidemiologui: žiūrėti į visą populiaciją ir klausti, ar yra dėsningumų, kurių vienas atvejis neatskleidžia. Ar klaida prasidėjo konkrečioje laidoje? Ar ji koreliuoja su vienu aparatinės įrangos SKU (konkrečiu CPU ir serverio modeliu), vienu regionu ar viena branduolio versija? Ar po tuo, kas atrodo kaip vienas sindromas, slepiasi keli atskiri klasteriai?
Dažniausiai buvome gydytojo režime. Esminis pokytis buvo sprendimas rinkti kokybiškus populiacijos duomenis.
Ankstesni bandymai automatiškai rasti visus problemos atvejus nepavyko, nes bandėme ieškoti teksto žurnaluose. Pačiose atminties išklotinėse informacijos daug daugiau, bet rankinis jų peržiūrėjimas nesiplėtė. Nusprendėme skirti pastangų konvejeriui, kuris automatiškai analizuotų atminties išklotines.
Paprašėme ChatGPT parašyti scenarijų, kuris atsisiųstų kiekvieno išklotinės failo prefiksą, ištrauktų registrus, pagal žurnalus atfiltruotų žinomus klaidingus teigiamus atvejus ir automatiškai pažymėtų strigtį kaip grįžimą į nulį, neteisingai sulygiuotą dėklą arba kitą. Tada tą scenarijų lygiagrečiai paleidome visoms ankstesnių metų produkcinėms Rockset atminties išklotinėms.
Tai buvo lūžio taškas.
Kai turėjome švarų duomenų rinkinį, koreliacijos pasirodė iškart. Tai, ką laikėme viena keista klaida, iš tikrųjų buvo dvi atskiros strigčių populiacijos.
Grįžimo į nulį išklotinės buvo pasklidusios po daugelį klasterių ir geografinių regionų. Jų dažnis neseniai padidėjo, bet nebuvo aiškios pradžios datos ar švarios infrastruktūros ribos.
Neteisingai sulygiuoto dėklo strigtys atrodė visiškai kitaip. Visos jos buvo iš vieno regiono, turėjo aiškią pradžios datą ir niekada neįvyko mazguose, kurie veikė ilgą laiką. Nors jos apėmė kelias Azure VM (debesijoje veikiančias virtualias mašinas), dėsningumas rodė vieną fizinį kompiuterį su bloga aparatine įranga, keliantį problemų bet kuriai VM, kuri ant jo atsidurdavo.
Tą akimirką supratome, kad mintyse suplakėme dvi klaidas. Kadangi maišėme abiejų klaidų kontrpavyzdžius, negalėjome rasti vieno nuoseklaus paaiškinimo.
Turėdami švarų Kubernetes mazgų ir laiko žymų sąrašą, neteisingai sulygiuoto dėklo strigtis atsekėme iki vieno fizinio serverio, kurį buvo lengva įtraukti į draudžiamųjų sąrašą.
Net po kelių savaičių apkrovos testų nepavyko kontroliuojamoje aplinkoje atkurti registrų korupcijos tame serveryje. Tačiau pašalinus probleminį serverį iš eksploatacijos, neteisingai sulygiuoto dėklo strigtys dingo.
Blogą serverį pašalinti nėra nuolatinis sprendimas ta prasme, kad tai neapsaugo nuo naujo tos pačios problemos pasikartojimo. Vis dėlto galime pakeisti programinę įrangą taip, kad panašiai problemai pasikartojus ją būtų lengva aptikti ir sutvarkyti. Patobulinome lemtingo signalo tvarkytuvą, kad jis įtrauktų registrų būseną ir pasikartojimą galėtume aptikti vien iš žurnalų (be atminties išklotinės). Pakeitėme valdymo plokštumą, kad VM paprastai būtų pakartotinai naudojamos, o ne perdirbamos; taip mūsų infrastruktūros sluoksnyje daug lengviau aptikti blogus mazgus. Taip pat atnaujinome runbook'us (ir komandos mąstymo modelius), kad įtrauktume šią galimybę.
Atskyrus blogo serverio strigtis, likusias grįžimo į nulį išklotines tapo daug lengviau suprasti. Anksčiau buvome atmetę išimčių išvyniojimą, nes manėme turį kontrpavyzdžių: strigčių kodo keliuose, kur išimtys tikrai nebuvo naudojamos. Tačiau visi tie kontrpavyzdžiai buvo iš aparatinės įrangos korupcijos klasterio.
Kai su tuo mintyje dar kartą peržiūrėjome likusias išklotines, pamatėme, kad išvada buvo visiškai priešinga: visos strigtys vyko išimčių išvyniojimo metu.
Kai C++ meta išimtį, vykdymo aplinka turi nustatyti, kuris catch blokas ją turėtų gauti ir kurie destruktoriai ar valymo tvarkytuvai turi būti paleisti pakeliui. Kompiliatorius išveda šiuos metaduomenis, bet tikrasis atitikimas vyksta dinamiškai vykdymo metu.
Išimties išvyniojimą iš tikrųjų atlieka ne funkcija, iškviečianti throw, o pagalbinės funkcijos, kurias kviečia gautas sukompiliuotas kodas. Šios vykdymo procedūros tiria dėklą, paima metaduomenis apie dėkle rastas funkcijas, dinamiškai ieško valymo tvarkytuvų ir catch blokų, o tada perduoda valdymą į vieną iš tų vietų. Valdymo perdavimas apima visų tarpinių dėklo kadrų išvyniojimą, įskaitant pagalbinių funkcijų kadrus.
Operaciniu požiūriu tai daug artimiau longjmp arba skaidulos perjungimui nei įprastam kvietimui ir grįžimui. Turi būti atkurti callee save registrai, taip pat dėklo kadrų registrai %rbp ir %rsp.
Mūsų dvejetainis failas susietas su dviem bibliotekomis, kuriose yra funkcijų, atliekančių C++ išimčių išvyniojimą, įgyvendinimai: libgcc ir GNU libunwind. Dinaminis susietuvas pasirinko GNU libunwind apibrėžtis. Tai mus nustebino: tikėjomės, kad dėl simbolių versijavimo taisyklių laimės libgcc įgyvendinimas; tačiau veikiančių dvejetainių failų patikra parodė, kad taip nėra.
Šiuo metu mūsų darbinė hipotezė pasikeitė, nes atlaisvinome dar vieną prielaidą, kurią buvome padarę manydami, kad yra tik viena klaida.
Galbūt nematėme įprasto funkcijos grįžimo į NULL. Galbūt matėme išvyniojimo perdavimą – iš esmės setcontext tipo registrų atkūrimą, kai paskirties instrukcijų rodyklė tapo NULL dar prieš perduodant valdymą. Kitaip tariant, neteisingus duomenis iš išvyniojimo bibliotekos, o ne neteisingą grįžimo adreso vietą dėkle.
Tai smarkiai susiaurino problemą. Arba GNU libunwind skaičiavo neteisingą paskirties būseną, arba skaičiavo teisingą būseną, bet kažkas ją sugadino dar prieš pritaikant.
Perskaitėme GNU libunwind šaltinį ir radome, kad ji dėkle sintetina ucontext_t, užpildo norimą registrų būseną valymo tvarkytuvo kadrui ir tada perduoda rodyklę į tą struktūrą vidinei asemblerio procedūrai: _Ux86_64_setcontext.
Šiuo metu turėjome visas dalis.
Susintetintas ucontext_t gyvena viename iš dėklo kadrų, kuriuos _Ux86_64_setcontext išvynioja tos funkcijos vykdymo metu. Ar _Ux86_64_setcontext skaitė iš struktūros po to, kai pakeitė %rsp, kai struktūra jau nebebuvo aktyvaus dėklo dalis? Tai padarytų ją pažeidžiamą perrašymui pateikiant signalą, pvz., mūsų dažną SIGUSR2.
Atsakymas buvo taip.
Štai paskutinės šešios _Ux86_64_setcontext instrukcijos mūsų naudotoje GNU libunwind versijoje; daugiausia tai mov instrukcijos, įkeliančios iš atminties į paskirties registrą:
(%rdi rodo į dėkle paskirtą ucontext_t, o UC_MCONTEXT_* makrokomandos tiesiog išsiplečia į fiksuotą poslinkį, kuriame saugomas konkretus registras.)
Pirmoji instrukcija yra lenktyniavimo lango pradžia. Ji atnaujina %rsp, kad rodytų į naują aktyvaus dėklo apačią. Vos tai įvyksta, struktūra, į kurią rodo %rdi, nebėra aktyvaus dėklo (ar raudonosios zonos) dalis ir branduoliui jos saugoti nebereikia.
Paprastai tai problemų nesukelia, bet jei signalas ateina tiksliai tinkamu (neteisingu?) momentu, branduolys sukurs signalo kadrą ties %rsp-128. Tai gali perrašyti atmintį, į kurią rodo %rdi.
Jei tai įvyksta prieš kitai instrukcijai perskaitant UC_MCONTEXT_GREGS_RIP(%rdi), atkuriama instrukcijų rodyklė gali būti sugadinta. Mūsų strigtyse ji tapo NULL.
Štai ir klaida.
Ši asemblerio seka taip pat paaiškina vieną mus klaidinusių pastebėjimų: kodėl funkcijos X ankstesnio dėklo kadro grįžimo adreso vietoje buvo NULL.
setcontext buvo parašyta atkurti visus registrus, įskaitant %rdi, todėl paskutinę valdymo perdavimo akimirką ji negali naudoti šio registro UC_MCONTEXT_GREGS_RIP(%rdi) skaityti. Vietoj to ji reikšmę perskaito anksčiau, išsaugo ją dėkle, atkuria dar kelis registrus, o tada naudoja retq išsaugotai reikšmei perskaityti ir valdymui perduoti.
Tai, kas išklotinėse atrodė kaip „funkcija grįžo į NULL“, iš tikrųjų buvo „unwinder dėkle susintetino tikslinį grįžimo adresą, bet tas tikslas buvo sugadintas prieš baigiant perdavimą“. Manėme, kad grįžimo adreso vieta turi būti sugadinama vietoje, nes nežinojome vietų, kuriose (sugadinami) duomenys sąmoningai rašomi į grįžimo adreso vietą.
Ši klaida atrodo absurdiška dėl to, koks siauras yra lenktyniavimo langas. Tokioje lenktyniavimo sąlygoje išorinis įvykis (signalas) turi įvykti tarp dviejų kitos gijos atliekamų žingsnių. Kuo tie žingsniai arčiau vienas kito, tuo mažiau tikėtina lenktyniavimo sąlyga.
Šiuo atveju pažeidžiamas langas tiesiogine prasme yra vienos instrukcijos pločio! Signalas turi būti pateiktas po to, kai %rsp pakeičiamas, bet prieš kitai instrukcijai įkeliant %rip. Šiuolaikiniame superskaliariniame netvarkingos vykdymo eilės CPU per ciklą gali būti įvykdytos kelios tokios paprastos instrukcijos, tad lenktyniavimo langas yra maždaug šimtas pikosekundžių.
Kai radome šią lenktyniavimo sąlygą, pirma reakcija buvo: ji turėtų būti per reta, kad paaiškintų stebimą strigčių dažnį. Visame parke matėme daugiau nei tuziną grįžimo į nulį strigčių per dieną. Ar vienos instrukcijos lenktyniavimas valant išimtį tikrai galėjo tai paaiškinti?
Pasitelkėme Fermi įvertį. Jei pažeidžiamas langas yra apie sekundės, o SIGUSR2 ateina kas CPU laiko sekundės, tuomet kiekvienas išimties valymo tvarkytuvas ar catch blokas turi maždaug tikimybę pralaimėti lenktynes.
Rockset išimtis naudoja kaip vidinio įtraukimo atgalinio spaudimo mechanizmo dalį. Vienas perkrautas serveris gali mesti apie išimčių per sekundę. Tai reiškia, kad serverio, naudojančio atgalinį spaudimą, vidutinis laikas tarp gedimų yra sekundžių, arba viena strigtis kas kelias valandas. Viso parko mastu to daugiau nei pakanka stebimam strigčių dažniui paaiškinti.
GNU libunwind klaida sena – daugiau nei 18 metų, buvusi pirmojoje x86_64 versijoje, palaikiusioje C++ išimčių išvyniojimą.
Tad kodėl ji pasireiškė dabar?
Strigčių dažnis maždaug proporcingas metamų išimčių ir pateikiamų signalų skaičiui. Jis taip pat priklauso nuo to, kiek dėklo sunaudoja signalo tvarkytuvas.
Rockset neįprasta visose trijose ašyse. Išimtis metame dideliu dažniu kaip įprastos perkrovos kontrolės dalį; SIGUSR2 pateikiame neįprastai dažnai dėl coarse_thread_cputime_clock; o šių metų pradžioje privertėme SIGUSR2 tvarkytuvą naudoti daugiau dėklo pridėdami kvietimą į timer_getoverrun, kad galėtume apskaityti sulietus signalus.
Atrodo, kad paskutinis pakeitimas buvo svarbus. Jei tvarkytuvas naudoja pakankamai mažai dėklo, jis gali nepasiekti ir neperrašyti pasenusios ucontext_t atminties. Iki to pakeitimo šių strigčių visai nematome. Po pakeitimo dažnis išliko mažas, kol padidinome apkrovą kai kuriems naudojimo atvejams, apkrovusiems atgalinio spaudimo mechanizmą.
Kitaip tariant, libunwind klaida visada buvo, bet mūsų išimčių dažnio, signalų dažnio ir tvarkytuvo dėklo naudojimo sandauga tik neseniai peržengė ribą, nuo kurios ji tapo matoma eksploatacijoje.
Šis mechanizmas taip pat paaiškina sutapimą, kad ir aparatinės įrangos klaida, ir libunwind klaida dažniausiai strigo viduje DocumentTree::updateDocument. libunwind strigtys buvo stipriai nukreiptos į šį metodą, nes jis visada aktyvus tuo momentu, kai metame išimtį įtraukimo atgaliniam spaudimui taikyti. Jis taip pat buvo stipriai atrinktas %rsp nesulygiavimo strigtims, nes blogas aparatinės įrangos mazgas buvo tokio SKU, kurį naudojame masiniam įtraukimui, o jis didžiąją CPU laiko dalį praleidžia tame metode.
Skubi mūsų mitigacija buvo pereiti nuo GNU libunwind prie libgcc unwinder. Tai savaime buvo geras kompromisas: libgcc įgyvendinimas pasinaudojo daugybe darbų mažinant užraktų varžymąsi, o tai svarbu plečiantis iki didelių VM.
Taip pat į GNU libunwind aukštyn pateikėme savarankišką atkūrimo pavyzdį ir pataisą(atsidaro naujame lange), ir patikrinome, kad kiti unwinder neturi panašios problemos.
Ši derinimo kelionė daug išmokė apie konkrečias dinaminio susiejimo, DWARF išvyniojimo metaduomenų, Linux signalų pateikimo, System V ABI ir C++ išimčių mechanikos detales. Tačiau pagrindinė pamoka buvo paprastesnė už visa tai.
Svarbiausias žingsnis nebuvo gudrus asemblerio skaitymas ar gilios detalių žinios. Tai buvo kokybiško duomenų rinkinio sukūrimas. Be šio duomenų rinkinio du atskirus reiškinius jungėme į vieną istoriją ir bandėme iš jos išmąstyti kelią. Kai turėjome tikslius ir išsamius populiacijos duomenis, problemos struktūra tapo akivaizdi: viena strigčių populiacija priklausė blogam serveriui, kita – lenktyniavimui libunwind. Pagerėjus duomenims, derinimas palengvėjo.
Tokiose infrastruktūros sistemose kaip Rockset tai labai svarbu. Šis tyrimas sustiprino mūsų įsipareigojimą giliai instrumentacijai, automatizuotiems tyrimams ir nuolatiniam operacinių įrankių tobulinimui. Patikimumas nėra vien klaidų taisymas po jų atsiradimo – tai duomenų, darbo eigų ir įgūdžių kūrimas, paverčiantis neįmanomas problemas diagnozuojamomis ir išsprendžiamomis.
Autoriai
By Nathan Bronson ir Member of Technical Staff


