Tóm tắt
OpenAI mô tả cách họ xử lý một nhóm crash C++ tưởng như không thể giải thích trong Rockset, một phần của hạ tầng dữ liệu dùng cho ChatGPT. Ban đầu các core dump trông như cùng một lỗi stack corruption, nhưng phân tích trên toàn bộ tập core dump cho thấy thực ra có hai vấn đề độc lập: một host Azure bị lỗi phần cứng âm thầm và một race condition 18 năm tuổi trong GNU libunwind.
Bài học chính không nằm ở một mẹo assembly hào nhoáng. Điểm quan trọng là biến crash thành dataset đủ sạch để nhìn thấy pattern theo vùng, hardware SKU, thời điểm bắt đầu và code path.
Bối cảnh
Một số service trong hạ tầng dữ liệu của OpenAI được viết bằng C++ để tối ưu hiệu năng và bộ nhớ. Đổi lại, lỗi memory safety có thể làm process crash bằng cách ghi vào địa chỉ sai hoặc không tồn tại.
Trong Rockset, các crash xuất hiện khi một hàm C++ bình thường kết thúc rồi return tới địa chỉ không hợp lệ. Có lúc slot return address trong stack frame là NULL. Có lúc thanh ghi stack pointer %rsp lệch 8 byte, như thể nó bị giảm ngay giữa lúc chương trình đang chạy bình thường.
Những failure mode này rất lạ. Một write lạc chỉ đụng đúng saved return address đã hiếm; %rsp lệch 8 byte mà không dùng inline assembly, setcontext, hoặc longjmp còn khó tin hơn.
Cách debug ban đầu
Team bắt đầu bằng kiểu debug truyền thống: mở vài core dump, soi thật kỹ, đặt giả thuyết rồi loại dần. Phần lớn crash xuất hiện quanh DocumentTree::updateDocument, một method lớn có nhiều inline, nên không gian tìm kiếm rất rộng.
Log ở tầng ứng dụng không đủ tốt để phân loại vì stack trace trong crash kiểu này cũng có thể đã bị corrupt hoặc thiếu. Soi thủ công thêm nhiều core dump giúp có thêm ví dụ, nhưng không tạo được dataset đáng tin.
Sai lầm ban đầu là coi mọi crash là một hiện tượng duy nhất. Vì crash xuất hiện ở nhiều region và nhiều loại hardware, team loại trừ hardware quá sớm và tiếp tục tìm nguyên nhân thuần software.
Bước ngoặt: debug như nhà dịch tễ học
OpenAI chuyển từ “doctor mode” sang “epidemiologist mode”. Thay vì chẩn đoán từng case riêng lẻ, họ phân tích toàn bộ population crash:
- Lỗi có bắt đầu sau một release cụ thể không?
- Có tập trung ở một region, kernel version, hardware SKU, hay node Kubernetes nào không?
- Có nhiều cụm lỗi khác nhau đang bị trộn thành một câu chuyện không?
Họ dùng ChatGPT để viết script tải một phần mỗi core file, trích xuất register, lọc false positive bằng log, rồi tự động gắn nhãn crash thành return-to-null, misaligned-stack, hoặc nhóm khác. Script chạy song song trên toàn bộ core dump production của Rockset trong năm trước.
Khi dataset sạch xuất hiện, pattern lộ ra ngay: đây không phải một bug, mà là hai crash population khác nhau.
Bug 1: host phần cứng lỗi
Nhóm misaligned-stack chỉ đến từ một region, có ngày bắt đầu rõ ràng, và không xảy ra trên node đã chạy lâu. Pattern này khớp với một physical host có phần cứng lỗi, nơi các VM tình cờ được đặt lên sẽ gặp register corruption.
OpenAI trace được danh sách node và timestamp về một host vật lý, rồi denylist host đó. Họ không reproduce được lỗi trong môi trường kiểm soát, nhưng sau khi loại host khỏi service, nhóm crash này biến mất.
Mitigation sau đó gồm:
- Bổ sung register state vào fatal signal handler để phát hiện tái diễn chỉ từ log.
- Điều chỉnh control plane để reuse VM thường xuyên hơn, giúp nhận diện bad node dễ hơn.
- Cập nhật runbook và mental model của team để không loại trừ hardware quá sớm.
Bug 2: race condition trong GNU libunwind
Sau khi tách nhóm hardware, phần return-to-null còn lại bắt đầu có logic hơn: tất cả đều xảy ra trong quá trình exception unwinding.
Khi C++ throw exception, runtime phải đi qua stack, tìm metadata, chạy cleanup handler và chuyển control tới catch block phù hợp. Việc này gần với longjmp hoặc fiber switch hơn là call/return bình thường, vì runtime phải restore nhiều register, bao gồm %rbp, %rsp, và instruction pointer.
Binary của Rockset link cả libgcc và GNU libunwind, nhưng dynamic linker chọn implementation của GNU libunwind. Trong source của libunwind, OpenAI thấy _Ux86_64_setcontext dựng một ucontext_t trên stack rồi dùng assembly để restore register.
Race xảy ra khi _Ux86_64_setcontext cập nhật %rsp trước, làm ucontext_t không còn nằm trong active stack nữa. Nếu signal đến đúng thời điểm đó, kernel dựng signal frame ở vùng dưới %rsp và có thể ghi đè memory mà %rdi vẫn đang trỏ tới. Nếu instruction pointer được đọc sau khi memory bị ghi đè, control transfer có thể đi tới NULL.
Đây là race window chỉ rộng một instruction. Nghe phi lý, nhưng Rockset có ba yếu tố khuếch đại xác suất:
- Throw exception với tần suất cao để xử lý ingest backpressure.
- Gửi
SIGUSR2thường xuyên để đo CPU time theo thread. - Signal handler dùng thêm stack sau một thay đổi gần đây.
Ở fleet scale, xác suất cực nhỏ vẫn đủ thành hơn chục crash mỗi ngày.
Cách sửa
Mitigation trước mắt là chuyển từ GNU libunwind sang unwinder của libgcc. Đây cũng là trade-off tốt vì implementation của libgcc đã được tối ưu lock contention cho VM lớn.
OpenAI cũng upstream reproducer và fix cho GNU libunwind, đồng thời kiểm tra các unwinder khác để đảm bảo không có lỗi tương tự.
Ý nghĩa với Dev
Bài này là ví dụ rất tốt cho debugging hạ tầng ở scale lớn. Khi một lỗi hiếm có vẻ “không thể xảy ra”, câu trả lời không nhất thiết là soi sâu hơn vào một case; nhiều khi phải xây dataset đủ sạch để thấy các cụm lỗi khác nhau.
Dev vận hành hệ thống C++, Rust, Go native extension, inference service hoặc worker queue nên rút ra vài điểm:
- Core dump không chỉ để mở bằng debugger; nó có thể là nguồn dữ liệu phân tích hàng loạt.
- Hardware fault vẫn là một giả thuyết hợp lệ, kể cả khi lỗi xuất hiện rải rác qua cloud VM.
- Signal handler, exception, unwinding và low-level runtime library có thể tương tác theo cách rất khó đoán.
- Log và runbook nên được thiết kế để phân loại lỗi hiếm mà không cần thao tác thủ công từng case.