Անցնել հիմնական բովանդակությանը
OpenAI

30 հունիսի, 2026 թ.

Ճարտարագիտություն

Core dump epidemiology: fixing an 18-year-old bug

Using population-level analysis to debug tricky crashes in our data infrastructure.

Բեռնվում է…

By Nathan Bronson, Member of Technical Staff

OpenAI’s models and agents increasingly rely on scalable data infrastructure in order to search for relevant data at inference time: when the models are thinking about your question. Some of these services are written in C++, whose low-level control of the system lets us maximize performance and minimize memory usage. Those efficiency benefits are important as we scale, but C++’s lack of memory safety means that bugs can cause crashes by writing to incorrect or non-existent memory addresses.

A few months ago we observed some crashes from inside the Rockset service, a bespoke part of our ChatGPT data infrastructure which is key to many data plugins and to searching over conversations. In each of these crashes, a normal C++ function seemed to finish and then return to a bogus address, causing the kernel to stop the program because the instruction pointer no longer pointed at code. Sometimes the return address slot in the stack frame was NULL. Sometimes the stack pointer CPU register itself seemed to be off by 8 bytes, as if %rsp had somehow been decremented in the middle of normal execution. In both cases the crash happened on return.

These are not normal failure modes for application code. A stray write that lands only on a saved return address is possible, but extremely unlikely. A bug that misaligns %rsp by 8 without involving inline assembly, setcontext, or longjmp (none of which we use) is even stranger, because compiled code only adjusts that register directly in the function prologue and epilogue. Every hypothesis we (or ChatGPT) could think of had strong evidence against it, so the bug seemed impossible.

What we assumed was one problem eventually turned out to be two unrelated bugs, coincidentally discovered at the same time. First, silent hardware corruption on one Azure host, where the CPU just didn’t do math correctly. Second, an 18-year-old race condition in GNU libunwind, an unnoticed bug in a widely used open source library.

This post is the story of how we identified and fixed seemingly inexplicable crashes by thinking like an epidemiologist and building a high-quality data set about the entire population of crashes.

First debugging attempt: carefully examining a few core dumps

First, let’s go deeper on Rockset. It’s a cloud-native data system for search and real-time analytics that we use for many internal use cases at OpenAI, such as sync connectors (Rockset was acquired by OpenAI in 2024). Streaming updates are used to maintain an up-to-date index of a workspace’s knowledge base so that ChatGPT can search for relevant information when answering questions or performing actions.

Rockset’s execution layer is written in C++. The C++ language provides low-level access to the CPU, which is good for performance and efficiency, but it means that application bugs can lead to invalid memory accesses and segfaults. To help track these down we use folly’s fatal signal handler to log a stack trace when a crash happens, and we upload the corresponding core dumps (a snapshot of the state of the program when it crashed) to Azure blob storage for later analysis. All of Rockset’s query processing leaves are replicated, which minimizes the client impact of a crash. However, each segfault corresponds to a bug that needs to be fixed to meet our reliability and quality goals.

Our initial approach was to treat these cores like a conventional debugging problem: inspect a few core dumps very closely, form hypotheses, and rule them out one by one.

Most of the crashes occurred in a method called DocumentTree::updateDocument. In these crashes it appeared that updateDocument had called some unknown function X, the stack had become corrupted while X was active, then X had returned to an address that wasn’t executable code. In some cases X’s just-popped frame looked valid except that its saved return address was NULL. In other cases the stack pointer itself looked wrong, but the next valid frame still seemed to be updateDocument.

We didn’t know when the stack was getting corrupted, which left a huge search space. updateDocument is a large method that undergoes a lot of inlining, so the number of candidates for X was overwhelming.

Was this a bug in our C++ code? A compiler or linkage issue? A problem in one of our runtime libraries? A Linux kernel bug around signal delivery or context switching? Something even rarer? If this was a stray write, why wasn’t it caught by our ASAN staging environment?

We tried to use our application-level logs to identify all occurrences of the problem, but stack-corruption bugs are hard to classify from logs alone because the logged stack traces are themselves corrupted or missing. We weren’t able to construct a log query that didn’t have both false positives and false negatives. We manually inspected more cores and found some additional examples, but that process was too labor-intensive to give us a trustworthy data set.

