跳到正文
The Pragmatic Engineer(youtube.com)· Gergely Orosz·· 2 天前品質評分58

與 Maude Lemaire 談 Slack 與 GitHub 的規模化

與 Maude Lemaire 談 Slack 與 GitHub 的規模化

重點導讀

Maude Lemaire 回顧 Slack 以團隊為單位的分片(sharding)遷移,以及 GitHub 的 16 分片 Vitesse 限制,結論是真正推動規模化工作的是壓力測試與移除分散式查詢(scatter query),而非 LLM。

AI 伴隨問答

自備 Key 深度分析

針對這篇報導有疑問?可直接點選快捷問題或向 AI 追問背景脈絡與技術細節:

Podcast 專家音訊節目

收錄於 888news Podcast 智庫專區,包含精華摘要與上市公司即時行情

正文

Maude Lemaire 是首席工程師,在 Slack 待了八年,後來在 GitHub 處理資料庫擴充,也在 Cursor 投入首字延遲(time-to-first-token)的最佳化。她主張,大型產品的擴充,關鍵多半在回頭檢視當年為了趕上線而做的架構捷徑,並先摸清產品實際的使用模式,再談最佳化。這場對談大部分時間放在 Slack 的資料庫分片(sharding)與壓力測試、GitHub 以 16 個分片承載所有 pull request 與 issue 的叢集及其分散式查詢(scatter query)問題,以及 Lemaire 如何在效能工作中使用 AI 工具。節目標題點名 Slack 和 GitHub,這兩家公司確實佔了大部分的內容;Cursor 與 AI 的討論則落在最後三分之一。

Lemaire 也是《Refactoring at Scale》的作者,她的敘述具體得罕見:壓力測試的實際花費、分片數量、每個頁面的查詢次數,還有她第一人稱描述自己讓 Slack 內部側邊欄掛掉半小時的經過。讀者讀完應該能建立一套可用的心智模型:多租戶分片系統在企業級規模下如何崩壞、為什麼壓力測試需要真實的 WebSocket 連線、為什麼分散式查詢會卡死 Rails 單體應用的水平擴充,以及 LLM 在效能調查中哪裡幫得上忙、哪裡幫不上。

IBM 上線後,Slack 的多租戶分片撐不住了

Slack 原本把所有資料依團隊分片,大客戶各自擁有專屬的資料庫與聊天伺服器。企業版產品想沿用這個模型,於是把大公司內部的各部門當成獨立團隊,上頭再掛一筆「enterprise」記錄,但跨團隊私訊與全公司頻道,逼得每位 IBM 使用者登入時都得查同一個 enterprise 分片。

側邊欄查詢是最外顯的症狀。頻道成員關係原本分散在公開頻道、私人頻道、私訊三張表,群組私訊後來又硬塞進私人頻道表,所以要組出左側側邊欄,就得做一個大型的 join-union。Lemaire 的同事在計算提及通知(mention badge)時也撞上同一個問題,於是由她接下遷移到單一資料表的任務。

她的遷移手法刻意做到最精簡:用正規表示式掃出大約 400 個呼叫點,然後把每一條 SQL 查詢原封不動貼進一個具名函式,外面包一層薄薄的資料存取層,完全不做最佳化。所有改動都掛上功能旗標(feature flag)、雙寫、回填,並在切換前對舊表做影子讀取(shadow read)。當時的程式碼庫沒有 ORM,API handler 是一長串程式式程式碼,SQL 直接寫在裡面;Lemaire 說團隊當時對「不要把 SQL 藏起來」這件事立場強硬,而她現在懂這個堅持了。

切換上線時,出錯的方式很具體。她在下班後的「齊聚時段」對整個 Slack 開啟新的讀取路徑,發現資料對不上,就照慣例跑指令碼清空資料表再重新回填——卻忘了旗標還開著。內部側邊欄空白了大約 25 分鐘,直到她把讀取切回舊表。整件事只有一位同事 Kevin 發現,還傳訊息給她。

「我把它開啟,然後才意識到,喔等等,不對,好像哪裡怪怪的。」

壓力測試需要真實的 WebSocket 連線,headless Chrome 太燒錢

Slack 早期的壓力測試只量 API 吞吐量和資料庫讀取。但那個 50 萬使用者的客戶,需要模擬 50 萬條同時線上的 WebSocket 連線,因為故障模式發生在訊息送達後的扇出(fan-out),而不是送出訊息本身。Lemaire 說,共同創辦人打造的有狀態 WebSocket 服務從來不是瓶頸;瓶頸在客戶端,以及下游一連串的事件連鎖反應。

