OpenAI 的模型和代理程式越來越依賴可擴展的資料基礎設施,以便在模型思考使用者問題時,即時搜尋相關資料。其中一些服務以 C++ 編寫,其低階系統控制能最大化效能並最小化記憶體使用。這些效率優勢在我們擴展時至關重要,但 C++ 缺乏記憶體安全,意味著錯誤可能因寫入不正確或不存在的記憶體位址而導致程式崩潰。

幾個月前,我們觀察到 Rockset 服務內部發生了一些崩潰。Rockset 是我們 ChatGPT 資料基礎設施的客製化部分,對許多資料外掛程式和對話搜尋至關重要。在每次崩潰中,一個正常的 C++ 函式似乎完成後卻返回到一個無效的位址,導致核心停止程式,因為指令指標不再指向程式碼。

有時堆疊框架中的返回位址欄位為 NULL。有時堆疊指標 CPU 暫存器本身似乎偏移了 8 位元組,就好像 %rsp 在正常執行中不知何故被遞減了。在這兩種情況下,崩潰都發生在返回時。

這些並非應用程式程式碼的正常故障模式。一個隨機寫入恰好落在儲存的返回位址上是可能的,但極為罕見。一個在不涉及內聯組譯、setcontext 或 longjmp(我們都沒有使用)的情況下,使 %rsp 偏移 8 位元組的錯誤則更為奇怪,因為編譯後的程式碼只會在函式前言和結尾直接調整該暫存器。我們(或 ChatGPT)能想到的每個假設都有強烈的反證,因此這個錯誤似乎不可能存在。

我們原以為是一個問題,最終卻發現是兩個不相關的錯誤,巧合地同時被發現。首先,是一個 Azure 主機上的靜默硬體損壞,其中 CPU 的數學運算不正確。其次,是 GNU libunwind 中一個長達 18 年的競爭條件錯誤,一個在廣泛使用的開源函式庫中未被注意到的錯誤。

這篇文章講述了我們如何透過像流行病學家一樣思考,並建立關於所有崩潰的高品質資料集,來識別並修復這些看似無法解釋的崩潰。

首先,讓我們深入了解 Rockset。它是一個用於搜尋和即時分析的雲原生資料系統,OpenAI 將其用於許多內部應用,例如同步連接器(Rockset 於 2024 年被 OpenAI 收購)。串流更新用於維護工作區知識庫的最新索引,以便 ChatGPT 在回答問題或執行動作時可以搜尋相關資訊。

Rockset 的執行層以 C++ 編寫。C++ 語言提供對 CPU 的低階存取,這對效能和效率很有利,但也意味著應用程式錯誤可能導致無效的記憶體存取和段錯誤(segfaults)。為了追蹤這些問題,我們使用 folly 的致命訊號處理器在崩潰發生時記錄堆疊追蹤,並將相應的核心傾印(程式崩潰時的狀態快照)上傳到 Azure Blob 儲存以供後續分析。

Rockset 的所有查詢處理葉節點都是複製的,這最大程度地減少了崩潰對客戶的影響。然而,每個段錯誤都對應一個需要修復的錯誤,以達到我們的可靠性和品質目標。

我們最初的方法是將這些核心傾印視為傳統的偵錯問題:仔細檢查幾個核心傾印,形成假設,然後逐一排除。

大多數崩潰發生在一個名為 DocumentTree::updateDocument 的方法中。在這些崩潰中,似乎 updateDocument 呼叫了某個未知函式 X,堆疊在 X 活躍時被損壞,然後 X 返回到一個不可執行的程式碼位址。在某些情況下,X 剛彈出的框架看起來有效,只是其儲存的返回位址為 NULL。

在其他情況下,堆疊指標本身看起來不正確,但下一個有效的框架似乎仍然是 updateDocument。

我們不知道堆疊何時被損壞,這留下了一個巨大的搜尋空間。updateDocument 是一個大型方法,經歷了大量的內聯,因此 X 的候選數量非常龐大。這是一個我們 C++ 程式碼中的錯誤嗎?編譯器或連結問題?我們的某個執行時函式庫的問題?Linux 核心在訊號傳遞或上下文切換方面的錯誤?還是更罕見的問題?如果這是一個隨機寫入,為什麼我們的 ASAN 預備環境沒有捕捉到它?

我們試圖使用應用程式層級的日誌來識別所有問題的實例,但堆疊損壞錯誤很難僅從日誌中分類,因為記錄的堆疊追蹤本身就已損壞或遺失。我們無法建構一個既沒有誤報也沒有誤報的日誌查詢。我們手動檢查了更多的核心傾印,並發現了一些額外的範例,但這個過程太過耗時,無法提供一個可靠的資料集。

在調查的這個階段,我們(錯誤地)排除了硬體錯誤,因為我們在多個區域和多種硬體類型上都看到了崩潰,所以我們仍在尋找純軟體原因。有幾天,我們深入研究了一個單一的堆疊指標錯位(misaligned-%rsp)崩潰,利用堆疊和暫存器內容重建了崩潰前的歷史。這產生了一些可能的線索,但由於我們沒有放棄所有錯誤都有相同原因的初步結論,這並沒有讓我們擺脫困境。

在我們調查的轉捩點之前,解釋我們從核心檔案中提取了哪些資訊很重要。Rockset 使用 -fno-omit-frame-pointer 編譯,因此活動的堆疊框架始終可以透過 %rbp 存取,並且呼叫者形成一個框架指標的鏈結串列。在 Linux x86_64 上,AMD64 System V ABI 還在 %rsp 下方保留了 128 位元組作為紅區(red zone)。