At this stage of the investigation, we (incorrectly) ruled out a hardware bug, because we saw crashes across multiple regions and multiple hardware types, so we were still looking for software-only causes. For a few days, we went super-deep on a single misaligned-%rsp crash, reconstructing the pre-crash history using stack and register contents. This produced some possible clues, but because we didn’t let go of our initial conclusions that all of the bugs had the same cause, this didn’t get us unstuck.

Clues from the stack

Before getting to the turning point of our investigation, it’s important to explain what kind of information we were extracting from the core files.

Rockset is compiled with -fno-omit-frame-pointer, so the active stack frame is always reachable through %rbp, and callers form a linked list of frame pointers.

On Linux x86_64, the AMD64 System V ABI also reserves 128 bytes below %rsp as the red zone. That region is available to userspace code and, importantly, the kernel promises not to clobber it when it delivers a signal, as part of the ABI contract.

The red zone was central to our debugging of a post-return crash, because it preserves some information from before the return. When a SIGSEGV is triggered, folly’s fatal signal handler runs on the crashing thread’s stack. Stack frames that are no longer active (because their function has returned) will get clobbered by the signal handler, except for the last 128 bytes. That’s why we can say things like “X’s just-popped stack frame looked valid, except for a NULL return address.” The red zone preserves some of the inactive frames, or sometimes just the tail of one inactive frame.

Կոռումպացված stack frame-ների stack դիագրամ, որը կարող է վերագրել վերադարձի հասցեները և առաջացնել խափանումներ։

We found one misaligned-stack crash in which all of the functions involved were very small. That let us see that %rsp had become misaligned during execution of a relatively simple function, and that more calls had succeeded afterward. The program only crashed when the active function finally tried to return. None of those code paths used exceptions, inline assembly, setcontext, or longjmp, so if the stack pointer truly changed in the way the core suggested, no plausible bug in userspace code explained the issue.

That pushed us toward the kernel.

Rockset uses signals more aggressively than most programs. Query execution is broken into many lightweight tasks that exchange data. This is important for handling high-QPS workloads efficiently, but it makes per-query CPU accounting awkward as work for many queries is multiplexed onto the same thread pool.

Our solution is something we call coarse_thread_cputime_clock, which approximates clock_gettime(CLOCK_THREAD_CPUTIME_ID, ...) cheaply enough to sample at every task boundary. The timer_create API can be used to schedule a periodic signal delivery based on several notions of the passage of time, including the accumulation of CPU time. We schedule a signal (SIGUSR2) to be delivered every few milliseconds of CPU time, at which point the signal handler updates a thread-local value. Even though many tasks don’t see the coarse clock advance while they are executing, summing all of the deltas produces an unbiased estimate of the actual CPU time for a query.

Because we deliver signals so often, a rare kernel bug around context switching or signal delivery seemed plausible. We spent time reading bug reports, kernel source code, and the Azure-specific kernel patches. We tried stress tests. We weren’t able to find anything that seemed related.

At that point we decided to step back and try a different approach.

Doctor or epidemiologist?

There are two broad ways to debug a problem like this.

One is to act like a doctor of sorts: focus on one patient, run lots of tests, and try to diagnose a single case from detailed evidence.

The other is to act more like an epidemiologist: look at the entire population and ask whether there are patterns that a single case cannot reveal. Did the bug start at a specific release? Does it correlate with one hardware SKU (the specific CPU and server model), one region, or one kernel version? Are there multiple distinct clusters hiding inside what looks like one syndrome?

We had mostly been in doctor mode. The key shift was deciding that we needed to gather high-quality population data.

Cleaning the data

Our previous attempts to automatically find all of the instances of the problem failed because we were trying to use text searches over the logs. The core dumps themselves have a lot more information, but looking at them manually didn’t scale. We decided to invest the effort to build a pipeline that could automatically analyze the core dumps.

We had ChatGPT write a script that downloaded a prefix of each core file, extracted the registers, filtered known false positives using the logs, and automatically labeled the crash as return-to-null, misaligned-stack, or other. Then we ran that script in parallel over every production Rockset core dump from the previous year.

This was the turning point.

Once we had a clean data set, correlations appeared immediately. What we had been treating as one weird bug was actually two separate crash populations.

The return-to-null cores were spread across many clusters and geographic regions. Their frequency had increased recently, but there was no crisp start date and no clean infrastructure boundary.

