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, 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.
Nhờ có trong tay một danh sách sạch sẽ gồm các nút mạng Kubernetes và các mốc thời gian cụ thể, chúng tôi đã có thể truy vết các cú sập do lệch ngăn xếp (misaligned-stack) về lại đúng một máy chủ vật lý duy nhất, từ đó dễ dàng đưa máy chủ này vào danh sách đen (denylist) để chặn hoạt động.
Chúng tôi đã không thể tái hiện lại lỗi hỏng dữ liệu thanh ghi (register corruption) trên máy chủ đó trong một môi trường được kiểm soát, ngay cả sau vài tuần thử nghiệm áp lực cao (stress test). Tuy nhiên, một khi máy chủ gặp sự cố bị loại bỏ khỏi hệ thống vận hành, các cú sập do lệch ngăn xếp (misaligned-stack) cũng hoàn toàn biến mất.
iệc loại bỏ máy chủ bị lỗi không phải là một giải pháp lâu dài, theo nghĩa là nó không thể ngăn chặn vấn đề tương tự tái diễn trong tương lai. Tuy nhiên, chúng tôi có thể chỉnh sửa phần mềm để nếu một sự cố tương tự xảy ra, hệ thống sẽ dễ dàng phát hiện và xử lý nó.Chúng tôi cải thiện trình xử lý tín hiệu nghiêm trọng để bao gồm trạng thái thanh ghi, nhờ đó có thể phát hiện tái diễn chỉ từ log (không cần core dump). Chúng tôi thay đổi control plane để VM thường được tái sử dụng thay vì tái tạo, giúp việc phát hiện nút lỗi dễ hơn nhiều ở tầng hạ tầng của chúng tôi. Chúng tôi cũng cập nhật runbook (và mô hình tư duy của nhóm) để bao gồm khả năng này.
Khi đã tách riêng các sự cố do host lỗi, các core return-to-null còn lại trở nên dễ suy luận hơn nhiều. Trước đó, chúng tôi từng loại trừ khả năng do cơ chế gỡ rối ngoại lệ (exception unwinding) bởi vì chúng tôi nghĩ mình đã có những phản ví dụ: các cú sập xảy ra ở những nhánh mã nguồn chắc chắn không hề sử dụng ngoại lệ. Thế nhưng, những phản ví dụ đó hóa ra đều nằm trong nhóm sập nguồn do lỗi hỏng phần cứng.
Khi xem lại các core còn lại với điều đó trong đầu, chúng tôi thấy kết luận này hoàn toàn ngược lại: mọi sự cố đều xảy ra trong lúc gỡ rối exception.
Khi C++ ném một exception, runtime phải tìm ra khối lệnh bắt (catch block)nào sẽ tiếp nhận ngoại lệ đó, đồng thời xác định những hàm hủy (destructor) hoặc trình dọn dẹp nào cần được thực thi trên suốt lộ trình quay lui. Trình biên dịch xuất ra phần dữ liệu đặc tả (metadata) này, nhưng việc so khớp thực tế lại diễn ra một cách động tại runtime.
Gỡ rối exception thực ra không được thực hiện bởi hàm gọi throw, mà bởi các hàm trợ giúp do mã đã biên dịch kết quả gọi. Các routine runtime đó kiểm tra stack, lấy dữ liệu đặc tả về các hàm tìm thấy trên stack, tìm một cách động các trình xử lý dọn dẹp và khối bắt, rồi chuyển quyền điều khiển đến một trong các vị trí đó. Chuyển quyền điều khiển bao gồm gỡ rối tất cả các stack frame nằm giữa (kể cả của các hàm trợ giúp).
Về mặt vận hành, cơ chế này gần gũi với một lệnh longjmp hoặc một pha chuyển đổi luồng gọn nhẹ (fiber switch) hơn rất nhiều so với một lệnh gọi hàm (call) và phản hồi (return) thông thường. Các thanh ghi callee-save lẫn thanh ghi stack frame %rbp và %rsp phải được khôi phục.
Tệp nhị phân (binary) của chúng tôi liên kết với hai thư viện cùng chứa các triển khai của những hàm thực hiện gỡ rối ngoại lệ C++: libgcc và GNU libunwind. Các định nghĩa của GNU libunwind lại là những định nghĩa được trình liên kết động (dynamic linker) lựa chọn. Điều đó làm chúng tôi ngạc nhiên; chúng tôi đã kỳ vọng rằng triển khai của libgcc sẽ thắng thế nhờ vào các quy tắc định phiên bản ký hiệu (symbol versioning rules); thế nhưng, việc kiểm tra các tệp nhị phân đang chạy cho thấy thực tế không phải như vậy.
Đến lúc này, giả thuyết làm việc của chúng tôi thay đổi, khi chúng tôi nới lỏng thêm một giả định đã đặt ra lúc còn nghĩ chỉ có một lỗi.
Có lẽ chúng tôi không phải đang chứng kiến một lệnh phản hồi hàm (return) thông thường quay về địa chỉ NULL. Có lẽ chúng tôi đang chứng kiến một pha chuyển giao gỡ rối (unwind transfer) — về bản chất là một quá trình khôi phục thanh ghi theo kiểu setcontext — nơi mà con trỏ lệnh đích (destination instruction pointer - %rip) đã bị biến thành NULL từ trước khi quyền điều khiển được chuyển giao. Nói cách khác, đây là do dữ liệu sai lệch đến từ chính thư viện gỡ rối (unwind library), chứ không phải do một ô nhớ chứa địa chỉ phản hồi bị ghi sai trên ngăn xếp.
Điều đó thu hẹp vấn đề rất nhiều. Hoặc GNU libunwind đang tính sai trạng thái đích, hoặc nó tính đúng trạng thái và có thứ gì đó làm hỏng trạng thái đó trước khi được áp dụng.
Chúng tôi đã đọc mã nguồn của GNU libunwind và phát hiện ra rằng nó tự tổng hợp một cấu trúc ucontext_t ngay trên ngăn xếp (stack), điền trạng thái thanh ghi mong muốn cho frame của trình dọn dẹp, rồi sau đó chuyển một con trỏ tới cấu trúc đó cho một thủ tục hợp ngữ (assembly routine) nội bộ: _Ux86_64_setcontext.
Đến lúc này, chúng tôi đã có đủ mọi mảnh ghép.
ucontext_t được tổng hợp nằm trong một trong các stack frame bị gỡ rối bởi _Ux86_64_setcontext trong lúc hàm đó thực thi. Có phải _Ux86_64_setcontext đọc từ struct sau khi nó thay đổi %rsp, lúc mà struct không còn là một phần của stack đang hoạt động nữa? Điều đó sẽ khiến nó dễ bị ghi đè hay phá huỷ bởi tiến trình chuyển giao tín hiệu hệ điều hànhu, chẳng hạn SIGUSR2 vô cùng thường xuyên của chúng tôi.
Câu trả lời là có.
Đây là sáu lệnh cuối của _Ux86_64_setcontext trong phiên bản GNU libunwind chúng tôi đang dùng, chủ yếu gồm các lệnh mov nạp từ bộ nhớ vào thanh ghi đích:
(%rdi trỏ tới ucontext_t được cấp phát trên stack, và các macro UC_MCONTEXT_* chỉ mở rộng thành offset cố định nơi một thanh ghi cụ thể được lưu.)
Lệnh thực thi đầu tiên chính là điểm khởi đầu của cửa sổ tương tranh (race window). Nó cập nhật %rsp để trỏ tới đáy mới của stack đang hoạt động. Ngay khi điều này xảy ra, cấu trúc dữ liệu (struct) được trỏ bởi thanh ghi %rdi không còn thuộc về stack đang hoạt động (hoặc vùng đệm red zone) nữa, đồng thời nó cũng không còn là vùng cấm đối với kernel.
Thông thường điều này không gây vấn đề, nhưng nếu tín hiệu đến đúng (hay sai?) thời điểm, kernel sẽ dựng frame tín hiệu tại %rsp-128. Điều đó có thể ghi đè vùng nhớ mà %rdi trỏ tới.
Nếu thảm họa đó xảy ra trước khi lệnh kế tiếp kịp đọc giá trị UC_MCONTEXT_GREGS_RIP(%rdi), thì con trỏ lệnh được khôi phục sẽ bị hỏng dữ liệu. Trong các ca sập nguồn của chúng tôi, nó đã bị biến thành NULL.
Lỗi chính là ở đây.
"Đoạn mã hợp ngữ (assembly) này cũng giải thích cho một trong những quan sát từng khiến chúng tôi bối rối: tại sao hàm X lại có một giá trị NULL nằm ở ô chứa địa chỉ trả về (return address slot) của khung ngăn xếp (stack frame) liền trước.
setcontext được viết để khôi phục mọi thanh ghi, bao gồm %rdi, nên nó không thể dùng thanh ghi đó để đọc UC_MCONTEXT_GREGS_RIP(%rdi) ở khoảnh khắc cuối của quá trình chuyển quyền điều khiển. Thay vào đó, nó đọc giá trị sớm hơn, lưu vào stack, khôi phục thêm vài thanh ghi, rồi dùng retq để đọc giá trị đã lưu và chuyển quyền điều khiển.
Những gì hiển thị trong các tệp ghi nhớ lõi (cores) trông như thể "một hàm trả về giá trị NULL" thực chất lại là "trình gỡ rối ngăn xếp (unwinder) đã tổng hợp một địa chỉ trả về đích trên ngăn xếp, nhưng mục tiêu đó đã bị hỏng trước khi quá trình chuyển giao hoàn tất". Trước đó, chúng tôi đã giả định rằng việc dữ liệu ở ô chứa địa chỉ trả về bị hỏng phải xảy ra ngay tại chỗ (in-place), bởi vì chúng tôi không biết có bất kỳ vị trí nào mà dữ liệu (có nguy cơ bị hỏng) lại cố tình được ghi vào ô chứa địa chỉ trả về.
Điều khiến cho lỗi này trở nên vô lý một cách kinh khủng chính là độ hẹp của cửa sổ tương tranh này. Trong kiểu lỗi tranh chấp tài nguyên (race condition) này, một sự kiện bên ngoài (ở đây là tín hiệu) cần phải xảy ra ngay giữa hai bước xử lý của một luồng khác. Hai bước đó càng nằm gần nhau bao nhiêu, thì xác suất để lỗi tương tranh xảy ra càng thấp bấy nhiêu.
Trong trường hợp này, cửa sổ dễ tổn thương đúng nghĩa chỉ rộng một lệnh! Tín hiệu phải được chuyển phát sau khi %rsp đã thay đổi, nhưng trước khi lệnh kế tiếp nạp %rip. Một CPU siêu tuần tự và thực thi không theo thứ tự (super-scalar out-of-order CPU) hiện đại có thể chạy vài câu lệnh đơn giản loại này trên mỗi chu kỳ xung nhịp, vì vậy cửa sổ tương tranh này chỉ kéo dài vỏn vẹn khoảng một trăm pico-giây (picoseconds).
Khi phát hiện ra lỗi tương tranh này, phản ứng đầu tiên của chúng tôi là nghĩ rằng nó quá hiếm gặp để có thể giải thích cho tỷ lệ sập nguồn đã ghi nhận. Chúng tôi đã chứng kiến hơn mốt tá cú sập dạng 'trả về giá trị NULL' (return-to-null) mỗi ngày trên toàn bộ hệ thống máy chủ (fleet). Liệu một cửa sổ tương tranh chỉ dài đúng một câu lệnh trong quá trình dọn dẹp ngoại lệ (exception cleanup) có thực sự là nguyên nhân gây ra điều đó?
Chúng tôi chuyển sang ước lượng Fermat. Nếu cửa sổ dễ tổn thương ở cỡ giây và SIGUSR2 đến mỗi giây thời gian CPU, thì mỗi trình dọn dẹp ngoại lệ (exception cleanup handler) hoặc khối lệnh bắt ngoại lệ (catch block) sẽ có xác suất thua cuộc trong trận chiến tương tranh này khoảng .
Rockset dùng exception như một phần trong cơ chế kiểm soát áp lực ngược nội bộ (internal ingest backpressure mechanism) khi nạp dữ liệu. Một host quá tải có thể ném khoảng exception mỗi giây. Điều đó hàm ý tthời gian trung bình giữa các lần gặp sự cố (MTBF) của một máy chủ đang kích hoạt cơ chế áp lực ngược là giây, tức một sự cố sau mỗi vài giờ. Ở quy mô toàn đội máy, như vậy là quá đủ để giải thích tần suất sự cố quan sát được.
Lỗi GNU libunwind vốn đã có từ lâu—hơn 18 năm rồi, nay xuất hiện phiên bản x86_64 đầu tiên hỗ trợ cơ chế gỡ rối ngoại lệ (exception unwinding) của C++.
Vậy tại sao bây giờ mới lộ diện?
Tỷ lệ sự cố xấp xỉ tỷ lệ thuận với số exception được ném và số tín hiệu được chuyển phát. Nó cũng phụ thuộc vào lượng stack mà trình xử lý tín hiệu tiêu thụ.
Rockset là một trường hợp khác biệt trên cả ba khía cạnh kể trên. Chúng tôi ném exception với tần suất cao như một phần của cơ chế kiểm soát quá tải thông thường; chúng tôi chuyển phát SIGUSR2 thường xuyên một cách bất thường do sử dụngcoarse_thread_cputime_clock; và đầu năm nay, chúng tôi khiến trình xử lý SIGUSR2 dùng nhiều stack hơn bằng cách thêm một lệnh gọi đến hàm timer_getoverrun, nhằm thống kê các tín hiệu bị gộp.
Thay đổi cuối cùng đó dường như đóng vai trò vô cùng quan trọng. Nếu trình xử lý (handler) tiêu tốn rất ít dung lượng ngăn xếp (stack), nó có thể sẽ không chạm tới và ghi đè lên vùng nhớ ucontext_t cũ đã hết hạn (stale). Trước khi thực hiện thay đổi đó, chúng tôi hoàn toàn không quan sát thấy các cú sập này. Sau khi thay đổi, tỷ lệ xảy ra lỗi vẫn duy trì ở mức thấp cho đến khi chúng tôi tăng tải cho một số trường hợp sử dụng cụ thể – điều đã gây áp lực lớn lên cơ chế kiểm soát áp lực ngược (backpressure).
Nói cách khác, lỗi libunwind luôn tồn tại, nhưng phải cho đến gần đây, tích số giữa tần suất ném ngoại lệ, tần suất gửi tín hiệu, và dung lượng ngăn xếp mà trình xử lý tiêu tốn mới vượt qua ngưỡng khiến nó bộc lộ rõ ràng trên hệ thống vận hành.
Cơ chế này cũng giải thích cho sự trùng hợp ngẫu nhiên khi cả lỗi phần cứng lẫn lỗi trong libunwind đều gây sập nguồn chủ yếu bên trong hàm DocumentTree::updateDocument. Các cú sập từ libunwind bị lệch mạnh về phía phương thức này, bởi vì nó luôn ở trạng thái hoạt động ngay tại thời điểm chúng tôi ném ra một ngoại lệ để áp dụng cơ chế áp lực ngược khi nạp dữ liệu. Nó cũng bị chọn trúng với tần suất rất cao trong các cú sập do lệch thanh ghi %rsp là vì nút mạng phần cứng bị lỗi thuộc về một dòng mã máy (SKU) mà chúng tôi chuyên dùng cho việc nạp dữ liệu số lượng lớn, vốn dành phần lớn thời gian chạy của CPU cho phương thức đó.
Biện pháp giảm thiểu ngay lập tức của chúng tôi là chuyển từ thư viện libunwind của GNU sang trình gỡ rối (unwinder) của libgcc. Bản thân việc chuyển đổi này đã là một sự đánh đổi có lợi: kiến trúc của libgcc đã được tối ưu hóa rất nhiều nhằm giảm thiểu tình trạng tranh chấp khóa (lock contention), một yếu tố cực kỳ quan trọng khi mở rộng hệ thống lên các máy ảo (VM) có cấu hình lớn.
Chúng tôi cũng đã đóng góp ngược lại (upstream) một bộ mã tái hiện lỗi độc lập (self-contained reproducer) cùng với một bản vá lỗi(mở trong cửa sổ mới) cho dự án GNU libunwind, đồng thời xác minh rằng các trình gỡ rối (unwinder) khác không gặp phải sự cố tương tự.
Hành trình tìm và sửa lỗi này đã dạy cho chúng tôi rất nhiều điều về chi tiết kỹ thuật cụ thể của liên kết động (dynamic linking), dữ liệu đặc tả gỡ rối DWARF (DWARF unwind metadata), cơ chế chuyển giao tín hiệu của Linux (Linux signal delivery), chuẩn giao tiếp System V ABI, và bộ máy vận hành ngoại lệ của C++ (C++ exception machinery). Nhưng bài học lớn rút ra được lại đơn giản hơn tất thảy.
Bước đi quan trọng nhất không phải là khả năng đọc mã máy (assembly) tài tình hay sự am hiểu sâu sắc về các chi tiết kỹ thuật. Đó là việc xây dựng được một tập dữ liệu chất lượng cao. Nếu thiếu tập dữ liệu này, chúng tôi đã gộp hai hiện tượng hoàn toàn khác biệt vào chung một câu chuyện và cố gắng dùng lập luận để tự thoát ra khỏi mớ hỗn độn đó. Khi có dữ liệu quần thể chính xác và đầy đủ, cấu trúc của vấn đề trở nên rõ ràng: một quần thể sự cố thuộc về một host lỗi, còn quần thể kia thuộc về một lỗi tương tranh libunwind. Khi dữ liệu tốt hơn, việc gỡ lỗi cũng dễ hơn.
Với các hệ thống hạ tầng như Rockset, điều đó rất quan trọng. Cuộc điều tra này củng cố cam kết của chúng tôi với việc đo đạc chuyên sâu, điều tra tự động và liên tục cải thiện công cụ vận hành. Sự tin cậy không chỉ nằm ở việc sửa lỗi ngay khi xảy ra—mà là xây dựng dữ liệu, quy trình làm việc và kỹ năng để biến những vấn đề tưởng bất khả thi thành các vấn đề có thể chẩn đoán và giải quyết.
Tác giả
By Nathan Bronson, Member of Technical Staff


