Nathan Bronson, a Rockset eszközén végzett vizsgálatát ismertetve, az OpenAI mérnökei két, egymástól független hibát azonosítottak, amelyek miatt a ChatGPT adat‑infrastruktúrájának egy, Rockset nevű komponense rendszeresen összeomlott. Az egyik probléma egyetlen Azure fizikai hoston jelentkező néma hardverhiba volt; a másik egy több mint 18 éve meglévő, figyelmen kívül hagyott race‑feltétel a GNU libunwind könyvtárában.
Mi történt és miért számít
Rockset egy felhőnatív keresési és valós idejű analitika rendszer, amelyet az OpenAI több belső célra használ (Rocksetet az OpenAI 2024‑ben vásárolta fel). A lekérdezések futtatása során a Rockset C++‑ban írt végrehajtási rétegét használja; a C++ nagy teljesítményt és hatékony memóriahasználatot tesz lehetővé, de memóriabiztonsági hiányosságok miatt a hibák súlyos memóriasérülésekhez és szegmentációs hibákhoz vezethetnek.
Néhány hónappal a vizsgálat előtt a csapat több összeomlást észlelt a Rockset szolgáltatásban. A core dumpok elemzésekor gyakran az látszott, hogy egy normál C++ függvény befejeződik, majd egy érvénytelen (például NULL) visszatérési címre próbál visszatérni, ami miatt a kernel leállítja a folyamatot. Más esetekben a veremmutató (%rsp) 8 bájttal eltérőnek tűnt, mintha közben félreállították volna. Ezek a viselkedések nem illeszkedtek a tipikus alkalmazási hibákhoz, és a kezdeti feltételezések sorra elvetődtek.
Diagnosztikai megközelítés: orvos kontra epidemiológus
A kezdeti módszer a hagyományos, „egy betegre fókuszáló” hibakeresés volt: néhány core közelről történő vizsgálata, hipotézisek felállítása és kizárása. Ez nem vezetett eredményre, mert a stack‑korrupt állapotok logokból nehezen osztályozhatók és a manuális elemzés lassú.
Végül a csapat áttért egy epidemiológiai megközelítésre: a teljes populációt elemezték automatikus csővezetékkel, amely minden évre vonatkozó production Rockset core dumpot feldolgozott. A pipeline által kigyűjtött és címkézett adathalmaz alapján tiszta klaszterek jelentek meg: a return‑to‑null típusú magyarázatok és a misaligned‑stack esetek külön populációt alkottak.
A hardverprobléma: egy fizikai host
A misaligned‑stack összeomlások mind egy régióból származtak, volt induló dátumuk, és soha nem jelentkeztek hosszabb ideje futó node‑okon. Ennek alapján a csapat visszakövette az eseményeket egyetlen fizikai hostig és azt denylistelte. A nehéz reprodukció ellenére, miután a hibás hostot kivonták a szolgáltatásból, a misaligned‑stack hibák megszűntek. Megoldásként továbbá a fatal signal handler‑t kiterjesztették a regiszterállapot naplózására (hogy a jövőben csak a logokból is észlelhető legyen), a kontroll‑síntémát úgy módosították, hogy a VM‑ek újrafelhasználása terjedjen el (könnyítve a rossz node detektálást), és frissítették a runbookokat.
A szoftveres probléma: 18 éves race a GNU libunwindben
A return‑to‑null összeomlások maradéka végül egy másik okra mutatott: a programokban akkor történt hibás viselkedés, amikor C++ kivételkezelés közben történt a „unwinding” (a kivétel felderítése és a megfelelő catch‑ág megtalálása). A bináris dinamikusan a GNU libunwind implementációját használta a libgcc helyett, és az unwind rutinok a stacken szintetizált ucontext_t struktúrát töltötték ki, majd egy belső assembly rutin (_Ux86_64_setcontext) vette át a regiszterállapot visszaállítását.
A csapat forráskód‑olvasás és core‑elemzések alapján egy nagyon szűk versenyablakot talált: _Ux86_64_setcontext egy utasítással előbb módosította %rsp‑t (ezáltal a szintetizált ucontext_t már nem volt az aktív verem része), majd a következő utasítások load‑olták az ucontext_t‑ból a visszatérési cím (%rip) értékét. Ha pontosan ebben a pillanatban megérkezett egy SIGUSR2 (amit Rockset gyakran használ a coarse_thread_cputime_clock miatt), a kernel a jel keretét a %rsp‑128 helyre építette — ez felülírhatta az épp olvasásra kijelölt ucontext_t memóriát. Ennek hatására az utasításmutató NULL‑ra, vagy más érvénytelen értékre változhatott, és az „a függvény visszatért NULL‑ra” tünet jelentkezett.
Ez a race‑ablak rendkívül szűk — gyakorlatilag egy utasítás szélességű, a modern CPU‑k miatt a valós idő nagy részében párszáz pikoszekundum lehet —, de Fermat‑szerű becsléssel a csapat megmutatta, hogy a gyakori kivételezés és a gyakori SIGUSR2 jelkézlések együtt elegendőek ahhoz, hogy a megfigyelt összeomlási arány előforduljon. Rockset esetében három tényező együtt növelte a kockázatot: magas kivétel‑dobási ráta (ingest backpressure mechanizmus részeként), gyakori SIGUSR2 jelzés a coarse_thread_cputime_clock miatt, és a jelkezelő által elfoglalt verem mennyiségének növekedése (timer_getoverrun meghívás hozzáadásával). Ezek együttesen csak mostanra hozták a hiba megfigyelhetőségét működésbe.
Mit tettek a javítás érdekében
Azonnali mérséklésként a csapat átváltott GNU libunwindről libgcc unwinderre, ami önmagában is előnyös volt lock‑csökkentés és skálázódás miatt. Emellett a mérnökök egy önálló reprodukálót és javítást upstreameltek a GNU libunwindbe, és ellenőrizték, hogy más unwinder implementációk nem mutatják‑e ugyanazt a problémát.
Tanulságok
A vizsgálat során szerzett legfontosabb tanulság az volt, hogy a magas minőségű, populációszintű adatgyűjtés nélkül a két különböző jelenség összekeveredett, és így a hibakeresés sikertelen volt. Miután automatizálták a core dumpok elemzését és megkülönböztették a klasztereket, a probléma struktúrája világossá vált: egy fizikai rossz host és egy nagyon régi, ritkán megnyíló race a libunwindben.
A történet rámutat arra is, hogy infrastruktúra‑rendszerek esetén mennyire fontos a mély instrumentáció, az automatizált vizsgálatok és olyan operációs eszközök építése, amelyek lehetővé teszik a rejtett, ritka hibák diagnosztizálását és kezelését.
— Nathan Bronson, Member of Technical Staff