The misaligned-stack crashes looked completely different. They all came from one region, had a clear start date, and never happened on nodes that had been running for a long time. Even though they involved multiple Azure VMs (virtual machines hosted in the cloud), the pattern looked like one physical machine with bad hardware causing problems for whichever VM happened to land on it.

Կլաստերներով խափանման հաճախականության կետային գծապատկեր՝ ժամանակի ընթացքում, որը ցույց է տալիս, որ խափանումների մեծ մասը կենտրոնացած է 2, 3 և 6 կլաստերներում, իսկ ժամանակահատվածի վերջում 1-ին կլաստերում նկատվում է կտրուկ աճ։

That was the moment we realized we had been mentally conflating two bugs. Because we had been mixing counterexamples from both bugs, we couldn’t find a single coherent explanation.

Սխալ #1․ անսարք հանգույցը

Մաքուր Kubernetes հանգույցների ցուցակի և ժամադրոշմների շնորհիվ մենք կարողացանք սխալ հավասարեցմամբ խափանումները հետագծել մինչև մեկ ֆիզիկական հանգույց, որը հեշտությամբ կարելի էր դնել արգելափակման ցուցակում։

Մենք չկարողացանք վերահսկվող միջավայրում վերարտադրել այդ հանգույցում ռեգիստրների վնասումը, նույնիսկ երկարատև սթրես-թեստավորումից հետո։ Բայց երբ խնդրահարույց հանգույցը դուրս բերվեց շահագործումից, stack-ի սխալ հավասարեցմամբ խափանումներն ամբողջությամբ դադարեցին։

Վատ հոսթը հեռացնելը մշտական լուծում չէ այն իմաստով, որ դա չի կանխում նույն խնդրի կրկնությունը։ Սակայն մենք կարող ենք փոխել ծրագրակազմը այնպես, որ նման դեպքի կրկնության ժամանակ այն հեշտությամբ հայտնաբերվի և մշակվի։ Մենք բարելավեցինք մեր ճակատագրական ազդանշանի մշակիչը՝ ավելացնելով ռեգիստրների վիճակի գրանցում, որպեսզի կրկնությունը հնարավոր լինի հայտնաբերել միայն լոգերից (առանց core dump-ի անհրաժեշտության)։ Մենք նաև փոխեցինք կառավարման հարթակը, որպեսզի VM-ները սովորաբար վերօգտագործվեն, այլ ոչ թե վերաստեղծվեն, ինչը հեշտացնում է վատ հանգույցների հայտնաբերումը մեր ենթակառուցվածքային ստեկի մակարդակում։ Բացի այդ, մենք թարմացրինք մեր գործառնական ընթացակարգերը (և մեր թիմի մտավոր մոդելները)՝ այս հնարավորությունը հաշվի առնելու համար։

Երբ անսարք հանգույցի հետ կապված խափանումները առանձնացվեցին, մնացած return-to-null core dump-երը վերլուծելը զգալիորեն ավելի հեշտ դարձավ։ Ավելի վաղ մենք բացառել էինք բացառությունների հետադարձ բացման վարկածը, որովհետև կարծում էինք, թե ունենք հակափաստարկներ՝ խափանումներ այնպիսի կոդային ուղիներում, որտեղ բացառություններ հաստատ չէին օգտագործվում։ Սակայն այդ բոլոր հակափաստարկները իրականում պատկանում էին սարքակազմի վնասմամբ պայմանավորված խափանումների կլաստերին։

Երբ այդ նկատառումով վերանայեցինք մնացած core-երը, պարզվեց, որ եզրակացությունը ճիշտ հակառակն էր․ բոլոր խափանումները տեղի էին ունենում բացառությունների հետադարձ բացման ընթացքում։

Բացառությունների մշակումը դինամիկ կառավարման փոխանցում է

Երբ C++-ը բացառություն է նետում, կատարման միջավայրը պետք է պարզի, թե որ catch բլոկը պետք է ստանա այն և որ դեստրուկտորները կամ մաքրման մշակիչները պետք է գործարկվեն։ Կոմպիլյատորը այդ նպատակով գեներացնում է համապատասխան մետատվյալներ, սակայն իրական համապատասխանեցումը կատարվում է դինամիկ կատարման միջավայրում։

