OpenAI 的每項產品都仰賴快速可靠的資料存取,無論是使用者登入、檢查 Codex 設定,或是在 ChatGPT 中開啟新對話。這些操作都可能需要多次獨立的資料查詢,產品才能做出回應。如果這些請求速度緩慢,產品體驗就會變差;如果請求失敗,產品甚至會完全停止運作。

Habitat 是 OpenAI 建立的線上儲存平台,旨在讓產品能快速可靠地存取所需資訊。Habitat 目前每秒處理超過 7,000 萬次請求,支援每週逾 10 億用戶,遍及近 40 個地理區域。兩年前,Habitat 僅是一個連接單一資料庫的簡單 Python 用戶端函式庫;如今,它已是一個複雜的分散式系統,服務超過 500 PB 的資料。

建立和營運如此大規模的基礎設施並非易事,但也並非特別困難。然而,OpenAI 的情況獨特之處在於,必須以史無前例的速度擴展,以支援驚人的用戶增長和產品需求,同時還要建立一個成熟的平台。系統工程師通常會為 10 倍的規模進行建置,並希望它能維持數年,同時為下一個 10 倍規模做準備。

但在 OpenAI 的案例中,過去三年每年都以超過 10 倍的速度增長。因此,建立和營運 Habitat 是一系列戰術決策和順序安排的結果:在最低層次理解每個元件,以從現有堆疊中榨取最大效益,同時應對儲存和運算容量的瓶頸,為基礎投資爭取時間。

隨著 OpenAI 的成長,Habitat 也必須隨之擴展:首先是變得足夠可靠,以處理關鍵任務的產品流量;接著是足夠快速,以服務全球用戶;最後,是能夠在大規模下靈活運作。這篇文章是關於如何擴展線上儲存的兩部曲系列中的第一篇。在本文中,我們將分享 Habitat 如何演變,為何我們將其從函式庫轉變為服務,以及如何將一個用不常見的服務堆疊語言—Python—編寫的服務,打造成一個可靠的儲存平台層。

在未來的文章中,我們將詳細介紹如何在大規模下實現多租戶的可靠性、我們用於優化讀取效能的分層策略,以及如何擴展與 Azure Cosmos DB 的合作關係,以可靠地處理前所未有的需求。

Habitat 是什麼?Habitat 源於一個簡單的想法:產品工程師不應該需要考慮資料庫管理。Habitat 於 2024 年年中作為一個小型 Python 函式庫開始,與 ChatGPT 的主伺服器互動。它支援一小部分操作,這些操作在底層映射到資料庫應用程式 Azure Cosmos DB。

這個函式庫的任務是為產品團隊提供一種簡單的方式來儲存和檢索資料,而無需掌握底層細節。Habitat 負責處理必要的工作:判斷涉及何種資料、資料應從何處來(或去何處)、請求是否被允許等等。

產品工程師無需擔心 schema 查詢、路由、授權、加密、序列化、請求塑形和連線池。他們甚至不需要考慮資料來源:是 Azure Cosmos DB、快取還是其他類型的儲存。

這個 Python 函式庫運作良好,儘管沒有集中推動放棄使用自助式 Postgres 和 Azure Cosmos DB,Habitat 仍在 OpenAI 的產品工程師中迅速普及。隨著產品需求的演變,產品開發人員甚至可以輕鬆地為共享函式庫添加對用戶端快取、壓縮或加密等功能的支持。

建立服務以更好地支援多個複雜產品。到 2025 年年中,Habitat 作為用戶端實作已達到其極限。隨著 Habitat 層變得越來越複雜,以及 OpenAI 服務數量的增加,向後相容的協定變更變得不可行。

在一個案例中,我們希望透過將最關鍵的資料集遷移到一組區域分散的 Azure Cosmos DB 帳戶,來減少任何單一區域中斷的影響範圍。進行這項變更需要向用戶端引入額外的路由邏輯,並透過功能旗標禁用,確保其部署到所有用戶端,然後再啟用該功能旗標。

協調數十個服務的部署並與每個團隊合作推出,花費了數天時間。在啟用之前,我們意識到需要引入一些影子測試,以確保分片邏輯正確無誤。這又花了幾天時間推出。發現錯誤需要修復?又需要幾天。最終,我們準備好啟用旗標,結果其中一個團隊因無關原因將其服務回滾到先前有錯誤的用戶端,導致了我們努力避免的中斷。

對用戶端函式庫的變更需要數十個服務之間的複雜協調,這個過程證明越來越脆弱、低效且容易發生操作失敗。為了減少未來部署的操作擴散,我們決定將 Habitat 獨立成一個服務。

透過將儲存邏輯解耦為獨立服務,我們為部署、可觀察性和平台增強建立了單一控制點。我們不再需要管理零碎的更新,而是可以集中實施改進,為每個 OpenAI 產品帶來即時效益。

