Habitat:把線上儲存擴展到十億使用者(OpenAI,Part 1)
OpenAI Engineering|Jon Lee、Chaomin Yu、Ben Ries|2026-09-11 〈Rapidly scaling online storage to serve over 1 billion ChatGPT users〉 兩篇系列的第一篇。這篇講 Habitat 怎麼從一個 Python client library 變成獨立服務,以及他們怎麼把一個 Python 服務硬撐成儲存平台層。
關於圖片與影片
這篇沒有可匯入的圖檔或影片。 文中的 Figure 01–05 是 client-side 渲染的互動式圖表(JS 元件),不是 png/jpg/mp4,抓不到檔案 URL——wayback 快照裡只有其他文章的封面圖,這篇本文 0 張圖片、0 支影片。 我把五張圖各自的標題與它在講什麼寫進對應段落了,要看互動版請回原文錨點。 (另:openai.com 直連 403,wayback 快照的內文是
Loading…,全文是用 r.jina.ai reader 取得的。)
統整
規模數字
| 請求量 | 每秒 7,000 萬+(70M+ rps) |
| 使用者 | 每週 10 億+ 人 |
| 地理區域 | 將近 40 個 |
| 資料量 | 500 PB+ |
| 起點 | DevDay 2023,為了支援 GPTs,一個連到單一資料庫的 Python client library |
作者對「難度」的定位很特別:
在這個規模建置與營運基礎設施並不輕鬆,但也不特別困難。讓我們的處境獨特的是必須擴展的速度。
系統工程師通常為 10 倍規模而建,期待它撐個幾年,同時準備下一個 10 倍。我們的情況是:連續三年每年成長超過 10 倍。
結果就是整篇的基調——一連串戰術性決策與排序:把現有 stack 榨到極限,同時擋住儲存與運算的容量危機,替基礎性投資買時間。
演進的三段弧線:先可靠到能承接關鍵產品流量 → 再快到能服務全球使用者 → 最後能在巨大規模下靈巧營運。
Habitat 是什麼
Figure 01 · What is Habitat?(互動圖:Request/Response/Changes (CDC) 三條路徑)
出發點是一句話:產品工程師不該需要思考資料庫管理。
它幫你處理掉:schema 查找、路由、授權、加密、序列化、request shaping、連線池。產品工程師甚至不需要知道資料從哪來——Azure Cosmos DB、快取、或其他儲存。
Figure 02 · Habitat service(互動圖:簡化的 Habitat 請求流程,Request/Response)
值得注意的一句:這個 Python library 在沒有任何由上而下推動(沒有刻意要大家離開自助式 Postgres 和 Cosmos DB)的情況下,快速被產品工程師採用。
為什麼從 library 改成 service(2025 年中)
client-side 實作到了極限:Habitat 層變複雜、OpenAI 的服務數量變多,向後相容的協定變更變得不可行。
文章給了一個很具體的災難故事:
目標:把最關鍵的資料集遷移到「分區域分佈的 Cosmos DB 帳號」,縮小單一區域故障的爆炸半徑
↓ 需要在 client 加路由邏輯,藏在 feature flag 後面
↓ 要確保它推到所有 client → 跨數十個服務協調部署,花了「好幾天」
↓ 想先做 shadowing 驗證分片邏輯 → 又花了好幾天
↓ 發現有東西寫錯,修一個 bug → 又是好幾天
↓ 終於準備好要打開 flag
✗ 其中一個團隊因為「無關的原因」把服務回滾到先前那個有 bug 的 client
→ 造成了他們一路拚命想避免的那場故障
對 client library 的變更需要跨數十個服務的複雜協調,這個流程愈來愈脆弱、低效、易於營運失敗。
改成獨立服務買到兩樣東西:
- 單一控制點——部署、可觀測性、平台強化都集中做,立即惠及每個 OpenAI 產品,而不是管理零碎的更新。
- 單一安全咽喉——集中執行存取控制策略、稽核日誌、限制對底層儲存的存取。文章明講這是為了防止外部、內部、以及 agent 行為者的未授權存取。
用 Python 跑一個這種規模的服務
他們知道要做服務,但還不想離開 Python。代價是明確的:作為服務比作為本地 library 多了網路延遲,還有可觀的 CPU 與記憶體擴展成本;而且他們很清楚 Python 的低效在 100 倍規模下不可接受,重寫幾乎是必然的。
我們把這視為一次戰略性的技術債舉債(a strategic incursion of technical debt)。
當時的首要目標不是成本或資源最佳化,而是解除產品開發者的阻塞、達成平台穩定。接受短期的效能取捨,換到的是先處理更緊迫的挑戰、確立核心 API、建出穩固的基礎設施。
還有一個賭注:
我們做了一個經過計算的賭注:我們自己的 coding model 的快速進展會簡化未來的技術路徑。我們賭的是,等到必須完整遷移離開 Python 的時候,Codex 和 GPT 會讓那次遷移變得可行。那個賭注最終證明是對的。
Python 服務的主戰場是尾延遲:
當平均一個使用者請求會產生數百次資料庫呼叫時,使用者感受到的是最慢的那一次。
追蹤 asyncio 延遲
Figure 03 · Tracking the asyncio delay(互動圖:Concurrency is not CPU parallelism——asyncio 允許併發處理請求,但同一時間 CPU thread 上只有一個請求在執行;CPU 工作多的時候對延遲影響極大)
asyncio 幫 Python 併發處理 I/O-bound 工作,但繞不過 GIL,也不提供 CPU 平行。而 Habitat 除了 I/O 密集的請求代理,還扛著一堆 CPU 密集的職責與背景任務:路由、壓縮、加密、checksum、下游健康檢查、request shadowing、hedging。
症狀很反直覺:
在 p99 以上的請求 trace 裡,下游儲存回應得很快,但請求卡在等待負責的 coroutine 被重新排程去解析那個回應。
他們的做法與結論:
- 除了 CPU/記憶體/網路/磁碟的標準利用率與飽和度指標,必須監控 asyncio loop 有多忙。
- 量法:週期性排一個背景任務,記錄「預期執行時間」與「實際執行時間」的差,就能即時量出事件迴圈的排程延遲。
- 在高利用率下,即使每個 process 只有不多的併發請求,也足以產生數百毫秒、極端情況下數秒的排程抖動。
- 所以:讓每個 process 只服務少量併發請求,改成大量橫向擴展 Python worker process 的數量。
一個藏在 feature flag 設定裡的尾延遲
用線上 CPU profiling 抓到的 root cause:週期性解析 Statsig 的 feature flag 設定 JSON。
三個因素疊在一起才炸:
| 因素 | 細節 |
|---|---|
| Statsig 預設 | 每分鐘輪詢刷新設定,沒有 jitter |
| 設定內容 | 包含所有服務的所有 production rule,是一個巨大的檔案 |
| 另一個架構決策 | 每個 pod 跑最多 8 個 Python process(為了推高 CPU 使用率、降低延遲) |
合起來就是:每一分鐘,每個 pod 都會有一個瞬間,它所有的 worker 同時停下手上的請求,去解析一個巨大的設定檔。
修法(一旦 profiling 定位到就很直接):部署更小的針對性設定、拉長刷新間隔、給這類背景任務加上 jitter。
負載平衡與連線池:一個 metastable failure
Figure 04A · Client-side connection pooling — LIFO sends new work back to the slow process Figure 04B · Client-side connection pooling — FIFO breaks the connection-reuse feedback loop
問題的形狀:client-side 連線池下,一個發出大量併發請求的 client process 可能只建立了少數幾條 server 連線,於是把所有負載送給少數幾個 process。修正之前,尾端的 process 服務的併發請求數是平均值的 5–10 倍。
發現的經過是個意外:
儘管已經停掉那個壓垮部分服務的 client,一部分 process 在突發流量結束後仍然持續劣化。事實上那些 process 出現了失控式劣化——收到愈來愈多請求,直到我們重啟它們為止。
一旦某個 pod 過載,某種行為會把更多流量釘在那個已經過載的 pod 上。文章點名這是 metastable failure,並連到 Meta 那篇經典的 link imbalance 文章。
根因:
Python 的 aiohttp
TCPConnector預設使用 LIFO 連線重用——最近被歸還的連線會被下一個請求選中。
這在平常是合理的預設(讓突發流量多建的連線能 idle timeout 掉,減少維護額外連線的開銷)。但在這裡:
突發流量期間
→ 比較慢、已過載的 server 比較晚把連線還回池子
→ 於是它們的連線是「最近歸還的」
→ 因此更常被後續請求選中
→ 流量逐漸集中到本來就在掙扎的 pod 上 ← 正回饋迴圈
修法:把連線池改成 FIFO 重用,打斷回饋迴圈,而且連穩態下的請求變異數也一起降低了。
今天他們主要靠 Istio 和 Envoy 提供連線池與更好的、感知伺服器負載的平衡策略,從根本避開這個問題。
避免淹沒下游(thundering herd)
Figure 05 · Connection fan-in — The same requests, fewer connections(互動圖:對照三種情況——Python 直連 18 條連線只有 6 條在忙/Envoy 池化 HTTP/1 降到 6 條/Envoy 升級 HTTP/2 後 1 條連線承載 6 個併發 stream)
「調低 asyncio 延遲」+「非常多 Python process」的副作用,就是很容易用大量連線壓垮下游:
- 一次沒有調慢的日常部署,就會因為連線循環造成顯著的 CPU churn
- 一個連線洩漏可以靠把 NAT gateway 塞爆來搞掉整個網路
這些對別的服務也不是罕見問題,但因為 process 數量多了一個數量級,觸發門檻被大幅拉低——會把「客戶端本來不預期在穩態下需要處理」的網路資源直接塞滿。
他們用 Envoy 最大化連線 fan-in:把 Python 的 HTTP/1 連線升級成 HTTP/2 以利用多工,再池化並延長連線壽命。Envoy 同時是實作速率限制與 circuit breaker 的集中點——這些東西放在每個獨立的 Python process 裡效果會差很多。
為什麼 Habitat「做得更少」
這一節是全篇最可遷移的部分。
Python 能撐這麼遠的一個原因,是 Habitat 受限的 API 讓請求成本可預測。
它不允許 client 構造任意 SQL(那會導致大表掃描或跨多表 join),只暴露一個簡單的 NoSQL API:
缺乏強大的 API 是 Habitat 設計中一個明確的取捨(an explicit tradeoff)。
理由:
- 優化目標是簡單、可預測、固定工作量(constant-work)的請求——這類系統明顯更容易擴展,也更難被寫錯或誤用。
- 具有不可預測 fanout 的請求在營運上是危險的:它們讓隔離變複雜、讓負載平衡變複雜,並引入難以擴展的延遲懸崖(latency cliffs)。
歷史教訓(這段是整篇最值得記的因果):
Habitat 之前,OpenAI 大部分線上資料在 Postgres
→ 團隊小的時候,可以人工 review 每一次 query 與 schema 變更,
確保它們行為良好、跑在有索引的資料上
→ 團隊和產品長大之後,這很快變得無法管理
→ 「單一一條昂貴的新查詢出現在熱路徑上,就把資料庫搞掛」成為故障的常見原因
問題在於成本不對稱(cost imbalance):寫出一條昂貴、難以執行的 SQL 查詢,是既便宜又容易的。
Habitat 的對策是讓昂貴的查詢在 client 端變得極其顯眼:沒有無界查詢能壓垮 Habitat;複雜的 join 和圖遍歷要求產品團隊自己做一部分重活,而這反過來讓整體設計往更有效率的方向走。
資料模型:受 TAO 啟發
API 建立在 client 自定義的 object 與 edge 型別之上,靈感來自 Facebook 的 TAO。Client 預先定義 object、edge、以及它們如何互相關聯,但不定義每個型別的內容。
結果的關係看起來像一張圖,但 Habitat 本身不支援典型的圖遍歷查詢,只能查某個 object 的直接 edge。
分區策略有個重要的邊界:
我們把這張圖分區,讓每個 object 和它對應的 edge 共置在同一個儲存層分區裡;但我們不特意去共置 object 和它的 edge 所指向的遠端 object。
後果是雙面的:模型很容易為水平擴展而分區,但圖遍歷是低效的——object 之間的任何一跳都可能需要從兩個完全不同、位於不同區域的 Cosmos DB 帳號抓取。
逃生閥:Rockset + CDC
對於有複雜查詢需求的 client,提供一個離線的次級檢視,用 change data capture (CDC) 近即時地把線上儲存的變更串流到獨立的 Rockset 實例。每個 client team 自己負責擴展自己的 Rockset 實例。
這個配置流程為 client 帶來額外的摩擦,但我們認為在現階段是正確的取捨:讓簡單查詢成為預設,同時為需要複雜查詢的人提供逃生閥。 這個設計把線上儲存和讀取密集的分析與搜尋工作負載隔離開來。
從 Python 遷移到 Rust
把 Python 重寫推遲一年,讓我們能在超高速成長期專注於更緊急、更有影響力的挑戰。
遷移的時機條件:平台成熟、成長持續加速,而且 Habitat 已經是 OpenAI 內部按核心數計第二大的服務(Envoy 佔用量第四)。
| Python 的巔峰 | 每秒服務超過 2,000 萬請求 |
| 重寫時間與人力 | 2026 Q2,只用 2 位工程師 + Codex + GPT-5.5,重寫了整個服務 |
| 現況 | Rust 服務處理 95% 的生產請求,Python 將在數週內完全退役 |
| 效能 | CPU 效率 6 倍、記憶體效率 15 倍,平均與尾延遲都顯著更低 |
Part II 預告
儲存層本身:多租戶可靠性、分層的讀取效能優化策略、以及他們怎麼擴展與 Azure Cosmos DB 的合作關係。
反覆出現的關鍵字
- 尾延遲(tail latency / p99):整篇的反派。「使用者感受到的是最慢的那一次資料庫呼叫」定調了後面每一個子節——asyncio 延遲、feature flag 卡頓、LIFO 連線池,全部是尾延遲的成因。
- 可預測 / constant work / 不可預測的 fanout:「做得更少」那一節的核心詞。可預測性本身被當成一個一級的設計目標,而不是效能的副產品。
- 買時間(buy time)/戰術性決策/strategic technical debt:文章對自己歷史的敘事框架。Python 不是錯誤,是被自覺選擇的、有還款計畫的債。
- 10x:唯一被拿來定義處境的數字。不是「規模多大」而是「成長多快」。
- explicit tradeoff:出現在 API 設計、Rockset 摩擦、Python 選型三處。作者反覆強調這些不是妥協,是選擇。
- Envoy / Istio:三個不同的問題(連線池平衡、連線 fan-in、速率限制與 circuit breaker)最後都收斂到同一個答案——把跨 process 的問題搬到 sidecar 層解決。
矛盾點
文章內部
- 「並不輕鬆,但也不特別困難」是同一句話裡的自我矛盾。(推論)他的意思應該是「這個規模本身業界已有解法,難的是速度」,但字面讀起來像在貶低自己前面說的事。這個修辭遮住了一件事:他們遇到的具體問題(asyncio 排程、LIFO 池、thundering herd)沒有一個是「規模大」造成的,全部是「用 Python 跑高吞吐服務」造成的。
- 數字要小心引用。 文章說 70M+ rps,但同一篇的 Python 巔峰是 20M+ rps,而 RSS 摘要寫的是 22M rps。(推論)三個數字指的是不同東西(現況總量/Python 歷史峰值/摘要當時的數字),拆開看都對,但混著引用會錯得很難看。
- 「賭 coding model 會讓遷移變可行,而那個賭注證明是對的」缺少反事實。(推論)2 位工程師一季重寫完是事實,但沒有人知道沒有 Codex 的 2 位工程師要多久。文章把「做到了」當成「賭對了」的證明,而這兩件事之間還缺一個對照組。
- 「戰略性技術債」的框架不完全撐得住那幾個事故。(推論)Statsig 沒有 jitter、aiohttp 預設 LIFO、部署太快造成連線 churn——這些預設值本身跟 Python 無關,任何語言都會踩。Python 的角色是放大器(因為 GIL 逼他們跑一個數量級更多的 process,觸發門檻被拉低),不是根因。把這些全記在「Python 這筆債」的帳上,會讓人以為換語言就不會遇到。
- 「Habitat 做得更少」的另一面是「別人做得更多」。(推論)複雜的 join 和圖遍歷「要求產品團隊自己做一部分重活」,Rockset 則是「每個 client team 自己負責擴展自己的實例」。複雜度沒有消失,它從平台團隊移到了產品團隊。文章承認了摩擦,但用「現階段正確的取捨」帶過,沒有算這筆帳落在誰身上。
與 vault 既有筆記
- Key-Value 的限制是為了換分片而付的價 在這裡拿到一個十億使用者等級的實證。 那篇的主張是「限制不是缺陷,是換擴展性付的價」;Habitat 明講**「缺乏強大的 API 是一個明確的取捨」**,而且理由一模一樣:可預測、易分片、難以誤用。(推論)Habitat 還多給了那篇一個延伸——不只是「不准做」,而是「讓昂貴的操作在呼叫端顯而易見」,把成本從隱性變顯性。
- 「資料庫撐不住」不是一個問題,是三個 被完整示範了一次。 Habitat 用 CDC 把分析型查詢導去 Rockset,正是那篇說的「分析查詢太慢 → 送去 OLAP,別在 OLTP 表上硬掃」。(推論)值得補進那篇的一句:隔離的手段不只是換儲存,還可以是「在 API 層就不讓那種查詢存在」。
- JOIN 貴在每一列都要去另一張表配對 的組織層版本。 那篇講的是單一查詢的成本模型;文章的**「寫起來便宜、跑起來昂貴」是同一件事在團隊規模上的後果**——小團隊靠 code review 擋得住,團隊一大就擋不住,於是只能從 API 層拿掉那個能力。(推論)這是那篇可以延伸的一個層次:成本不對稱的危險程度和團隊人數成正比。
- Document DB 快在局部性:把該一起用的東西實際存在一起 需要補一個邊界。 Habitat 把 object 和它的 edge 共置,但刻意不共置 edge 指向的遠端 object——所以局部性是有半徑的,越過那個半徑就掉回跨區抓取。(推論)那篇現在寫得像「擺在一起」是沒有代價的二元選擇,實際上局部性的範圍本身就是一個要設計的參數。
- 技術債 1 需要修正。 那篇寫「技術債是一切不好的或次好的決定」。Habitat 是一個刻意選擇次好方案來買時間、而且事後證明是對的案例。(推論)該補的判準是:技術債的好壞不在當下的決定好不好,而在它是不是被自覺地選擇、有沒有明確的償還條件。 文章甚至寫出了還款觸發條件(「100 倍規模下不可接受」)。
- 規模型功能要有規模型測試 在這裡看到另一條路。(推論)那篇的主張是「宣稱撐 N 就要測到接近 N」。Habitat 選的是另一個方向:讓 API 根本無法表達那種會爆的請求。兩者不衝突,但排序值得記——能用型別/API 排除的,就不要靠測試去驗證。
- 監督 AI 產出的檢查成本可能比自己寫還高 在這裡遇到一個反例場景。(推論)2 位工程師 + Codex + GPT-5.5 重寫整個服務之所以可行,很可能是因為這個任務的正確性判準異常明確:同一組 API、同一批生產流量可以影子比對、效能數字可以量。這正好呼應 Jev 不生成文字,只回傳帶機率的決策 那組筆記的主線——可驗證性決定了 AI 協作的成本。
待確認
- 70M / 20M / 22M 三個數字各自的定義與時間點,文章沒有並排說明。
- 「按核心數計第二大的服務」——那第一大是什麼?這會說明 OpenAI 的運算重心在哪。
- Rust 的 6x CPU / 15x memory 是在同一份工作負載、同一組 API 下的對照嗎?文章沒有說明測量方法。
- 「2 位工程師一季重寫完」有沒有包含測試、影子比對、漸進式切流的時間?95% 切流是在同一季完成的嗎?
- Part II(儲存層、多租戶、Cosmos DB)還沒出。
- 文中提到的 hedging(對同一請求發出備援呼叫)只出現在 CPU 密集工作的列表裡,沒有展開——他們的 hedging 策略長什麼樣?