Բացառությունների հետադարձ բացումը իրականում չի իրականացվում այն ֆունկցիայի կողմից, որը կանչում է throw այլ կատարվում է այն օժանդակ ֆունկցիաների կողմից, որոնք կանչվում են ստացված կոմպիլացված կոդի կողմից։ Այդ կատարման միջավայրի ռուտինները ուսումնասիրում են stack-ը, ստանում stack-ում հայտնաբերված ֆունկցիաների վերաբերյալ մետատվյալներ, դինամիկ կերպով որոնում են մաքրման մշակիչներ և catch բլոկներ, ապա կառավարումը փոխանցում դրանցից մեկին։ Կառավարման փոխանցումը ներառում է նաև բոլոր միջանկյալ stack frame-երի հետադարձ բացումը, (այդ թվում՝ օժանդակ ֆունկցիաներինը)։

Գործառնական տեսանկյունից սա շատ ավելի մոտ է longjmp-ի ֆայբերի փոխարկմանը, քան սովորական ֆունկցիայի կանչին և վերադարձին։ Պետք է վերականգնվեն callee-save ռեգիստրները, ինչպես նաև stack frame-ի %rbp և %rsp ռեգիստրները։

Մեր բինար ֆայլը կապակցված է երկու գրադարանների հետ, որոնք պարունակում են C++-ի բացառությունների հետադարձ բացում իրականացնող ֆունկցիաների իրականացումներ՝ libgcc և GNU libunwind։ Դինամիկ կապակցիչը ընտրել էր GNU libunwind-ի սահմանումները։ Դա մեզ զարմացրեց․ սպասում էինք, որ symbol-ների տարբերակավորման կանոնների պատճառով կհաղթի libgcc-ի իրականացումը, սակայն աշխատող բինար ֆայլի ուսումնասիրությունը ցույց տվեց, որ այդպես չէ։

Վերջին ենթադրության չեղարկումը

Այս պահին մեր աշխատանքային վարկածը փոխվեց, քանի որ թուլացրինք ևս մեկ ենթադրություն, որն արել էինք՝ կարծելով, թե կա միայն մեկ սխալ։

Գուցե մենք չէինք տեսնում սովորական ֆունկցիայի վերադարձ դեպի NULL։ Գուցե տեսնում էինք հետադարձ բացման փոխանցում՝ ըստ էության setcontext-ի ոճի ռեգիստրների վերականգնում, որտեղ նպատակային հրահանգի ցուցիչը NULL էր դարձել մինչև կառավարման փոխանցումը։ Այլ կերպ ասած՝ սխալ տվյալներ unwind գրադարանից, այլ ոչ թե stack-ում սխալ վերադարձի հասցեի դաշտ։

Սա կտրուկ նեղացրեց խնդրի շրջանակը։ Կամ GNU libunwind-ը սխալ էր հաշվարկում նպատակային վիճակը, կամ այն ճիշտ էր հաշվարկում, բայց ինչ-որ բան այն վնասում էր նախքան այդ վիճակի կիրառումը։

Կարդացինք GNU libunwind-ի սկզբնական կոդը և գտանք, որ այն stack-ում սինթեզում է ucontext_t, լրացնում մաքրման մշակիչի շրջանակի ցանկալի ռեգիստրների ցանկալի վիճակը, ապա այդ struct-ի ցուցիչը փոխանցում ներքին ասեմբլի ռուտինի՝ _Ux86_64_setcontext-ին։

Այդ փուլում մենք ունեինք բոլոր բաղադրիչները։

Սինթեզված ucontext_t-ը գտնվում է այն stack շրջանակներից մեկում, որը հետադարձ բացվում է _Ux86_64_setcontext-ի կողմից իր կատարման ընթացքում։ Արդյո՞ք _Ux86_64_setcontext-ը struct-ից կարդում էր այն բանից հետո, երբ փոխել էր %rsp-ն, այսինքն այն պահին struct-ն այլևս ակտիվ stack-ի մաս չէր։ Դա այն խոցելի կդարձներ signal delivery-ի կողմից վերագրվելու համար, օրինակ՝ մեր հաճախակի SIGUSR2-ի դեպքում։

Սխալ #2․ libunwind-ի սխալը

Պատասխանը այո էր։