第一種做法是把 Slack 當成 headless Chrome 例項,跑在 Kubernetes pod 裡,用一小段 JavaScript 指令碼點按鈕。每個 headless Chrome 例項、每五小時一輪的壓測要花 37 美分,換算下來,一次 50 萬連線的測試大約要 15 萬美元——而且每個測試時段有一半的時間都耗在重啟 crash 掉的 pod。

做法擬真度成本/可操作性
Kubernetes pod 裡的 headless Chrome最高,是真實客戶端每例項每五小時一輪 37 美分;50 萬連線測試約 15 萬美元;pod 不斷 crash
以 Go 程式發出 WebSocket、API 與邊緣快取請求較低,但涵蓋關鍵路徑輕量、可重複、便宜

替代方案是一群小型 Go 程式,每個程式開一條 WebSocket 連線,重播一段錄製好的請求序列,包含 API 請求、WebSocket 收發與邊緣快取請求。Lemaire 做了一個 Slack slash command,能在真實產品流程中錄下每一個請求與 WebSocket 訊息,再做一支工具把錄製內容轉成模擬客戶端可用的指令碼。這麼一來,產品經理可以自己錄製操作流程,把檔案交回來加進壓測系統——團隊把這套系統叫做 KoiPond。

驚群效應與事件洪水是兩種不同的故障,解法也不同

Lemaire 把故障分成兩類。驚群效應(thundering herd)是大量使用者同時做同一件事——她的經典例子是執行長在一個 10 萬人的頻道裡發「這週五大家放假」,接著 10 萬人同時開啟頻道、拉歷史訊息、加上自訂表情回應,每一個回應又再扇出給所有人。事件洪水(event flood)則是單一客戶端被事件淹沒,通常是同時身處許多熱門頻道或工作區的重度使用者。

事件洪水的例子是 Jeff Robke,一位 IBM 管理員,他同時登入大約 30 個工作區,同一則企業層級的私訊或共享頻道訊息會毫無去重地送達 30 次,讓他的客戶端反覆 crash。Lemaire 點出當中的諷刺:Slack 最重度的使用者,往往也是公司內部最大的擁護者,得到的體驗卻最差。

Slack 的緩解做法是在 WebSocket 上為訊息型別排優先順序:文字訊息優先,高負載時丟棄表情回應。Lemaire 認為這是產品決策,不是工程決策,並指出目前每則訊息最多 23 種自訂表情回應的限制只是個不完整的補丁——這些表情上的 +1 計數沒有上限,而且自訂表情這個功能始終沒有明確的負責人。

GitHub 用 16 個分片扛下所有 pull request 與 issue,因為分散式查詢卡死了擴充

Lemaire 在 2024 年加入 GitHub 時,公司大部分仍跑在自己的機房,而且正面臨容量天花板。一位前 Rent the Runway 的同事告訴她,GitHub 必須同時搬到雲端、並整體減少工作量,才能換取成長空間。GitHub 的分片層 Vitesse 是包在 MySQL 外、支援動態重新分片的封裝,但用得很少:所有 pull request 和 issue 都放在 16 個分片上。

之所以沒加更多分片,是因為分散式查詢——不帶分片鍵的查詢,回傳前必須打遍每一個分片。分片越多,每次分散式查詢就越慢,而現有的分片早已過大、負載過重。Lemaire 的直覺是消滅分散式查詢,但她說在一個用 ActiveRecord 的 Rails 單體應用裡,這難如登天。

ActiveRecord 把資料模型藏得太好,工程師根本看不到自己產生了什麼 SQL。她在一個把 pull request 邏輯抽到獨立 Go 子系統的領域拆分專案裡與人結對,問一位熟悉 Rails 的同事:一個五行函式裡的 SQL 在哪?得到的回答是:「Mo,這可是百萬美元的問題。」她說,正是這種不可見性讓問題愈滾愈大——沒有人看得見新增一行程式的代價。

GitHub 的內部管理介面會顯示一個頁面背後所有的 MySQL 查詢、打到哪個叢集、延遲多少。在檔案消歧義頁(也就是帶 readme 的 repo 首頁),Lemaire 說那個介面顯示超過 100 條 MySQL 查詢,橫跨至少三個叢集。ActiveRecord 只會重用位元組完全相同的查詢的快取結果,所以使用者查詢只要差一點點就快取不中,而 GitHub 頁面上幾乎每一個計數都是即時算出來的。她主張,超過某個門檻的計數應該直接顯示「99+」,而不是真的去數。

