所有 OpenAI 產品都需要快速、可靠地存取資料,而這項能力由我們的儲存平台 Habitat 提供。Habitat 必須以毫秒級延遲,回應分布於 40 個地理區域的逾 10 億名用戶。我們的基礎設施面臨前所未有的挑戰:在支援爆炸式增長和新使用情境的同時,持續提升可靠性、效能及產品開發速度。為解決這項挑戰,我們必須重新思考線上儲存平台的設計和運作方式。我們不斷評估取捨,遵循以下次序:先確保系統足夠可靠,能承載關鍵產品流量;再確保速度足夠快,能服務全球用戶;最後實現超大規模下的靈活運作。
這個分為兩部分的系列,會介紹我們如何將 Habitat 從簡單的用戶端程式庫和資料庫,發展成提供逾 500 PB 資料存取服務的全球分散式儲存平台。第一部分着重介紹 Habitat 如何從 Python 用戶端演變成服務。當中亦介紹了我們採用的技術,如何讓以 Python 建立的服務突破許多人認為可行的極限。
本文是系列的第二部分,將詳述我們如何擴展底層儲存元件 Azure Cosmos DB。透過與 Azure 團隊緊密合作,我們確立了共同管理容器的多租戶運作方式,既切實可行,也能在全球超大規模部署下提供出色的讀取效能。
圖 01 · Habitat 是甚麼?
線上儲存平台
Habitat 是我們建立的線上儲存平台,讓 OpenAI 產品快速可靠地存取所需資料。
- 請求
- 回應
- 變更(CDC)
隨着公司規模擴大,基礎設施團隊往往會從運行單租戶系統,轉向多租戶系統。單租戶架構由負責各產品功能的團隊自行維護系統,能縮小故障影響範圍,清楚劃分權責,並輕易隔離工作負載。隨着團隊和產品功能增加,這種架構的運作成本也會變得高昂。單租戶架構要求每個產品團隊自行負責容量規劃,往往導致大量過度佈建。
相反,多租戶架構將控制權交給中央資料庫團隊,讓具備更豐富知識和經驗的團隊負責資源佈建。但多租戶架構也有自身的問題,例如「吵鬧鄰居」:某個租戶佔用異常大量的資源,影響其他租戶。
設計 Habitat 時,我們必須權衡這些取捨,才能建立可靠、高效的大規模儲存系統。如今,Habitat 是許多 OpenAI 產品共用的多租戶資料庫層。但我們走過一段繞回原點的歷程:從使用單一多租戶 Azure Cosmos DB 容器,到佈建單租戶後端容器,再回到預設使用多租戶容器。Habitat 的多租戶發展仍處於很早期的階段。我們支援大量使用情境,但透過盡可能利用 Azure Cosmos DB 的內建基礎功能,得以延後在這方面的投入。
在介紹我們使用 Azure Cosmos DB 的歷程之前,先說明一些基本概念:
Azure Cosmos DB 帳戶定義了一組容器及一項複寫原則
每個容器都會佈建一定的吞吐量,以每秒請求單位(RU/s)計算
每個容器內的資料採用常見的分片方式,按指定的邏輯分割區索引鍵儲存;文件列明了每個邏輯分割區索引鍵的儲存及吞吐量限制
Azure Cosmos DB 會將邏輯分割區索引鍵分片至數量較少的底層實體分割區。當儲存量超過門檻,或容器需要更多實體分割區來支撐已佈建的吞吐量時,系統便會將其分拆成多個實體分割區
最初,我們允許任何用戶端負載通過,依靠文件所述每個邏輯分割區索引鍵 1 萬 RU/s 的上限來提供背壓。剛起步時,我們在每個帳戶中也只使用一個預設的 Azure Cosmos DB 容器。這種做法起初尚算可行,但後來因幾個原因而出現問題:
由於早期的 API 設計選擇,我們直接讓用戶端選擇邏輯分割區索引鍵。結果,不同產品使用情境經常使用相同的常用分割區索引鍵,例如 user_id 或 workspace_id,造成衝突。由於這些使用情境共用相同邏輯分割區索引鍵的 Azure Cosmos DB 額度,這種設計限制了個別使用者可用的總吞吐量。
在轉用 Habitat 和 Azure Cosmos DB 之前,我們擴展 Postgres 時經常遇到一個問題:來自負載極高的組織或熱點索引鍵的大量請求,也就是某個租戶產生不成比例的高負載,足以令整個資料庫癱瘓。我們投入大量協作,為這些索引鍵建立快取,並在快取未命中處理流程中加入租約機制,避免未命中所產生的負載壓垮底層資料庫。
Figure 02a · Logical partition key limits
Different clients, the same partition key
文件列明的資料大小和吞吐量限制雖然適用於邏輯分割區索引鍵,但實際執行限制的是實體分割區。實際上,這代表邏輯分割區無法達到 10,000 RU/s。它們必須與同一實體分割區內的所有其他邏輯分割區索引鍵,共用該實體分割區的額度。
這讓「吵鬧鄰居」得以影響許多互不相關的邏輯分割區索引鍵:處理小眾產品功能資料的工作負載,竟會干擾核心的登入及註冊流程。
Figure 02b · Noisy neighbors
Different users, the same physical partition
個別實體分割區的吞吐量上限也會變動,取決於分配給 Azure Cosmos DB 容器的總吞吐量。容器佈建的吞吐量會大致平均分配給該容器的各個實體分割區。
隨着資料量增加,實體分割區會分拆,分割區總數因而增加,但佈建的總吞吐量保持不變。結果是資料量越大,每個分割區的實際吞吐量上限便越低。
對於最佔儲存空間、類似大型二進位物件(blob)的使用情境,這會產生一種故障模式:即使請求速率和使用量保持穩定,單是儲存資料量增加,也會令正在運行的工作負載可用的吞吐量下降。
Figure 02C · Weak partitions
The limit falls below demand
A partition splits when it reaches its storage limit. Each child receives half the throughput capacity.
使用獨立容器,有助部分解決這些分割區索引鍵限制和吵鬧鄰居問題。採用獨立容器後,每個容器都可獲分配專屬的實體分割區。只有同一容器內的不同工作負載,才可能產生吵鬧鄰居問題,或出現分割區索引鍵空間重疊。缺點是佈建新容器並非沒有額外成本:每個容器都需要獨立規劃容量。因此,我們希望只在合理的服務或使用情境邊界上建立容器。
我們開始為最關鍵、需要強隔離的新使用情境,採用獨立的 Azure Cosmos DB 容器。同時,我們利用指令碼,按各容器的實體分割區數目同步調整其佈建總吞吐量,以處理分割區吞吐量不足的問題。兩者配合,基本上避開了當時面對的所有問題。
然而,我們最初使用的是預設容器,因此許多既有的關鍵使用情境若不遷移資料,便只能繼續留在該容器中。為保護仍然依賴共用預設容器內資料的關鍵服務,我們經常建議團隊為新服務或主要產品功能建立獨立容器。隨着規模擴大,預設容器的壓力日增,正式環境事故也越來越多。我們開始為每項新服務或大型產品功能佈建容器,純粹是為了避免預設容器承受更多負載。
這讓我們避開了多次全站服務中斷,將影響範圍限制在個別功能或服務效能下降及中斷。但佈建新容器的額外負擔,最終還是成了問題:
在以往的共用預設容器中,任何一個新使用情境即使從 0 QPS 增至 100 萬 QPS,也不太可能令總使用量出現顯著變化。但在獨立容器中,新使用情境從 0 增至 100 萬 QPS,負載可能是僅供內部試用時基準值的 1,000 倍。
使用專用容器後,團隊必須記得在推出產品前,按預期吞吐量預先佈建容器。產品團隊往往將功能部署至正式環境,卻忘記預先擴容。結果我們在上線當天,只能在重大事故應變室內,臨時嘗試擴充容器。
混合工作負載是多租戶系統的一項核心優勢;採用這種容器策略後,我們為了取得更好的隔離效果,犧牲了這項優勢。
Figure 03 · Lack of stable aggregate growth
The same workload, different headroom
1K RU/s during load testing → 1M RU/s at launch
隨着時間推移,要監察專用容器的使用量,也需要深入了解產品,知道流量何時會轉變或增長。
我們可以提供基本的控制平面指令碼,根據增長趨勢提高各容器佈建的吞吐量。但缺乏穩定的整體增長後,要正確擴容,幾乎 OpenAI 每個產品團隊都得參與容量規劃。
這種做法無法隨組織規模擴大而持續運作,因為各服務負責團隊對 Azure Cosmos DB 佈建工作的重視和細緻程度相差甚遠。
隨着組織成長,這些營運問題越來越難以應付。沒有單一中央團隊統籌規劃,導致許多團隊規劃不周。因此,我們最終轉回多租戶方案,並與 Azure 團隊緊密合作,採用共享租戶設計,從工程層面解決最初的多租戶問題:
優先對存取最頻繁的索引鍵限流,可防止吵鬧鄰居耗盡某個實體分割區的可用吞吐量,減少單一熱點索引鍵波及全站的吵鬧鄰居效應。
請求分級 QoS 同樣能防止離線工作負載,例如大量插入或資料擷取,耗盡某個實體分割區的吞吐量。我們為這些工作負載的請求設定較低優先級,讓它們先受到限流,避免直接服務終端使用者、優先級較高的互動式請求被限流。
Azure Cosmos DB 原本已有無伺服器模式,可以解決我們在佈建模式下遇到的分割區吞吐量不足問題。但它有一大缺點:只按儲存使用量擴展,而且每個實體分割區的 RU 使用量上限為 5,000 RU/s。這雖然能解決分割區吞吐量不足的問題,卻會妨礙我們擴展 RU 用量大的使用情境。
Azure Cosmos DB 團隊為我們開發了改良版無伺服器模式,讓每個實體分割區都能擴展至 1 萬 RU/s。除了按儲存使用量分拆外,新模式也支援按 RU 使用量分拆,讓我們不再需要手動監察和擴充容器。
有了這些改動,我們得以重新使用單一共用容器,在維持工作負載隔離的同時,大幅簡化產品團隊在前期及持續容量規劃方面的工作。此後數月,即使產品開發持續加速,我們也沒有再遇到任何佈建或吵鬧鄰居事故。
當公司成長至 OpenAI 的規模,託管服務便不再能完全「託管」。許多工作負載會超出雲端供應商基礎設施的常規範圍。隨着 OpenAI 成為 Azure Cosmos DB 最大的使用者之一,要順利運作,就需要另一套模式:共用測試、共用儀表板、直接的工程溝通渠道,以及共同負責處理只會在我們這種規模下出現的故障模式。如今,我們最大的 Azure Cosmos DB 帳戶儲存了數十 PB 的資料,每秒處理超過 2,000 萬次查詢。據我們所知,我們是唯一以這種規模部署 Azure Cosmos DB 的公司。
2024 年底的一次區域性服務中斷,清楚顯示了這種協作的必要性。當時,我們其中一個最大 Azure Cosmos DB 帳戶所在的主要 Azure 區域發生停電。在隨後的容錯移轉過程中,我們才發現最大的幾個帳戶需要數小時才能切換至次要區域。我們和 Azure Cosmos DB 團隊都未曾在這種規模下測試過這項操作。
約一個月後,我們將這些帳戶切換回原來的區域時,又觸發了另一次嚴重服務中斷。大量連線同時湧向原來的區域,壓垮了該區域的 Azure Cosmos DB 閘道。閘道模式下的 Azure Cosmos DB 用戶端,在處理容錯移轉時有一項特殊行為:即使只有部分實體分割區移回原來的區域,所有用戶端也會立即向該區域建立連線並傳送請求。
每次服務中斷時,OpenAI 和 Azure 都依照各自的事故管理流程處理,找出多項根本原因並部署修正。但這些服務中斷只是表徵,背後是我們與 Azure Cosmos DB 團隊在運作及測試方式上的更大系統性問題。起初,使用託管服務讓我們能快速推進,專注於業務目標。但規模擴大後,託管服務便不再能完全交由供應商管理;從設計到營運,各個層面都需要與服務供應商深度整合。
如今,Habitat 和 Azure 工程團隊共同監察 Habitat 的每一層,從我們的服務層一直到底層 Azure Cosmos DB 的實體儲存層。我們會在一個規模與正式環境相同、但不屬於正式環境的 Azure Cosmos DB 帳戶中運行及測試變更。我們共同測試和迭代 SDK,並確立 Azure Cosmos DB 帳戶擴展及容量配置的最佳實務。雙方工程團隊可透過 Teams 直接聯絡,甚至設有直接呼叫對方值班人員的方式,無須經過逐級支援流程。
過去一年,我們雙方投入大量協作,提升 Azure Cosmos DB 控制平面和閘道的擴展能力。工作包括在閘道加入負載保護機制,並快取各類 Azure Cosmos DB 中繼資料,以保護其中繼資料分割區;透過連線集區及提高連線重用率,減輕大量 Python habitat-service 程序帶來的壓力;盡可能將工作負載部署在同一位置;以及透過不同的 VPC,將關鍵工作負載與離線工作負載隔離。
這種程度的聯合工程協作,對過去一年 Azure Cosmos DB 的穩定運作至關重要。任何規模夠大或重要性夠高的系統,都需要這種團隊合作:打破權責壁壘,跨越組織界線協作。
如第一部分所述,我們的核心理念是:先讓系統運作,再加快速度,最後降低成本。先確保可靠性,再提升效能,最後改善效率。Habitat 已成為 OpenAI 的關鍵基礎設施,因此首要任務是確保其可靠性。2025 年,隨着系統逐漸穩定,以及 OpenAI 向全球擴展,延遲成為使用者和產品團隊面對的下一個瓶頸。
運算與儲存容量往往位於不同區域,因此無法避免跨區域請求。
Figure 04 · Improving performance at a global scale
Global traffic
Compute and storage capacity were often located in different regions, making cross-region requests unavoidable.
運算與儲存的地理位置僅供示意。
資料的儲存位置和請求的處理位置不同,延遲也相差甚遠。為提升 Habitat 請求的效能,產品開發人員需額外加入用戶端快取、批次處理請求、盡量減少資料庫讀寫,並盡可能將工作移出關鍵路徑。這雖然促成了一些有趣的產品設計,讓產品更能抵禦資料庫問題,卻也令產品更複雜,拖慢迭代速度,未能達成 Habitat 的目標:讓開發人員無須了解資料庫,也能快速開發產品。
這是我們在 2026 年的一大工作重點。我們已制定分層策略,優化佔 Habitat 請求逾 90% 的讀取操作效能:
我們在全球不同 Azure 區域設有多個 Azure Cosmos DB 帳戶,供有資料駐留要求的使用者使用。即使使用者沒有嚴格的資料駐留要求,將資料部署在使用者附近,也能透過降低延遲改善體驗。
實際上,除了有資料駐留要求的使用情境外,我們尚未能廣泛採用這種做法,因為資料的放置位置受到雲端實體容量限制。我們的資料集規模龐大,許多區域若要按其使用者流量佔比分擔資料,所需的龐大儲存空間會超出當地容量。
在資料中心容量、電力或冷卻能力有限的區域,我們有時需要直接作出取捨:預留容量在當地儲存資料,以降低 Habitat 延遲;或把這些容量用作提供服務所需的額外運算資源,並將儲存請求路由至更遠的地方。
因此,資料放置決策往往是產品層面的取捨,需要平衡:A)不同產品和使用者群組的延遲優化,以及 B)實際可用的儲存與運算容量。
對延遲要求最嚴格的工作負載,例如每個請求都需要的身分驗證中繼資料,我們使用複寫至多個次要區域的 Azure Cosmos DB 帳戶,以縮短運算端與 Habitat 之間的來回時間(RTT)。部分帳戶會將資料複寫至我們處理流量的全部 40 個 Azure 區域;其他帳戶則至少複寫至每個洲的一個區域。
我們不得不比原本希望的時間更早,在 Habitat 中建立對使用方透明的資料移動及路由層,以便在複寫拓撲不同的帳戶之間移動資料集。
不過,這種做法最終仍受容量限制,因此只能用於少數經仔細挑選的資料集。
Habitat 的工作負載絕大多數以讀取為主,應用程式不斷取得設定、狀態及其他屬性;由常用資料類型構成的工作集雖然相對較小,卻產生了佔比特別高的流量。因此,這個系統特別適合使用分散式快取。快取能改善單一區域基礎設施的效能;跨區域運作時,效益更會疊加,因為快取可以「隱藏」跨區域延遲。
我們已在 Habitat 中整合分散式旁路快取層。這一層會將最近存取的物件和邊的副本儲存在 Valkey 伺服器中,而這些伺服器部署於我們的基礎設施內,靠近處理使用者請求的位置。這類快取通常會設定存留時間(TTL),在命中率與資料過時程度之間取捨。較短的 TTL 可減少資料過時情況,但也會削弱快取的效益,因為項目很快便過期,需要更頻繁地重新擷取。較長的 TTL 能提高命中率,卻會長時間提供過時資料,導致使用者體驗欠佳,產品行為難以預測。
另一種做法是,當應用程式寫入同一份底層資料時,明確執行快取失效操作。這能大幅提升快取資料的時效性,但要正確實作並不容易。而且仍會受到多種邊界情況影響,例如部分操作失敗,導致快取保留過時資料。多區域部署會加劇這些問題:跨區域網絡的可靠性較低,快取操作與底層儲存的複寫也會出現競爭條件。配合 TTL 可限制資料過時的最長時間,但仍然避不開上述 TTL 的根本取捨。
我們借鑑 Meta 的 TAO 等系統,採用了更進階的快取失效處理方式。Habitat 的整合式快取,透過 Azure Cosmos DB 變更摘要進行變更資料擷取(CDC),在數秒內將失效通知傳播至全球,兼顧高命中率和低資料過時程度。我們基礎設施內的一項服務會讀取這個變更摘要,並在底層資料更新時,使本地快取中的相關資料失效。因此,下次讀取該資料時便會發生快取未命中,並以最新版本的資料重新填入快取。
我們亦以 Rust 重寫原有的 Python 程式碼,透過效能更高的實作減少 Habitat 服務本身的額外開銷,從而大幅降低延遲。
這些改動合起來,將我們全球整體讀取延遲 p50 從約 120 毫秒降至約 10 毫秒,即降至原來的約十二分之一。這是我們自今年較早時開始着重改善效能以來取得的成果。
Figure 05 · Improving performance at a global scale
p50 latency for reads
Three major design changes improved read latency
快取雖然大幅降低了延遲中位數,但最慢的請求仍需從底層儲存取得未快取的資料,而且往往要跨越相距甚遠的區域。隨着更多容量投入使用,以及儲存效率提升,我們計劃解決一個更根本的挑戰:從一開始,就將資料放在更接近其存取來源的位置。
可靠且可擴展的 CDC,負責將資料庫變更傳播至下游系統;它不只是 Habitat 快取策略的關鍵元件。系統每秒從數千個容器、數百萬個容器分割區擷取數百萬項變更,經由逾百個 Kafka 主題傳送,同時支援具最終一致性的次要索引、資料移動管道、離線分析及其他即時產品功能。隨着底層基礎設施擴展至全球,監聽資料列變更也變得越來越複雜。如果用戶端想註冊回呼函式,在所屬資料類型的資料列變更時執行,就必須先找出資料位於哪些 Cosmos 帳戶和容器、各區域由哪個 CDC 服務部署監聽這些容器,並適當擴展 CDC 服務的工作程序群、Kafka 及自身的取用端等。我們的 CDC 平台旨在讓產品開發人員無須處理這些細節,只需透過簡單介面,註冊希望在資料列變更時呼叫的函式;所有底層整合和擴展工作都由我們負責。
內部控制平面會持續確保所有必要容器的變更都被擷取,選擇要從哪些 Cosmos 區域讀取,並在多個地理區域的數千個工作程序之間高效分配工作。透過使用複本區域及較低優先級的變更摘要讀取,我們能保護對延遲敏感的 Habitat 流量,減少背景擷取工作負載造成的干擾。在如此龐大的工作程序群中,工作程序崩潰、容器問題、租約儲存錯誤、無效設定及目的端故障,都會不斷發生。我們強化了 CDC 工作程序生命週期及租約處理的穩健性,隔離不同串流之間的故障,並為設定變更和推出流程建立防護機制。我們使用死信佇列,避免永久傳遞失敗造成隊頭阻塞。我們會將讀取程序切換至其他 Cosmos 區域,以承受區域性服務中斷。在我們的規模下,甚至需要修改 Azure 的 CDC SDK,將檢查點先在記憶體中批次彙集,再定期寫出,以提高檢查點實作的效率。
Habitat 內部使用 Azure Cosmos DB 的方式雖然複雜,但其核心價值一直是讓產品開發人員無須處理基礎設施細節,從而加快產品開發;我們的 CDC 平台也不例外。
透過刻意精簡儲存 API 的功能範圍、持續擴展 Python 服務,並優先解決最迫切的工程挑戰,一個小型團隊便能支撐快速成長的儲存平台。我們先確保關鍵產品流量的可靠性,再改善全球使用者的讀取效能,最後提升大規模運作下的營運效率。這種做法讓我們能支援快速增長和新使用情境,同時提升可靠性、效能及產品開發速度。
為了擴展 Habitat 和對話儲存,為下一批 10 億名用戶提供服務,我們需要解決一項難題:如何在更大規模下,將資料複寫至整個基礎設施。為提前應對,我們一直在內部自行開發 EB 級線上儲存系統,專門針對 Habitat 體積龐大的資料類型作出優化。這套仍在開發中的系統,結合了 Rockset 的 RocksDB Cloud 核心與基於 Raft 的複寫機制,採用與 Azure Cosmos DB 相同的文件資料模型,但利用解耦式儲存和實體複寫,減少所需的快閃儲存容量,代價是較低的峰值 QPS。
如果你有興趣參與這類在前沿規模下進行的資料基礎設施工程,歡迎查看我們的職位空缺。