Ահա մեր օգտագործած GNU libunwind տարբերակում _Ux86_64_setcontext-ի վերջին վեց հրահանգները, որոնք հիմնականում mov հրահանգներ են,որոնք հիշողությունից արժեք են բեռնում նպատակային ռեգիստրների մեջ․

Պարզ տեքստ

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-ն ցույց է տալիս stack-ի վրա տեղադրված ucontext_t-ին, իսկ UC_MCONTEXT_* մակրոնսերը պարզապես ընդլայնվում են որպես ֆիքսված offset, որտեղ պահվում է կոնկրետ ռեգիստրի արժեքը։)

Առաջին հրահանգը մրցավազքի պատուհանի սկիզբն է։ Այն թարմացնում է %rsp-ն այնպես, որ այն ցույց տա ակտիվ stack-ի նոր ստորին սահմանը։ Հենց դա տեղի է ունենում, %rdi-ով մատնանշված struct-ն այլևս ակտիվ stack-ի (կամ red zone-ի) մաս չէ և այլևս արգելված տարածք չէ կեռնելի համար։

Սովորաբար սա խնդիր չի առաջացնում, բայց եթե սիգնալը գալիս է ճիշտ ( կամ սխալ) պահին, կեռնելը սիգնալ շրջանակը կառուցում է %rsp-128 հասցեում։ Դա կարող է վերագրել %rdi-ով մատնանշված հիշողությունը։

Եթե դա տեղի է ունենում մինչև հաջորդ հրահանգը կարդա UC_MCONTEXT_GREGS_RIP(%rdi), ապա վերականգնված հրահանգի ցուցիչը կարող է վնասվել։ Մեր խափանումներում այն դառնում էր NULL։

Ահա սխալը։

Ինչու core-երը դիտվում էին որպես սովորական սխալ վերադարձներ

Այս ասեմբլին նաև բացատրում է մեզ շփոթեցնող դիտարկումներից մեկը՝ ինչու էր X ֆունկցիան նախորդ stack frame-ի վերադարձի հասցեի դաշտում NULL։

setcontext-ը գրված էր բոլոր ռեգիստրները, այդ թվում՝ %rdi-ն վերականգնելու համար, ուստի չի կարող այդ ռեգիստրը օգտագործել վերահսկման փոխանցման վերջին պահին UC_MCONTEXT_GREGS_RIP(%rdi) կարդալու համար։ Փոխարենը այն արժեքը կարդում է ավելի վաղ փուլում, պահում stack-ում, վերականգնում ևս մի քանի ռեգիստր, ապա retq-ով կարդում է պահված արժեքը և փոխանցում կառավարումը։

Այն, ինչ core-երում երևում էր որպես «ֆունկցիան վերադարձել է NULL-ի», իրականում հետևյալն էր․ «unwinder-ը stack-ում սինթեզել էր նպատակային վերադարձի հասցե, բայց այդ նպատակային հասցեն վնասվել էր մինչև փոխանցման ավարտը»։ Մենք ենթադրել էինք, որ վերադարձի հասցեի դաշտի վնասումը պետք է տեղի ունենա տեղում, քանի որ չգիտեինք որևէ տեղ, որտեղ վերադարձի հասցեի դաշտում դիտավորյալ գրվում էր (վնասման ենթակա) տվյալ։

Մեկ հրահանգի չափով մրցավազքի պատուհան

Այս սխալն անհավանական է թվում այն պատճառով, որ մրցավազքի պայմանի առաջացման ժամանակային պատուհանը չափազանց նեղ է։ Այսպիսի մրցավազքի պայմանի դեպքում արտաքին իրադարձությունը (ազդանշանը) պետք է տեղի ունենա մեկ այլ կատարման թելի կատարած երկու գործողությունների միջև։ Որքան այդ գործողությունները ժամանակային առումով ավելի մոտ են միմյանց, այնքան ավելի փոքր է մրցավազքի պայմանի առաջացման հավանականությունը։

Այս դեպքում խոցելի ժամանակային պատուհանն ընդամենը մեկ հրահանգի տևողություն ունի։ Ազդանշանը պետք է հասնի այն բանից հետո, երբ %rsp-ն փոխվել է, բայց մինչև հաջորդ հրահանգը բեռնի %rip-ը։ Ժամանակակից սուպերսկալյար, հրահանգների ոչ հերթական կատարմամբ (out-of-order) պրոցեսորում մի քանի պարզ հրահանգ կարող է կատարվել մեկ ցիկլում, ուստի մրցավազքի պայմանի ժամանակային պատուհանը մոտավորապես հարյուր պիկովայրկյան է։