集中式服務也為我們提供了一個單一的瓶頸點,以提供最強大的資料安全和隱私原語。Habitat 服務是我們可以集中執行存取控制策略、執行稽核日誌記錄,並限制對底層儲存資源(如 Azure Cosmos DB)存取的地方。Habitat 在保護用戶資料和防止來自外部、內部和代理行為者的未經授權存取方面扮演著關鍵角色。

大規模啟動 Python 服務。我們知道我們需要一個服務,但當時我們還不想放棄 Python,即使 Python 作為服務會增加額外開銷。對於高吞吐量服務使用 Python 會增加網路延遲,並相較於本地函式庫執行,顯著增加 CPU 和記憶體擴展成本。此外,我們認識到 Python 的低效率在 100 倍規模下是不可接受的,這使得最終重寫幾乎是必然的。

然而,我們將這視為一種策略性的技術債。當時我們的主要目標不是成本或資源優化,而是解除產品開發人員的阻礙並實現平台穩定性。透過短期內接受 Python 服務的效能權衡,我們能夠優先解決更緊迫的挑戰,建立核心 API,並建構一個強大的基礎設施。

我們也做了一個經過計算的賭注,認為我們自己的程式碼模型快速進步將簡化未來的技術路徑。我們賭,到需要完全從 Python 遷移時,Codex 和 GPT 將使這次遷移變得可行。這個賭注最終證明是正確的。

將 Habitat 作為 Python 服務運行在效能上會是次優的選擇,但卻是必要的。Python 讓我們能夠快速行動,但這並不意味著我們可以掉以輕心並接受明顯更差的延遲。當平均用戶請求導致數百次資料庫呼叫時,用戶感受到的就是最慢的資料庫呼叫。我們發現,以這種規模運行 Python 服務的主要挑戰在於管理這些尾部延遲。

追蹤 asyncio 延遲。Asyncio 幫助 Python 同時執行 I/O 密集型工作負載,但無法解決 Python GIL 並提供 CPU 並行性。除了 I/O 密集型請求代理,Habitat 還處理許多 CPU 密集型職責和背景任務:路由、壓縮、加密、校驗和、下游健康檢查、請求影子測試和對沖。

由於服務中有如此多的 CPU 密集型工作負載和背景任務,asyncio 排程延遲很容易主導尾部請求延遲。在針對我們首次服務發布進行調整之前,我們在 p99 或更高延遲的請求追蹤中發現,儘管下游儲存回應迅速,但請求經常在等待負責的協程被重新排程以解析回應時停滯。

對於 OpenAI 的 Python 服務,我們發現除了測量記憶體、CPU、網路和磁碟使用率的標準利用率和飽和度指標外,監控 asyncio 迴圈及其繁忙程度,然後進行相應調整也至關重要。

透過定期排程背景任務並記錄預期執行時間與實際執行時間之間的差異,我們能夠即時經驗性地測量事件迴圈排程延遲。在高利用率且有許多昂貴任務的情況下,即使每個程序只有少量並發請求,也足以產生顯著的排程抖動,高達數百毫秒,在某些極端情況下甚至數秒。

因此,我們採取的方法是讓每個程序只處理少量並發請求,並大幅擴展 Python 工作程序數量。

減少功能旗標配置中的尾部延遲。在我們首次服務發布時,透過即時服務 CPU 分析,我們發現了高 asyncio 延遲(以及由此導致的高尾部延遲)的一個根本原因:透過 Statsig(一個管理功能旗標並可用於 A/B 測試等的工具)定期解析功能旗標配置的 JSON。預設情況下,Statsig 配置為每分鐘輪詢一次更新的配置,沒有抖動,而且配置包含了所有服務的所有生產規則。

在其他地方,一個架構決策是每個 Pod 運行多達 8 個 Python 程序,以提高 CPU 使用率並提供更低的延遲。綜合來看,這意味著每分鐘每個 Pod 都會有一段時間,其所有工作程序會停止處理進行中的請求,轉而將其 CPU 週期用於解析一個巨大的配置檔案。

一旦 CPU 分析幫助我們找出問題的根本原因,解決方案就非常直接:部署一個更小、更有針對性的配置,延長刷新間隔,並為這些背景任務添加一些抖動。

負載平衡和連線池管理。為了維持低 asyncio 延遲,保持請求在伺服器程序之間良好的負載平衡也至關重要;如果沒有適當調整,連線池最終可能會與此背道而馳。

透過用戶端連線池,一個執行許多並發請求的單一用戶端程序可能只建立少數伺服器連線,結果將其所有負載發送到少數幾個程序。在調整負載平衡方式之前,我們的服務利用率差異很大,一些尾部程序的並發請求數量是平均值的 5-10 倍。

我們在一次偶然事件中發現了這一點,儘管我們停止了導致服務部分過載的用戶端,但一部分程序在突發流量過後仍長時間處於降級狀態。事實上,我們注意到這些程序經歷了...