該區域可供使用者空間程式碼使用,重要的是,核心承諾在傳遞訊號時不會破壞它,這是 ABI 契約的一部分。

紅區對於我們偵錯返回後崩潰至關重要,因為它保留了返回前的一些資訊。當 SIGSEGV 被觸發時,folly 的致命訊號處理器會在崩潰執行緒的堆疊上執行。不再活躍的堆疊框架(因為其函式已返回)將被訊號處理器破壞,除了最後 128 位元組。這就是為什麼我們可以說「X 剛彈出的堆疊框架看起來有效,除了返回位址為 NULL」。

紅區保留了一些不活躍的框架,或者有時只是一個不活躍框架的尾部。我們發現了一個堆疊錯位崩潰,其中涉及的所有函式都非常小。這讓我們看到 %rsp 在一個相對簡單的函式執行期間變得錯位,並且之後有更多的呼叫成功執行。程式只在活動函式最終嘗試返回時才崩潰。

這些程式碼路徑都沒有使用例外、內聯組譯、setcontext 或 longjmp,因此如果堆疊指標確實如核心傾印所示那樣改變,那麼使用者空間程式碼中沒有任何合理的錯誤可以解釋這個問題。

這將我們推向了核心。Rockset 比大多數程式更積極地使用訊號。查詢執行被分解成許多輕量級任務,這些任務交換資料。這對於高效處理高 QPS 工作負載很重要,但由於許多查詢的工作被多工處理到相同的執行緒池上,因此每個查詢的 CPU 計費變得尷尬。

我們的解決方案是一種我們稱為 coarse_thread_cputime_clock 的機制,它以足夠低的成本近似 clock_gettime(CLOCK_THREAD_CPUTIME_ID, ...),以便在每個任務邊界進行取樣。timer_create API 可用於根據時間流逝的幾種概念(包括 CPU 時間的累積)來安排週期性訊號傳遞。

我們安排每隔幾毫秒的 CPU 時間傳遞一個訊號(SIGUSR2),此時訊號處理器會更新一個執行緒局部變數。儘管許多任務在執行時沒有看到粗略時鐘推進,但將所有增量相加會產生對查詢實際 CPU 時間的無偏估計。

因為我們如此頻繁地傳遞訊號,所以一個罕見的關於上下文切換或訊號傳遞的核心錯誤似乎是合理的。我們花時間閱讀錯誤報告、核心原始碼和 Azure 特定的核心修補程式。我們嘗試了壓力測試。我們未能找到任何看似相關的問題。在那時,我們決定退一步,嘗試一種不同的方法。

醫生還是流行病學家?有兩種廣泛的方法來偵錯這類問題。一種是像醫生一樣:專注於一個「病人」,進行大量測試,並嘗試從詳細證據中診斷單一案例。另一種是更像流行病學家:觀察整個「族群」,並詢問是否存在單一案例無法揭示的模式。這個錯誤是從特定版本開始的嗎?它是否與某個硬體 SKU(特定的 CPU 和伺服器型號)、某個區域或某個核心版本相關?在看似單一症狀的背後,是否隱藏著多個不同的群集?

我們之前大多處於醫生模式。關鍵的轉變是決定需要收集高品質的族群資料。

清理資料。我們之前嘗試自動尋找所有問題實例的嘗試失敗了,因為我們試圖透過日誌進行文字搜尋。核心傾倒本身包含更多資訊,但手動查看它們無法擴展。我們決定投入精力建立一個可以自動分析核心傾倒的管道。

我們讓 ChatGPT 編寫了一個腳本,該腳本下載每個核心檔案的前綴,提取暫存器,使用日誌過濾已知的誤報,並自動將崩潰標記為返回到 NULL、堆疊錯位或其他。然後我們將該腳本並行運行在過去一年中所有生產環境的 Rockset 核心傾倒上。

這就是轉捩點。一旦我們有了乾淨的資料集,相關性立即顯現出來。我們一直視為一個奇怪錯誤的問題,實際上是兩個獨立的崩潰族群。

返回到 NULL 的核心傾倒分佈在許多叢集和地理區域。它們的頻率最近有所增加,但沒有明確的開始日期和清晰的基礎設施邊界。

堆疊錯位崩潰看起來完全不同。它們都來自一個區域,有明確的開始日期,並且從未發生在長時間運行的節點上。儘管它們涉及多個 Azure 虛擬機器(VM),但模式看起來像是一台實體機器硬體不良,對恰好落在其上的任何虛擬機器造成問題。

那一刻我們意識到我們一直將兩個錯誤混為一談。由於我們混合了來自兩個錯誤的反例,我們無法找到一個單一的、連貫的解釋。

錯誤 #1:有問題的主機。有了乾淨的 Kubernetes 節點和時間戳列表,我們能夠將堆疊錯位崩潰追溯到單一實體主機,並輕鬆地將其列入黑名單。

我們無法在受控環境中重現該主機上的暫存器損壞,即使經過數週的壓力測試。然而,一旦有問題的主機停止服務,堆疊錯位崩潰就消失了。移除有問題的主機並非永久解決方案,因為它不能防止相同問題再次發生。但是,我們可以修改軟體,以便如果類似問題再次發生,可以輕鬆檢測和處理。

我們改進了致命訊號處理器以包含暫存器狀態,這樣我們就可以僅從日誌中檢測復發(無需核心傾倒)。我們更改了控制平面,使虛擬機器通常被重複使用而不是回收,這使得在我們基礎設施堆疊層級更容易檢測到有問題的節點。我們還更新了我們的操作手冊(以及我們團隊的心智模型)以包含這種可能性。

將有問題主機的崩潰分離出來後,剩餘的返回到 NULL 的核心傾倒變得更容易理解。