Երբ հայտնաբերեցինք այս մրցավազքի պայմանը, մեր առաջին արձագանքն այն էր, որ այն պետք է չափազանց հազվադեպ լինի՝ մեր դիտարկած խափանումների հաճախականությունը բացատրելու համար։ Ամբողջ ենթակառուցվածքում մենք օրական դիտարկում էինք ավելի քան մեկ տասնյակ return-to-null խափանում։ Բացառության մշակման ավարտի ընթացքում մեկ հրահանգի տևողությամբ մրցավազքի պայմանը կարո՞ղ էր իսկապես այդ ամենի պատճառը լինել։

Դիմեցինք Ֆերմիի գնահատմանը։ Եթե խոցելի ժամանակային պատուհանը 101010^{-10}վայրկյանի կարգի է, իսկ SIGUSR2 ազդանշանը հասնում է պրոցեսորի ժամանակի յուրաքանչյուր 10210^{-2} վայրկյանը մեկ, ապա բացառությունների մշակման յուրաքանչյուր ավարտական մշակիչը կամ catch block-ն ունի մոտավորապես 10810^{-8} հավանականություն՝ պարտվելու մրցավազքում։

Rockset-ը բացառություններն օգտագործում է որպես տվյալների ընդունման հետճնշման ներքին մեխանիզմի մաս։ Գերբեռնված մեկ հանգույցը կարող է վայրկյանում առաջացնել մոտ 10410^{4} բացառություն։ Սա նշանակում է, որ հետճնշման մեխանիզմն օգտագործող հանգույցի խափանումների միջև միջին ժամանակը կազմում է 10410^{4} վայրկյան, այսինքն՝ մեկ խափանում մի քանի ժամը մեկ։ Ամբողջ ենթակառուցվածքի մասշտաբով դա լիովին բավարար է՝ բացատրելու մեր դիտարկած խափանումների հաճախականությունը։

Ինչո՞ւ libunwind-ի սխալն ի հայտ եկավ հենց հիմա։

GNU libunwind-ի այս սխալը հին է, այն գոյություն ունի ավելի քան 18 տարի։ Այն առկա էր արդեն x86_64-ի առաջին տարբերակում, որն աջակցում էր C++-ի բացառությունների հետադարձ բացմանը։

Ուրեմն ինչո՞ւ այն հայտնվեց հիմա։

Խափանումների հաճախականությունը մոտավորապես համեմատական է բացառությունների առաջացման հաճախականությանը և ազդանշանների առաքման հաճախականությանը։ Այն նաև կախված է նրանից, թե ազդանշանի մշակիչը որքան stack հիշողություն է օգտագործում։

Մենք բարձր հաճախականությամբ բացառություններ ենք առաջացնում՝ որպես գերբեռնվածության կառավարման սովորական մեխանիզմի մաս SIGUSR2 ազդանշանն առաքում ենք անսովոր հաճախ՝ coarse_thread_cputime_clock-ի պատճառով, իսկ այս տարվա սկզբին SIGUSR2 -ի կանչն ավելացնելով՝ timer_getoverrun ազդանշանի մշակիչը սկսեց ավելի շատ stack հիշողություն օգտագործել, որպեսզի կարողանայինք հաշվառել միավորված ազդանշանները։

Վերջին փոփոխությունն, ըստ երևույթին, կարևոր էր։ Եթե մշակիչը բավական քիչ stack է օգտագործում, այն կարող է չհասնել և չվերագրել հնացած ucontext_t հիշողությունը։ Մինչ այդ փոփոխությունը մենք ընդհանրապես չէինք դիտարկում նման խափանումներ։ Փոփոխությունից հետո դրանց հաճախականությունը շարունակում էր ցածր մնալ, մինչև որոշ կիրառման դեպքերի համար ավելացրինք բեռնվածությունը, ինչը մեծ ճնշում գործադրեց հետճնշման մեխանիզմի վրա։

