核心概念
2026 年 6 月,OpenAI 工程師 Nathan Bronson 記錄了一場持續數月的神秘崩潰調查,主角是 Rockset——ChatGPT 資料基礎設施中用 C++ 撰寫的搜尋與即時分析服務。症狀讓人費解:函數執行完後跳回不存在的地址,要嘛回傳地址是 NULL,要嘛堆疊指標 %rsp 偏移了 8 個位元組。每一個假說都有反例,bug 看起來「不可能存在」。
最終調查結論:兩個完全不相關的 bug 被誤當成同一個問題。破局的關鍵不是任何深度技術洞察,而是從「醫生模式」切換到「流行病學家模式」。
兩種調試哲學
醫生模式(Doctor mode):深度檢查個別案例,構建假說,逐一排除。OpenAI 團隊最初幾週都是這樣做的——仔細讀每個 core dump 的堆疊和暫存器,嘗試重建崩潰前的執行歷程。問題在於,他們在不知情的情況下把兩個不同 bug 的案例混在一起,導致一個 bug 的反例干擾另一個 bug 的假說,無法找到單一內聚的解釋。
流行病學家模式(Epidemiologist mode):建立整體族群的高品質資料集,尋找只有大規模數據才能揭露的統計規律。崩潰集中在某個硬體 SKU?某個地理區域?有清晰的時間起點?這些問題單靠個案分析根本無從回答。
轉折點是他們用 ChatGPT 寫了一個自動化腳本,下載過去一年所有 production Rockset core dump 的前綴、提取暫存器狀態、過濾已知誤報,並自動標記每個崩潰為 return-to-null、misaligned-stack 或其他類型,然後對全部歷史 core dump 並行執行。資料一乾淨,兩個崩潰族群的邊界立刻清晰可見。
Bug #1:矽基硬體靜默腐蝕
Misaligned-stack 崩潰(%rsp 偏移 8 bytes)的分布特徵極其明確:全部集中在同一個地理區域、有清晰的開始日期、從未發生在長時間運行的節點上。追蹤下去,是 Azure 雲端的一台實體主機——CPU 靜默地計算出錯誤的數學結果(silent hardware corruption),跑在上面的不同 VM 都受到波及。
修復很直接:把那台主機加入黑名單。但硬體失效不是可以從根本上修復的軟體 bug,而是需要偵測機制。後續改善包括:讓 fatal signal handler 記錄完整暫存器狀態(不需 core dump 即可偵測復發)、修改控制平面讓 VM 優先重用而非回收(讓壞節點更容易被定位到特定實體主機)。
Bug #2:一條指令寬的競爭窗口
去除硬體崩潰族群後,剩餘的 return-to-null 崩潰的模式反轉了:原本被排除的「exception unwinding 相關」假說,在乾淨數據下反而得到強力支持——所有崩潰都發生在 C++ exception unwinding 過程中。
C++ 的 exception 機制在 throw 時不是普通函數呼叫,而是類似 longjmp 的動態控制轉移:runtime 掃描堆疊找 catch block,還原中間堆疊幀的所有暫存器(包括 %rbp 和 %rsp),再跳到 catch handler。這個工作由 Rockset binary 鏈接的 GNU libunwind 負責,核心函數是 _Ux86_64_setcontext。
問題藏在這個函數的最後幾條指令裡:
74: mov UC_MCONTEXT_GREGS_RSP(%rdi),%rsp ← 競爭窗口開始
...
77: mov UC_MCONTEXT_GREGS_RIP(%rdi),%rcx ← 競爭窗口結束
78: push %rcx
82: retq
第 74 行把 %rsp 更新為新的堆疊底部。這個瞬間之後,%rdi 所指的 ucontext_t struct 已不在 active stack 或 red zone 範圍內——Linux ABI 不再承諾保護它。若此時 SIGUSR2 到達,kernel 會在 %rsp-128 建立 signal frame,可能覆寫 ucontext_t。若覆寫發生在第 77 行讀取 %rip 之前,恢復的指令指標就會變成 NULL,程式崩潰。
競爭窗口只有一條指令寬,約 100 皮秒。Fermi 估算:SIGUSR2 每 10 毫秒 CPU 時間送一次,每次 exception 觸發概率約 10^-8。Rockset 用 exception 做 ingest 背壓控制,高峰每秒拋出 10^4 次——平均故障間隔數小時,足以解釋觀察到的每日十幾次崩潰。
為何現在才出現? 這個 GNU libunwind bug 存在超過 18 年(自第一個支援 C++ exception unwinding 的 x86_64 版本起),但三個因素的乘積剛剛跨越可見閾值:exception 頻率 × 信號頻率 × signal handler 堆疊使用量。今年初他們為統計合併信號加入了 timer_getoverrun 呼叫,讓 signal frame 剛好可以覆寫到 stale ucontext_t 記憶體。
修復:切換回 libgcc 的 unwinder(在鎖爭用方面也表現更好),並向上游 GNU libunwind 提交了可重現的複製器與 patch。
關鍵要點
- 族群資料是突破口:混合了兩個不同 bug 的噪音中,任何個案分析都會產生矛盾。只有完整、乾淨的族群資料集,才能讓兩個崩潰族群的邊界清晰可見
- 硬體靜默錯誤比軟體 bug 更隱蔽:它跨多個 VM(但實際是同一實體主機)、症狀與軟體 bug 幾乎相同,唯有族群數據的時間戳記和地理邊界才能揭露
- narrow race window 不等於低影響:100 皮秒的競爭窗口乘以足夠高的 exception 率,每日仍產生十幾次崩潰。Fermi 估算是「這個假說是否能解釋觀察到的崩潰率」的必要工具
- 廣泛使用的開源基礎元件可能藏著 18 年的地雷:只有特定的使用組合(高 exception 率 × 高信號頻率 × 大 signal handler stack)才能觸發,且症狀會偽裝成普通的應用程式崩潰
- 可觀測性是可靠性的前提:能夠自動化分析大量 core dump、從族群角度看崩潰分布,是把「看起來不可能」的 bug 轉為「可診斷、可修復」問題的基礎建設
實務應用
C/C++ 服務出現低頻率但症狀神秘的崩潰時:
- 先投資建立自動化 core dump 分析 pipeline,批量標記崩潰類型
- 對整體崩潰族群做時序與硬體分布分析,確認是否為同質族群;若不是,先拆分
- 使用 Fermi 估算評估假說是否與觀察到的崩潰頻率相符,不要因「概率極低」而過早排除
- 涉及 signal delivery + C++ exception 的崩潰,優先懷疑 unwinder 競爭條件,並確認實際使用的是哪個 unwinder 實作
- 修復後增強 fatal signal handler 的可觀測性,讓未來類似問題能直接從日誌診斷
延伸觀點
OpenAI 的調查揭示了「靜默硬體錯誤」在大規模雲端環境中的普遍性。Meta 的 Engineering 部落格(2022)提出了兩種互補的偵測方法:Fleetscanner(在維護窗口做更深度的離線測試)與 Ripple(在生產環境中低開銷地持續測試)。兩者都必要——Ripple 能在 15 天內偵測到 70% 的艦隊範圍腐蝕,而 Fleetscanner 負責捕捉剩下 30% 只有更長時間測試才能暴露的故障。OpenAI 的做法與 Meta 的 Fleetscanner 思路一致:先把問題 host 加入黑名單,再改善控制平面讓壞節點更容易被偵測,而不是試圖在軟體層徹底消除硬體腐蝕的可能性。
在族群調試方法論上,OpenAI 的「流行病學家 vs. 醫生」框架與 Netflix、Google SRE 等大型基礎設施團隊的實踐一脈相承:先建立可信賴的可觀測性數據層,再基於數據做推論,而非憑直覺深挖個案。這個思路在 GPU 時代尤為重要——隨著 AI 訓練集群規模擴大,硬體靜默錯誤的出現頻率線性增長,但 GPU SDC 的偵測難度遠高於 CPU(因為 GPU 計算結果更難自動驗證正確性)。
GNU libunwind 的競爭條件案例則提醒工程師:廣泛使用的 C runtime 基礎庫往往未被充分測試在高信號密度環境下的行為。libgcc 的 unwinder 在鎖爭用優化方面也有實質優勢,是更安全的預設選擇。工程師在選擇 unwinder 實作時,應主動確認 dynamic linker 的符號解析順序,而非假設預期的實作「自然就會被選到」。