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, 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.
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.
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.
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.
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.
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.
Με μια καθαρή λίστα από κόμβους Kubernetes και χρονικές σημάνσεις, μπορέσαμε να εντοπίσουμε τα σφάλματα κατάρρευσης με κακή ευθυγράμμιση στοίβας πίσω σε έναν μόνο φυσικό host, τον οποίο ήταν εύκολο να βάλουμε σε λίστα αποκλεισμού.
Δεν καταφέραμε να αναπαραγάγουμε την αλλοίωση καταχωρητών σε αυτόν τον host σε ελεγχόμενο περιβάλλον, ακόμη και μετά από αρκετές εβδομάδες δοκιμών καταπόνησης. Μόλις όμως ο προβληματικός host τέθηκε εκτός υπηρεσίας, τα σφάλματα κατάρρευσης με κακή ευθυγράμμιση στοίβας εξαφανίστηκαν.
Η αφαίρεση του προβληματικού host δεν είναι μόνιμη λύση, με την έννοια ότι δεν αποτρέπει μια νέα εμφάνιση του ίδιου προβλήματος. Μπορούμε όμως να αλλάξουμε το λογισμικό ώστε, αν επανεμφανιστεί παρόμοιο ζήτημα, να εντοπίζεται και να αντιμετωπίζεται εύκολα. Βελτιώσαμε τον χειριστή μοιραίων σημάτων ώστε να περιλαμβάνει την κατάσταση των καταχωρητών, για να μπορούμε να εντοπίζουμε επανεμφάνιση μόνο από τα αρχεία καταγραφής (χωρίς να χρειάζεται core dump). Αλλάξαμε το control plane ώστε οι VM συνήθως να επαναχρησιμοποιούνται αντί να ανακυκλώνονται, κάτι που κάνει τον εντοπισμό προβληματικών κόμβων πολύ ευκολότερο στο δικό μας επίπεδο της στοίβας υποδομής. Ενημερώσαμε επίσης τα runbook μας (και τα νοητικά μοντέλα της ομάδας μας) ώστε να περιλαμβάνουν αυτήν την πιθανότητα.
Με τα σφάλματα κατάρρευσης του προβληματικού host διαχωρισμένα, τα υπόλοιπα core return-to-null έγιναν πολύ ευκολότερα στην κατανόηση. Νωρίτερα είχαμε αποκλείσει το unwinding εξαιρέσεων επειδή πιστεύαμε ότι είχαμε αντιπαραδείγματα: καταρρεύσεις σε μονοπάτια κώδικα όπου οι εξαιρέσεις σίγουρα δεν χρησιμοποιούνταν. Αλλά αυτά τα αντιπαραδείγματα προέρχονταν όλα από την ομάδα αλλοίωσης υλικού.
Όταν επανεξετάσαμε τα υπόλοιπα core έχοντας αυτό κατά νου, διαπιστώσαμε ότι αυτό το συμπέρασμα ήταν ακριβώς ανάποδο: οι καταρρεύσεις συνέβαιναν όλες κατά το unwinding εξαιρέσεων.
Όταν η C++ ρίχνει μια εξαίρεση, το runtime πρέπει να εντοπίσει ποιο μπλοκ catch θα τη δεχθεί, καθώς και ποιοι καταστροφείς ή χειριστές καθαρισμού πρέπει να εκτελεστούν κατά τη διαδρομή. Ο μεταγλωττιστής παράγει αυτά τα μεταδεδομένα, όμως η πραγματική αντιστοίχιση πραγματοποιείται δυναμικά κατά τον χρόνο εκτέλεσης.
Το unwinding εξαιρέσεων δεν εκτελείται στην πραγματικότητα από τη συνάρτηση που καλεί το throw, αλλά από βοηθητικές συναρτήσεις που καλούνται από τον παραγόμενο μεταγλωττισμένο κώδικα. Αυτές οι ρουτίνες runtime εξετάζουν τη στοίβα, ανακτούν μεταδεδομένα για τις συναρτήσεις που βρίσκονται στη στοίβα, αναζητούν δυναμικά χειριστές καθαρισμού και μπλοκ catch, και έπειτα μεταφέρουν τον έλεγχο σε μία από αυτές τις θέσεις. Η μεταφορά ελέγχου περιλαμβάνει το ξετύλιγμα όλων των ενδιάμεσων πλαισίων στοίβας (συμπεριλαμβανομένων των πλαισίων των βοηθητικών συναρτήσεων).
Λειτουργικά, αυτό μοιάζει πολύ περισσότερο με ένα longjmp ή με εναλλαγή fiber παρά με μια κανονική κλήση και επιστροφή. Οι callee-save καταχωρητές πρέπει να επαναφερθούν, όπως και οι καταχωρητές πλαισίου στοίβας %rbp και %rsp.
Το binary μας συνδέεται με δύο βιβλιοθήκες που περιέχουν υλοποιήσεις των συναρτήσεων που εκτελούν unwinding εξαιρέσεων C++: libgcc και GNU libunwind. Οι ορισμοί του GNU libunwind ήταν αυτοί που επέλεξε ο δυναμικός linker. Αυτό μας εξέπληξε. Περιμέναμε να επικρατήσει η υλοποίηση του libgcc λόγω των κανόνων versioning συμβόλων. Ωστόσο, η επιθεώρηση εκτελούμενων binary έδειξε ότι δεν ίσχυε αυτό.
Σε αυτό το σημείο η υπόθεση εργασίας μας άλλαξε, καθώς χαλαρώσαμε άλλη μία υπόθεση που είχαμε κάνει όταν πιστεύαμε ότι υπήρχε μόνο ένα σφάλμα.
Ίσως δεν βλέπαμε μια συνηθισμένη επιστροφή συνάρτησης σε NULL. Ίσως βλέπαμε μια μεταφορά unwind—ουσιαστικά μια επαναφορά καταχωρητών τύπου setcontext—όπου ο δείκτης εντολής προορισμού είχε γίνει NULL πριν μεταφερθεί ο έλεγχος. Με άλλα λόγια, λανθασμένα δεδομένα από τη βιβλιοθήκη unwind και όχι λανθασμένη θέση διεύθυνσης επιστροφής στη στοίβα.
Αυτό περιόρισε δραματικά το πρόβλημα. Είτε το GNU libunwind υπολόγιζε τη λάθος κατάσταση προορισμού είτε υπολόγιζε τη σωστή κατάσταση και κάτι την αλλοίωνε πριν μπορέσει να εφαρμοστεί.
Διαβάσαμε τον πηγαίο κώδικα του GNU libunwind και βρήκαμε ότι συνθέτει ένα ucontext_t στη στοίβα, συμπληρώνει την επιθυμητή κατάσταση καταχωρητών για το πλαίσιο του χειριστή καθαρισμού και έπειτα παραδίδει έναν δείκτη σε αυτή τη δομή σε μια εσωτερική ρουτίνα assembly: _Ux86_64_setcontext.
Σε αυτό το σημείο είχαμε όλα τα κομμάτια.
Το συντεθειμένο ucontext_t ζει σε ένα από τα πλαίσια στοίβας που ξετυλίγεται από το _Ux86_64_setcontext, κατά την εκτέλεση αυτής της συνάρτησης. Μήπως το _Ux86_64_setcontext διάβαζε από τη δομή αφού άλλαζε το %rsp, οπότε η δομή δεν ήταν πλέον μέρος της ενεργής στοίβας; Αυτό θα την έκανε ευάλωτη σε καταστροφή από την παράδοση ενός σήματος, όπως το συχνό μας SIGUSR2.
Η απάντηση ήταν ναι.
Ακολουθούν οι τελευταίες έξι εντολές του _Ux86_64_setcontext στην έκδοση του GNU libunwind που χρησιμοποιούσαμε, οι οποίες αποτελούνται κυρίως από εντολές mov που φορτώνουν από τη μνήμη σε έναν καταχωρητή προορισμού:
(Το %rdi δείχνει στο ucontext_t που έχει δεσμευτεί στη στοίβα, και οι μακροεντολές UC_MCONTEXT_* απλώς επεκτείνονται στη σταθερή μετατόπιση στην οποία αποθηκεύεται ένας συγκεκριμένος καταχωρητής.)
Η πρώτη εντολή είναι η αρχή του παραθύρου ανταγωνισμού. Ενημερώνει το %rsp ώστε να δείχνει στο νέο κάτω άκρο της ενεργής στοίβας. Μόλις συμβεί αυτό, η δομή στην οποία δείχνει το %rdi δεν είναι πλέον μέρος της ενεργής στοίβας (ή της red zone) και δεν είναι πλέον απαγορευμένη για τον πυρήνα.
Συνήθως αυτό δεν προκαλεί προβλήματα, αλλά αν ένα σήμα φτάσει ακριβώς τη σωστή (ή λάθος;) στιγμή, ο πυρήνας θα δημιουργήσει το πλαίσιο σήματος στο %rsp-128. Αυτό μπορεί να αντικαταστήσει τη μνήμη στην οποία δείχνει το %rdi.
Αν αυτό συμβεί πριν η επόμενη εντολή διαβάσει το UC_MCONTEXT_GREGS_RIP(%rdi), τότε ο δείκτης εντολών στον οποίον έχει γίνει επαναφορά μπορεί να αλλοιωθεί. Στις δικές μας καταρρεύσεις, γινόταν NULL.
Αυτό είναι το σφάλμα.
Αυτό το assembly επεξηγεί επίσης μία από τις παρατηρήσεις που μας μπέρδεψαν: γιατί η συνάρτηση X είχε NULL στη θέση της διεύθυνσης επιστροφής του προηγούμενου πλαισίου στοίβας.
Το setcontext γράφτηκε για να επαναφέρει όλους τους καταχωρητές, συμπεριλαμβανομένου του %rdi, άρα δεν μπορεί να χρησιμοποιήσει αυτόν τον καταχωρητή για να διαβάσει το UC_MCONTEXT_GREGS_RIP(%rdi) την τελική στιγμή της μεταφοράς ελέγχου. Αντίθετα, διαβάζει την τιμή νωρίτερα, την αποθηκεύει στη στοίβα, επαναφέρει μερικούς ακόμη καταχωρητές και έπειτα χρησιμοποιεί το retq για να διαβάσει την αποθηκευμένη τιμή και να μεταφέρει τον έλεγχο.
Αυτό που στα core έμοιαζε με «μια συνάρτηση επέστρεψε σε NULL» ήταν στην πραγματικότητα «ο μηχανισμός απο-στοίβαξης (unwinder) δημιούργησε συνθετικά έναν στόχο διεύθυνσης επιστροφής στη στοίβα, αλλά ο στόχος αυτός είχε ήδη αλλοιωθεί πριν ολοκληρωθεί η μεταφορά ελέγχου». Είχαμε υποθέσει ότι η αλλοίωση της θέσης της διεύθυνσης επιστροφής πρέπει να συμβαίνει επιτόπου, επειδή δεν γνωρίζαμε καμία περίπτωση όπου δεδομένα που μπορούν να αλλοιωθούν να γράφονται σκόπιμα σε αυτή τη θέση.
Αυτό που κάνει το σφάλμα να φαίνεται παράλογο είναι το πόσο στενό είναι αυτό το παράθυρο ανταγωνισμού. Σε αυτό το είδος συνθήκης ανταγωνισμού, το εξωτερικό συμβάν (το σήμα) πρέπει να συμβεί ανάμεσα σε δύο βήματα που εκτελεί ένα άλλο νήμα. Όσο πιο κοντά είναι αυτά τα βήματα μεταξύ τους, τόσο λιγότερο πιθανό είναι να συμβεί η συνθήκη ανταγωνισμού.
Σε αυτήν την περίπτωση, το ευάλωτο παράθυρο έχει κυριολεκτικά πλάτος μίας εντολής! Ένα σήμα πρέπει να παραδοθεί αφού έχει αλλάξει το %rsp, αλλά πριν η επόμενη εντολή φορτώσει το %rip. Πολλές απλές εντολές σαν αυτή μπορούν να εκτελεστούν ανά κύκλο σε μια σύγχρονη υπερβαθμωτή CPU με εκτέλεση εκτός σειράς, οπότε το παράθυρο ανταγωνισμού είναι περίπου εκατό πικοδευτερόλεπτα.
Όταν βρήκαμε αυτή τη συνθήκη ανταγωνισμού, η πρώτη μας αντίδραση ήταν ότι πρέπει να είναι υπερβολικά σπάνια για να εξηγεί το παρατηρούμενο ποσοστό καταρρεύσεων. Βλέπαμε πάνω από δώδεκα καταρρεύσεις return-to-null την ημέρα σε όλο τον στόλο. Θα μπορούσε πράγματι μια συνθήκη ανταγωνισμού μίας εντολής κατά τον καθαρισμό εξαιρέσεων να ευθύνεται γι’ αυτό;
Στραφήκαμε στην εκτίμηση τύπου Fermi. Αν το ευάλωτο παράθυρο είναι της τάξης των δευτερολέπτων και το SIGUSR2 φτάνει κάθε δευτερόλεπτα χρόνου CPU, τότε κάθε χειριστής καθαρισμού εξαίρεσης ή μπλοκ catch έχει πιθανότητα περίπου να χάσει τον αγώνα.
Το Rockset χρησιμοποιεί εξαιρέσεις ως μέρος του εσωτερικού μηχανισμού backpressure κατά την εισαγωγή δεδομένων. Ένας μόνο υπερφορτωμένος host μπορεί να ρίχνει στην τάξη των εξαιρέσεων ανά δευτερόλεπτο. Αυτό συνεπάγεται ότι ο μέσος χρόνος μεταξύ αστοχιών ενός host που χρησιμοποιεί backpressure είναι δευτερόλεπτα, δηλαδή ένα σφάλμα κατάρρευσης κάθε λίγες ώρες. Σε κλίμακα στόλου, αυτό είναι υπεραρκετό για να εξηγήσει την παρατηρούμενη συχνότητα καταρρεύσεων.
Το σφάλμα του GNU libunwind είναι παλιό—πάνω από 18 ετών, παρόν στην πρώτη έκδοση x86_64 που υποστήριζε unwinding εξαιρέσεων C++.
Γιατί λοιπόν εμφανίστηκε τώρα;
Το ποσοστό σφαλμάτων καταρρεύσεων είναι περίπου ανάλογο με το πόσες εξαιρέσεις ρίχνονται και πόσα σήματα παραδίδονται. Εξαρτάται επίσης από το πόση στοίβα καταναλώνει ο χειριστής σήματος.
Το Rockset είναι ασυνήθιστο και στους τρεις άξονες. Ρίχνουμε εξαιρέσεις με υψηλούς ρυθμούς ως μέρος του κανονικού ελέγχου υπερφόρτωσης, παραδίδουμε SIGUSR2 ασυνήθιστα συχνά λόγω του coarse_thread_cputime_clock και νωρίτερα φέτος κάναμε τον χειριστή SIGUSR2 να χρησιμοποιεί περισσότερη στοίβα προσθέτοντας μια κλήση στο timer_getoverrun, ώστε να μπορούμε να λογαριάζουμε συγχωνευμένα σήματα.
Αυτή η τελευταία αλλαγή φαίνεται πως ήταν σημαντική. Αν ο χειριστής χρησιμοποιεί αρκετά λίγη στοίβα, μπορεί να μη φτάσει και να μην αντικαταστήσει την παρωχημένη μνήμη ucontext_t. Πριν από αυτή την αλλαγή, δεν παρατηρούμε καθόλου αυτά τα σφάλματα κατάρρευσης. Μετά την αλλαγή, ο ρυθμός παρέμεινε χαμηλός μέχρι που αυξήσαμε το φορτίο για ορισμένες περιπτώσεις χρήσης που πίεζαν τον μηχανισμό backpressure.
Με άλλα λόγια, το σφάλμα του libunwind ήταν πάντα εκεί, αλλά το γινόμενο του ρυθμού εξαιρέσεων, του ρυθμού σημάτων και της χρήσης στοίβας από τον χειριστή είχε μόλις πρόσφατα περάσει το όριο όπου έγινε ορατό επιχειρησιακά.
Αυτός ο μηχανισμός επεξηγεί επίσης τη σύμπτωση ότι τόσο το σφάλμα υλικού όσο και το σφάλμα του libunwind προκαλούσαν σφάλματα κατάρρευσης κυρίως μέσα στη DocumentTree::updateDocument. Τα σφάλματα κατάρρευσης από το libunwind είχαν έντονη μεροληψία προς αυτή τη μέθοδο, επειδή είναι πάντα ενεργή τη στιγμή που ρίχνουμε μια εξαίρεση για να εφαρμόσουμε backpressure στην εισαγωγή δεδομένων. Επιλεγόταν επίσης έντονα για τα σφάλματα κατάρρευσης κακής ευθυγράμμισης του %rsp, επειδή ο προβληματικός κόμβος υλικού ανήκε σε SKU που χρησιμοποιούμε για μαζική εισαγωγή, η οποία περνά το μεγαλύτερο μέρος του χρόνου CPU σε αυτή τη μέθοδο.
Η άμεση μετρίασή μας ήταν να περάσουμε από το GNU libunwind στον unwinder του libgcc. Αυτό ήταν από μόνο του μια καλή ανταλλαγή: η υλοποίηση του libgcc έχει επωφεληθεί από πολλή δουλειά για τη μείωση της διένεξης κλειδωμάτων, κάτι που έχει σημασία κατά την κλιμάκωση σε μεγάλες VM.
Στείλαμε επίσης upstream έναν αυτοτελή αναπαραγωγό και μια διόρθωση(ανοίγει σε νέο παράθυρο) στο GNU libunwind, και επαληθεύσαμε ότι οι άλλοι unwinder δεν έχουν παρόμοιο ζήτημα.
Αυτή η διαδρομή αποσφαλμάτωσης μάς έμαθε πολλά για τις συγκεκριμένες λεπτομέρειες της δυναμικής σύνδεσης, των μεταδεδομένων DWARF unwind, της παράδοσης σημάτων στο Linux, του System V ABI και του μηχανισμού εξαιρέσεων της C++. Αλλά το κύριο μάθημα ήταν απλούστερο από όλα αυτά.
Το πιο σημαντικό βήμα δεν ήταν η έξυπνη ανάγνωση assembly ή η βαθιά γνώση των λεπτομερειών. Ήταν η δημιουργία ενός υψηλής ποιότητας συνόλου δεδομένων. Χωρίς αυτό το σύνολο δεδομένων, αναμειγνύαμε δύο διακριτά φαινόμενα σε μία ιστορία και προσπαθούσαμε να βγάλουμε άκρη μόνο με συλλογισμό. Μόλις αποκτήσαμε ακριβή και πλήρη δεδομένα πληθυσμού, η δομή του προβλήματος έγινε προφανής: ο ένας πληθυσμός καταρρεύσεων ανήκε σε έναν προβληματικό host και ο άλλος σε μια συνθήκη ανταγωνισμού στο libunwind. Όταν τα δεδομένα έγιναν καλύτερα, η αποσφαλμάτωση έγινε ευκολότερη.
Για συστήματα υποδομής όπως το Rockset, αυτό έχει μεγάλη σημασία. Αυτή η έρευνα ενίσχυσε τη δέσμευσή μας στη βαθιά παρατηρησιμότητα, στις αυτοματοποιημένες διερευνήσεις και στις συνεχείς βελτιώσεις των επιχειρησιακών εργαλείων μας. Η αξιοπιστία δεν αφορά μόνο τη διόρθωση σφαλμάτων αφού συμβούν, αφορά τη δημιουργία των δεδομένων, των ροών εργασίας και των δεξιοτήτων που μετατρέπουν αδύνατα προβλήματα σε διαγνώσιμα και επιλύσιμα.
Συντάκτες
By Nathan Bronson, Member of Technical Staff