Այլ կերպ ասած՝ libunwind-ի սխալը միշտ գոյություն է ունեցել, սակայն բացառությունների հաճախականության, ազդանշանների հաճախականության և մշակիչի կողմից օգտագործվող stack հիշողության համակցված ազդեցությունը միայն վերջերս հատեց այն շեմը, որից հետո այդ սխալը դարձավ գործառնական միջավայրում նկատելի։

Այս մեխանիզմը նաև բացատրում է այն զուգադիպությունը, որ թե՛ սարքակազմի և թե՛ libunwind-ի սխալների հետևանքով առաջացած խափանումները հիմնականում տեղի էին ունենում DocumentTree::updateDocument-ի ներսում։ Libunwind-պատճառով առաջացած խափանումները զգալիորեն կենտրոնացած էին հենց այս մեթոդում, քանի որ տվյալների ընդունման հետճնշման մեխանիզմը կիրառելու համար բացառությունը միշտ առաջանում է այն պահին, երբ այդ մեթոդը դեռ ակտիվ է։ %rsp-ի ի սխալ հավասարեցմամբ խափանումների դեպքում նույնպես գերակշռում էր նույն մեթոդը, քանի որ անսարք սարքակազմով հանգույցը պատկանում էր այն SKU-ին, որը մենք օգտագործում ենք տվյալների զանգվածային ընդունման համար, իսկ դրա պրոցեսորային ժամանակի մեծ մասն անցնում է հենց այդ մեթոդի կատարման վրա։

Մեր անմիջական լուծումը GNU libunwind-ից libgcc-ի unwinder-ին անցնելն էր։ Սա ինքնին հաջող ընտրություն էր, քանի որ libgcc-ի իրականացումն անցել է մեծածավալ օպտիմալացման աշխատանքների միջով՝ կողպեքների մրցակցությունը նվազեցնելու նպատակով, ինչը կարևոր նշանակություն ունի մեծ ծավալի VM-ների մասշտաբավորման դեպքում։

Մենք նաև GNU libunwind նախագծին փոխանցեցինք խնդիրը վերարտադրող ինքնաբավ օրինակ և շտկում(բացվում է նոր պատուհանում), և համոզվեցինք, որ մյուս unwinder-ները՝ հետադարձ բացման մեխանիզմները նման խնդիր չունեն։

Պոպուլյացիոն մակարդակի ախտորոշման ուժը

Այս վրիպազերծման ընթացքը մեզ շատ բան սովորեցրեց դինամիկ կապակցման, DWARF-ի հետադարձ բացման մետատվյալների, Linux-ում ազդանշանների առաքման, System V ABI-ի և C++ բացառությունների մշակման մեխանիզմների մասին։ Սակայն հիմնական դասը դրանցից բոլորից էլ ավելի պարզ էր։

Ամենակարևոր քայլը ոչ թե ասեմբլիի խելացի վերլուծությունը կամ մանրամասների խորքային իմացությունն էր։ Դա բարձրորակ տվյալակազմ կառուցելն էր։ Առանց այդ տվյալակազմի մենք երկու տարբեր երևույթ միավորում էինք մեկ պատմության մեջ և փորձում տրամաբանությամբ դուրս գալ ստեղծված շփոթությունից։ Երբ ստացանք ճշգրիտ և ամբողջական պոպուլյացիոն տվյալներ, խնդրի կառուցվածքը ակնհայտ դարձավ․ խափանումների մի պոպուլյացիան կապված էր անսարք հանգույցի հետ, իսկ մյուսը՝ libunwind-ում առկա մրցավազքի պայմանի։ Տվյալների որակի բարելավմանը զուգընթաց վրիպազերծումն էլ դարձավ ավելի հեշտ։

Rockset-ի նման ենթակառուցվածքային համակարգերի համար դա շատ կարևոր է։ Այս հետաքննությունը ամրապնդեց մեր հանձնառությունը խորքային գործիքավորման, ավտոմատացված հետաքննությունների և մեր գգործառնական գործիքակազմի շարունակական կատարելագործման հանդեպ։ Հուսալիությունը միայն սխալներն ի հայտ գալուց հետո դրանք շտկելը չէ․ այն նաև այնպիսի տվյալների, աշխատանքային գործընթացների և հմտությունների ձևավորումն է, որոնք անհնար թվացող խնդիրները դարձնում են ախտորոշելի և լուծելի։

Հեղինակներ

By Nathan Bronson, Member of Technical Staff