flowchart TD
    A["查詢未帶分片鍵送達"] --> B["分散式查詢"]
    B --> C["打遍全部 16 個分片"]
    C --> D["回應變慢、負載沉重"]
    D --> E["團隊不敢再加分片"]
    E --> F["分片越長越大"]
    F --> D
    G["ActiveRecord 藏住產生的 SQL"] --> A

AI 有助於實作與基本監測,對效能調查幫不上忙

Lemaire 說,LLM 對她的效能最佳化「基本上完全沒幫上忙」,至少在調查階段是如此。一旦她知道該做什麼,實作是快了一些。她在 Cursor 處理首字延遲的工作,牽涉到上下文組裝、受成本與隱私限制的快取決策、推論代理(inference proxy),以及走 HTTP/2 的 gRPC 串流——同步與非同步步驟交錯,她得親手把它們畫出來。

她請 LLM 產出一張涵蓋整條首字延遲路徑的循序圖,在 Staples 印成藍圖紙,再自己對照程式碼逐項註記。她認為 LLM 擅長做基本的監測埋點,卻不擅長判斷非同步系統裡該埋哪些點,也不擅長 trace 的衛生管理:Cursor 有些 trace 包含數百萬個 span,而且取樣是在多處分別加上,而不是集中在一處——她說這是典型且代價高昂的錯誤。

「它對我的效能最佳化基本上完全沒幫上忙,至少調查那一段是如此。」

她最犀利的例子:LLM 建議把幾個延遲敏感的工作並行化。但最長的那個工作永遠是瓶頸,而且只有大約 1% 的使用者會觸發另外三個工作中的任何一個,所以這個改動只會增加負載,延遲一點也沒改善。真正有幫助的改動都很平淡,例如在合理範圍內調高 Redis 的 TTL。她也用變異測試(mutation testing)逼 LLM 寫出涵蓋邊界情況的測試,而不是隻寫出印證它剛剛那段程式的測試。

產品團隊與平臺團隊應該互相輪調,因為平臺現在就是那道護欄

對想投入效能與可靠度的工程師,Lemaire 的建議是先從產品團隊做起,察覺同一個問題反覆出現,再轉往能一次解決它的平臺團隊。風險在於漂移:平臺工程師會逐漸失去對產品開發實際感受的掌握,兩邊的落差越拉越大。

她的解法是雙向輪調——一個核心小組維護平臺,同時有產品團隊的人輪調進來。她把這比作嵌入式 SRE 模式。她說,在現階段,平臺護欄必須比以往任何時候都更堅固,因為產品團隊出貨 AI 生成程式碼的速度更快、審查卻更少。

「護欄必須比以往任何時候都更堅固,因為產品端的人……理想上應該可以愛怎麼亂噴就怎麼亂噴。」

她把同樣的邏輯推及 AI 生成的程式碼:LLM 傾向在既有基礎上硬加新東西,而不去質疑這個基礎該不該存在——而這正是她說平臺工程師必須不斷提出的問題。

背景:Lemaire 怎麼走到這一步

Lemaire 進麥基爾大學時主修微生物學與分子生物學,上了一門電腦科學導論後轉系,還叮囑媽媽別告訴爸爸——她父親教了 17 年電腦科學。她的第一份工作在 Rent the Runway,一部分是被技術長 Camille Fournier 吸引,一部分是對精品二手轉售的模式感興趣;她回去復職幾個月後 Fournier 就離職了,但一年後幫她牽線進了 Slack。她在 Slack 是企業效能團隊裡最資淺的一員,到職時產品即將正式上市,而 IBM 已經在試用了。

Slack、GitHub、Cursor 三段的共同主線是同一件事:為了出貨而做的短期決策,日後在負載下回頭檢視,以及從程式碼庫內部難以看清這些決策的代價。未解的張力在於:讓問題惡化的工具——生成更多程式碼、更多查詢的 AI 編碼代理——正是團隊被要求採用的工具。Lemaire 的答案不是要產品團隊慢下來,而是讓平臺層接住他們漏掉的東西,而這需要兩邊持續對話。輪調制度能不能在「出貨要更快」的壓力下存活,是這集節目留下來的開放問題。

來源: The Pragmatic Engineer (youtube.com)
開啟原文

讀者討論

理性交流 · 即時討論