大家好!我是 Jim,在 Bluesky 負責系統相關事務。我在此向大家說明本週一發生的情況,該情況導致 Bluesky 服務中斷,影響了約一半的用戶,持續了大約 8 小時。

首先,我必須為服務中斷對用戶造成的影響致歉。這是我任職期間遇到的最嚴重的服務中斷事件,完全無法接受。

其次,如果您對這類工作感興趣,我們正在招聘!

問題實際上始於週末。以下是事件發生前幾天 Bluesky AppView 的請求圖表:

圖中的黃色/綠色部分不重要,但那些下降的點非常糟糕!它們代表了用戶實際面臨的服務中斷時間。太痛了!

我們在 4 月 4 日星期六收到了警報。我查看了一下,認為可能是網路傳輸問題。我們有相當全面的網路監控,一切看起來都很正常。

然而,我確實注意到我們 AppView 資料後端(稱為「資料平面」)的日誌行出現了激增,類似於這樣:

這些日誌激增的時間點與用戶流量下降的時間點一致,這是有道理的。我們的資料平面大量使用 memcached 來減輕主 Scylla 資料庫的負載,如果我們耗盡了連接埠,那將是一個巨大的問題。

由於可觀察性不足,我們花了很多時間才找到實際問題。我們通常對資料平面有出色的監控,但它假設每個請求都很小,並且工作量不大。

這個特定的 RPC(GetPostRecord)接收一批貼文 URI,在 memcached 中查找它們,然後在快取未命中時查找 Scylla。我忽略的是,我們上週部署了一個新的內部服務,該服務每秒發送少於三個 GetPostRecord 請求,但有時會發送 15,000 到 20,000 個 URI 的批次。通常,我們每次請求大約會進行 1-50 次貼文查找。

資料平面中的每個 RPC 處理器都有綁定的併發(例如 errgroup.SetLimit)。但是,這個端點沒有!它是整個系統中唯一缺少這個功能的端點。

這意味著我們為請求啟動了 15,000 到 20,000 個 goroutine,透過建立大量連接來猛烈衝擊 memcached,然後關閉並將它們返回給作業系統,因為我們的最大閒置連接池大小是 1000。它們會在 TCP TIME_WAIT 狀態中堆積,並耗盡所有可用的連接埠。

糟糕!我們幾乎立刻就看到連接埠被耗盡,但卻不知道根本原因。我們有很多地方使用 memcached,我特意挑出了那一行 JSON 日誌,因為它明確指出問題出在貼文快取。我們還有用戶快取、互動計數快取等等。我們看到了所有快取類型的錯誤日誌(這是有道理的,因為所有 memcached 行為都受到影響),所以一開始並不清楚問題出在 GetPostRecord。

另外請注意,新的內部服務目前只運行在我們的一個資料中心,這就是為什麼我們只看到該站點出現問題。這無疑加劇了混亂,因為我們在資料平面中沒有按客戶端進行指標統計。

直到本週三我們才找到並修復這個問題,儘管服務在週一就已經穩定下來。那麼,在此期間我們做了什麼來阻止事態惡化呢?

我花了大部分時間在週六和週日追查這個問題,但仍未找到根本原因,服務運行不暢,但還能維持。然後,週一,某個東西觸發了。結果是我們把自己推入了死亡螺旋!這種負回饋循環導致了週一的大規模服務中斷。

結果是,每當我們從 memcache 收到錯誤時,我們都會記錄下來。我們每秒向 memcached 實例發送數百萬個請求,因此我們試圖每秒記錄數百萬條日誌。

Go 中的日誌記錄使用阻塞的 write(2) 系統調用。這種巨大的阻塞系統調用數量,加上我們試圖繼續處理每秒數百萬個請求,導致 Go 運行時生成了更多的作業系統線程(在 Go 的術語中稱為 M)。與健康的基線相比,M 的數量大約增加了 10 倍(150 對 1500)。

這批更大的 M 又給垃圾回收器帶來了壓力:

這種大規模的「停止世界」GC 暫停時間意味著請求被卡住了。

再加上我們對 GOGC 和 GOMEMLIMIT 環境變數值進行了非常積極的調整,以及記憶體限制,這意味著我們的資料平面實際上會不時地發生 OOM(記憶體不足)!這就是為什麼服務會工作大約 30 分鐘,然後中斷一段時間,然後又恢復一點,然後重複。

OOM 顯然是糟糕的(我們應該為零),但它們通常不是什麼大問題。然而,由於 memcached 連接池已經飽和,當資料平面重新啟動時,由於現有連接卡在 TIME_WAIT 狀態,它無法建立新的 memcached 連接,這導致了更多的連接埠耗盡問題。死亡螺旋!

這個臨時修復很瘋狂,但確實奏效了。這就是我們在找到根本原因之前,在週一實際修復服務中斷的方法:

這讓我們擺脫了死亡循環,因為它擴展了客戶端 IP+埠空間。瘋狂,但有效!在我們修復了根本原因後,我們就移除了這個。

在我最近的演講中,我提到您應該在發生服務中斷之前添加廣泛的可觀察性。我們確實有很多,但永遠不夠!我們需要添加按客戶端的可觀察性,並獲得關於客戶端發送少量大請求的更好指標。

一切都埋藏在裡面了,但當所有東西都在崩潰時,很難知道該從哪裡入手。您需要有心理上的紀律和高粒度的指標來穿透噪音,找到真正的根本原因。這是一項艱苦的工作!

此外,記錄太多東西也不是好事。這裡記錄一點,那裡記錄一點是可以的,但我更傾向於使用 prometheus 指標或 OTEL 追蹤,因為它們更適合高規模的系統。

最後,再次為這次長時間的服務中斷致歉。我和我的團隊非常認真地對待我們的營運,這一天真的很糟糕。

編輯:另外,狀態頁面顯示這是第三方供應商的問題。顯然不是,對此誤傳表示歉意!在我發布該狀態頁面更新時,我正在查看一些 traceroute,這些 traceroute 表明從雲端供應商到我們資料中心的封包遺失相當嚴重,但那並不是問題的根本原因。

再次編輯:哇!!Lorin Hochstein 撰寫了一篇非常棒的後續部落格文章,深入探討了我在此文中略過的許多技術細節!真是太棒的閱讀體驗了!