OpenAI 詳解 Pando 如何擴展 Git monorepo
Scaling Git at OpenAI
OpenAI 建成 Pando,作為 Git 倉庫的新後端,現已支援 OpenAI monorepo 的全部開發工作。Pando 保留 Git 協議和用戶端相容性,透過分支級並行更新及重用 packfile 擴展寫入和讀取能力。官方表示,Pando 將中位數推送延遲降低近 80%,每日可傳輸近一 petabyte Git 數據。
文章拆解 Pando 如何以分支級因果一致性及重用 packfile 支援並行寫入與大規模讀取,並交代放寬全倉庫一致性的取捨。
在 OpenAI 擴展 Git
建構支援 Agent 規模工作的版本控制系統,同時投資 Git 生態系統。
由技術團隊成員 Aaron Friel、Taylor Blau 及 Ted Nyman 撰寫
載入中…
分享
為甚麼我們需要新的版本控制系統
過去一年,我們 monorepo 的規模及承受的負載以驚人速度上升。顯然,我們需要改造版本控制系統,以配合 Codex 所帶來的並行工作規模。
我們建構了 Pando,一個 Git 儲存庫的新後端,過程中沒有中斷任何工程工作,也沒有改變團隊使用 Git 的方式。Pando 現已支援 OpenAI 單一儲存庫中的所有開發工作。Pando 的設計旨在實現兩個雄心勃勃的目標:第一,實現可水平擴展的寫入能力,並針對多個分支上的並行推送進行最佳化;第二,將讀取吞吐量最大化,讓用戶端下載數據的速度足以充分用盡可用網絡頻寬。
我們在達成這些目標的同時,保留了與 Git 協定及用戶端的相容性,讓工程師和 Agent 都能無縫地將 Pando 納入現有工作流程。在正式環境中,Pando 將推送延遲中位數降低近 5 倍。如今,Pando 每天傳送近 1 petabyte 的 Git 數據。
本文會介紹我們建構新版本控制系統的動機,以及選擇保留 Git 相容性的原因。我們會將系統與常見的 Git 擴展方案作比較,説明 Pando 為何能以獨特方式滿足 OpenAI 的擴展需求。我們亦會討論接下來的工作,以及如何繼續為 Git 作出貢獻,以支援 Agent 規模的工作。
為甚麼我們需要新的版本控制系統
Codex 改變了 OpenAI 開發工作的規模和步伐,讓工程師能夠並行處理更多工作。這意味着分支、推送和合併數量增加,開發環境、研究工作及 CI 發起的 fetch 和 clone 操作亦更多。
這些需求會互相疊加。現在,一名工程師可能同時有多項變更準備交由 CI 處理,而每次推送都可能令多項工作擷取同一個 commit。
從去年年底左右開始,支援我們 monorepo 的系統開始難以應付。我們遇到抓取和推送速度緩慢,以及合併失敗等問題,可靠度有時更跌至 90% 以下。我們遇到的是 Git 本身的限制,還是我們提供 monorepo 的方式出了問題?Git 源自一個透過電郵交換核心修補程式的社羣。二十多年來,隨着軟件開發方式不斷改變,開發者一直圍繞 Git 調整工具。當我們開始感受到自己的 monorepo 出現阻滯時,能夠在這些成果之上繼續開發,對我們十分重要。
我們找到一種方法,既能保留熟悉的工作流程,又能重建後端,讓系統平行處理推送至不同分支的操作,並同時向多名讀取者提供相同的歷史記錄。以 Git 作為版本控制工作流程的基礎,意味着客戶端的改進也能惠及 OpenAI 以外的用戶。我們希望將這些改進從我們的 Git 副本(在新視窗開啟)貢獻至上游,與更廣泛的社羣分享。
大規模 Git 的工程設計
擴展版本控制系統有兩個關鍵面向:大量寫入者向不同分支推送,以及大量讀取者抓取相同的歷史記錄。Pando 針對兩者作出最佳化。
首先,Pando 會獨立協調各分支的更新。互不相關分支的更新可以平行進行;讀取者抓取某個分支時,會取得該修訂版本所需的歷史記錄。取捨是,讀取者查看多個分支時,看到的可能是各分支在不同時間點的狀態。
其次,Pando 會在多次抓取時重用 packfile(Git 用來將多個物件一併壓縮的格式),而非為每個用戶端各自產生 packfile。用戶端可能會下載部分已經擁有的物件,但伺服器只需進行少量運算,便可向多個用戶端提供相同的位元組。
這些設計讓我們能夠擴展不同分支的寫入處理能力,並從快取提供讀取服務。要了解這些設計如何配合,先從 Git 儲存及傳輸歷史記錄的方式説起。
Git 物件儲存入門
Git 必須讓儲存庫的兩個部分保持同步:物件和引用。
Git 儲存庫會以物件圖記錄檔案快照及串連這些快照的歷史。物件可以是 blob、tree、commit 或 tag。Blob 儲存個別檔案的內容;tree 則將名稱與 blob 或其他 tree 配對,形成目錄結構。Commit 指向根 tree 及零個或多個父 commit,組成儲存庫的歷史。
物件以其類型及內容計算出的雜湊值識別。物件也不可變:記錄變更後的檔案會建立新的 blob,而未變更的檔案和目錄則可重用現有物件。
分支和標籤都是指向 commit 的引用,或稱「refs」。在分支上提交會建立新的 commit,並更新該分支的 ref,令它指向這個 commit。
Git 物件會以 ID 互相引用:commit 指向 tree,而 tree 指向其他 tree 和 blob。分支 refs/heads/main 指向 commit C2,而 C2 的父 commit 是 C1。C1 指向根 tree T1,其中 parser.py 指向 blob B1。C2 指向根 tree T2,其中 parser.py 指向 blob B2。在兩個根 tree 中,docs/ 都指向同一個 tree T3。T3 的 README.md 項目指向 blob B3,而 usage.md 則指向 blob B4。
Git 會以「loose」或「packed」狀態儲存物件。Loose 物件會各自儲存在獨立檔案中,並以其物件 ID 識別。Packed 物件則會一同儲存在 packfile 中,讓相似物件可以彼此為基礎,以差異資料的形式儲存。
Git 可以將 blob 儲存為獨立檔案,也可以將它們封裝在一起,並以一個 blob 為基礎,將另一個 blob 編碼為差異資料。
在 Git 中,如果 Git 能夠沿著提交的父提交、樹及 blob 的連結追蹤並找到某個物件,該物件便是該提交的 可達 物件。執行 fetch 時,用戶端會告訴伺服器它需要哪些提交,以及已經有了哪些提交。伺服器會找出從所需提交可達的物件,排除它知道用戶端已有的物件,再以 Git 在磁碟上使用的相同格式,將其餘物件作為 packfile 傳送。完整複製儲存庫時,起初不會有任何物件,因此需要取得所複製歷史記錄中所有可達的物件。
Push 會以相反方向傳送物件,並要求伺服器更新一個或多個 refs。用戶端會利用伺服器公佈的 refs 集合,避免傳送伺服器已有的物件。因此,正在傳送的 pack 可能依賴現有物件:單靠接收 pack 並不足以還原新的歷史記錄。
複製 Git 儲存庫,代表要在多部伺服器上保留可用的物件和引用。副本要提供某個引用的新值之前,必須能夠提供從該值可達的物件。服務亦需要協調互相競爭的 引用更新,即多個 push 嘗試將同一分支從相同的舊頂端移至不同的新提交。若系統強制儲存庫的所有寫入都依照同一順序進行,寫入不相關分支的 push 便可能需要互相等候。
現有的分散式 Git 服務模式
設計 Pando 時,我們評估了使用 Git 擴展版本控制服務的傳統做法:一種是使用三階段提交(3PC,一種用於協調多個節點提交的協定),在儲存庫副本之間協調 ref 更新;另一種是在 blob 儲存空間中透過共用的預寫日誌(WAL,即在變更套用前寫入的變更記錄)為更新排序。
三階段提交(3PC)
在我們建模的三階段提交設計中,Git 服務會在多個副本上保留完整的儲存庫副本。任何伺服器收到 push 時,都會複製傳入的 packfile,並同時傳送給所有副本。要發布該 push,必須有過半數副本接受 ref 更新。負責管理此流程的伺服器稱為協調者;它需要與其他每個副本進行多輪通訊。
要接受引用更新,每個副本都會鎖定其 ref 副本,並檢查該 ref 是否仍指向用戶端預期的提交。接著,協調者會向接受更新的副本傳送預提交訊息。過半數副本確認收到預提交訊息後,協調者便會要求它們執行更新。之後,任何已同步的副本都可以從其本機儲存庫副本公佈新的分支頂端。
保留完整副本可讓讀取操作在本機處理,並確保無論用戶端選擇哪個副本,都能一致地檢視所有引用。因此,引用會逐一更新,而每次 push 都需要多輪往返通訊,才能協調各副本之間的 ref 更新。
等待法定人數會令副本磁碟效能和通訊延遲影響推送操作的關鍵路徑。寫入量增加時,副本必須同時應付傳入的 packfile 和讀取流量。
預寫式日誌(WAL)
在我們建模的 WAL 架構中,Git 服務會使用儲存在 blob 儲存空間的共用清單,為整個儲存庫的更新排序。伺服器收到推送後,會把傳入的 packfile 上載到 blob 儲存空間,並在儲存庫的本機副本中準備 ref 更新。要讓推送生效,伺服器會在 blob 儲存空間的共用清單中新增一個項目,記錄 ref 更新並指向該 packfile。接着,Git 伺服器會使用比較並交換操作更新清單。此操作會以原子方式取代清單,否則便會拒絕變更。如果期間有其他寫入者更新了清單,伺服器便會讀取期間新增的項目,檢查自己的 ref 更新是否仍然有效,然後使用已上載的 packfile 重試。
寫入者可以平行上載 packfile,但即使推送至互不相關的分支,也必須更新同一份清單。成功執行比較並交換操作所需的往返時間,限制了儲存庫發布新清單的頻率。低延遲的 blob 儲存空間可加快這個步驟。批次處理可讓一次清單更新同時處理多個 refs,但代價是更新需要排隊,延遲亦會增加。
blob 儲存空間可以平行接收 packfile,但對共用清單的更新仍會互相競爭。單靠提升 blob 儲存空間的容量,並不能解決這個瓶頸。每個副本仍須先從 blob 儲存空間讀取 packfile 並建立索引,才能在本機提供服務。如果傳入的寫入速度超過這項工作的處理速度,副本便會落後,用戶端擷取新推送的提交時亦要等候更久。
Pando 架構設計
接下來介紹 Pando。Pando 是一種全新架構,用於提供 Git 儲存庫服務,目標是支援大量用戶端同時推送。以下是它的運作方式:
重新思考限制
開發 Pando 前,我們初步認為可以在不犧牲 Git 的情況下滿足需求,但確實需要放棄一些大多數 Git 系統視為必要條件的假設:
全儲存庫一致性:要為所有 refs 提供一致的時間點快照,便需要在整個儲存庫內進行協調。在一個統一排列所有 ref 更新次序的系統中,推送至互不相關分支的操作會爭用同一個協調點。
每個用戶端各自獨有的 packfile:如果兩個本機歷史紀錄不同的用戶端擷取同一分支,伺服器可能需要為各自產生不同的 pack,以免傳送用戶端已擁有的物件。建立這些回應會耗用 CPU 和磁碟 I/O,令系統難以應付大量讀取。
相反,Pando 採用較寬鬆的一致性模型(因果一致性)來擴展寫入能力,並重用 packfile 來擴展讀取能力。
因果一致性(CC)
用戶端推送至分支時,Pando 會把傳入的 packfile 作為不可變的 blob,儲存在 blob 儲存空間。每當分支指向新位置,Pando 便會在 metadata 儲存空間記錄一個 ref 版本,其中包含新的分支頂端、提供該頂端的 pack,以及該 pack 所依賴的任何較早 ref 版本。blob 儲存空間和 metadata 儲存空間都可水平擴展。
從 Pando 擷取提交或參照,需要將一組參照版本(packfile)重播至用戶端,以重建建立該參照版本的用戶端狀態。這會在整個儲存庫範圍內提供較弱形式的一致性,同時為個別參照的更新提供可序列化性。用戶端只有在同時推送至相同參照時才會衝突。用戶端讀取多個分支時,不能假設這些讀取結果代表整個儲存庫在同一時間點的狀態。
推送至主幹會加入一個增量 pack,其中包含一個或多個提交。每次推送至支線分支時,都會再次包含該主題分支先前的提交,因此持有已公告主幹版本的用戶端可透過單一 pack 擷取該主題分支。
這是一種因果一致性。要擷取參照,讀取者會觀察產生該參照的寫入者的歷史記錄。推送期間,Pando 會控制用戶端與伺服器所認同的共享歷史記錄,確保用戶端產生更容易共享的 packfile。
因此,Pando 在 Git 操作期間不會提供所有參照在某一時間點的狀態,也不會提供複製或擷取期間參照更新的次序。具體而言,假設在完整複製的協商期間,分支 foo 和 bar 都有更新。在協商期間,一個用戶端可能會看到 foo 處於版本 1,而 bar 處於版本 2;另一個用戶端則可能會看到 foo 處於版本 2,而 bar 處於版本 1。實際上,用戶和 Agent 不依賴參照更新的全域排序,而是依賴單一參照內更新的次序。
比較不同方法
在這項對三種一致性模型的模擬中,我們設定 1,000 個用戶端各自選取不同的分支,然後反覆推送不重複的提交,並測量總實際執行時間,以及推送至擷取的延遲。後者是工作負載的重要指標,因為它測量用戶和 Agent 協作,或使用其他依賴版本控制的系統時所需的往返時間。這項模擬涵蓋從一般級至高階的虛擬機器及 Blob 儲存系統等硬件配置。在此模擬中,我們放寬了三階段提交的全儲存庫一致性要求。
模擬 Git 伺服器實作
追蹤每個寫入者的多次推送,並另外測量獨立讀取者何時可以簽出每個提交。
播放 重設
基礎設施
播放
寫入者
- 模擬合併 - [x] 批次合併至主幹
三階段提交
就緒
模擬時間 0 s
平均推送至擷取延遲—
已完成的寫入者 1,000 / 1,000 0 / 1,000
每個方格代表一個寫入者。所有已選架構均執行相同的工作負載。推送獲確認後,方格會逐漸變淺;寫入者完成時,方格則會轉為藍色。圖表顯示已完成寫入者的數量隨模擬時間的變化;其後,獨立讀取者仍可繼續擷取。使用方向鍵選取寫入者,按 Home 或 End 跳至首項或末項,按 Escape 清除。
已確認 0 / 10,000 次推送 · 已擷取 0 次
選取方格以查看該寫入者。
物件儲存 WAL
就緒
模擬時間 0 s
平均推送至擷取延遲—
已完成的寫入者 1,000 / 1,000 0 / 1,000
每個方格代表一個寫入者。所有已選架構均執行相同的工作負載。推送獲確認後,方格會逐漸變淺;寫入者完成時,方格則會轉為藍色。圖表顯示已完成寫入者的數量隨模擬時間的變化;其後,獨立讀取者仍可繼續擷取。使用方向鍵選取寫入者,按 Home 或 End 跳至首項或末項,按 Escape 清除。
已確認 0 / 10,000 次推送 · 已擷取 0 次
選取方格以查看該寫入者。
Pando
就緒
模擬時間 0 s
平均推送至擷取時間
已完成的寫入者 1,000 / 1,000 0 / 1,000
每個方格代表一名寫入者。所有選取的架構都執行相同的工作負載。推送獲確認後,方格顏色會變淺;寫入者完成時,方格會變成藍色。圖表顯示寫入者在模擬時間內的完成情況;之後,獨立讀取者仍可繼續擷取。使用方向鍵選取寫入者,按 Home 或 End 跳至最前或最後,按 Escape 清除選取。
0 / 10,000 次推送已確認 · 已擷取 0 次
選取方格以查看該寫入者的資料。
寫入者經驗證擷取的進度
- 三階段提交
- 物件儲存 WAL
- Pando
模擬時間 0 s
- 等候中
- 推送中
- 所有推送均已確認
0 s 後
結果 設定
合成教學模型,並非正式環境基準測試。平均推送至擷取時間涵蓋主題分支推送,從首次嘗試開始,經過重試,直到獨立讀取者驗證該提交為止。此時間包括等候讀取者名額,但不包括建置與測試。啟用合併時,寫入者完成時間包括所需的擷取及主幹合併。
實驗設定
取消
正在準備模擬…
套用並重設 取消
結果
關閉
播放比較過程以查看記錄的結果。
跳至最終結果
合成教學模型,並非正式環境基準測試。平均推送至擷取時間涵蓋主題分支推送,從首次嘗試開始,經過重試,直到獨立讀取者驗證該提交為止。此時間包括等候讀取者名額,但不包括建置與測試。啟用合併時,寫入者完成時間包括所需的擷取及主幹合併。
執行此模擬,比較 Pando 與另外兩種方案。選擇由一般硬件至高階硬件的基礎設施、用戶端數量及其他設定,查看各方案的表現。
模擬中的三階段提交系統在負載增加時,磁碟達到飽和,寫入延遲便會顯著增加;同樣地,預寫日誌系統的讀取延遲也會顯著增加,因為伺服器難以跟上從日誌讀取資料的速度。在我們的 monorepo 真實環境測試及此模擬中,Pando 均支援數以千計的用戶端同時推送,而且在負載下不會影響效能。
處理推送時,Pando 會在將傳入的 packfile 串流至 blob 儲存的同時進行驗證,而不會將它解包至本機儲存庫。用戶端開始擷取時,Pando 會選取讀取者所需的已儲存 packfile,再由讀取者下載。伺服器無須將 packfile 套用至本機儲存庫,也無須執行成本高昂的解包及封裝操作。由於這些伺服器主要負責協商及選取 packfile,Pando 伺服器可在一般硬件上運行。
這讓 Pando 的擴展能力可達用戶端、用戶端與 Pando 之間的網絡,以及 blob 儲存服務各自的上限。容量充足時,在不同分支上工作的用戶端各自都可按其連線速度傳輸 packfile。要讓更多用戶端以該速度運作,我們可以增加 Pando 伺服器,並同步擴展中繼資料儲存及 blob 儲存。
在 Pando 推送及擷取分支
Pando 會將寫入者的 packfile 儲存在 blob 儲存中,並在中繼資料服務記錄 ref 版本。擷取時,Pando 會選取讀取者所需的 packfile,再由 Express 提供這些 packfile。
播放
串流推送 擷取分支
- Packfile 位元組
- 中繼資料
- 請求/確認
- Packfile URI
Agent 編輯檔案、在本機建立提交,並執行 git push;該命令會建立包含這些變更的 packfile,以更新分支。Pando 接收該 pack,並將其串流至 blob 儲存空間。Pando 以單次解碼驗證傳入的推送後,會使用多鍵交易,在我們的中繼資料儲存區記錄 ref 版本,並以原子方式更新 ref 指標。這讓 Pando 能提供「讀取自己寫入內容」一致性。整個寫入操作只需 $O \left(\right. 1 \left.\right)$O(1) 次中繼資料讀取和寫入。
當另一個用戶端抓取分支時,Pando 會根據用戶端已有的歷史記錄,選取一組已儲存的 pack。此操作需要 $O \left(\right. log \Delta \left.\right)$O(lo g Δ) 次中繼資料讀取;$\Delta$Δ 代表用戶端現有版本與所需版本之間,沿 ref 版本計算的最短距離。伺服器會傳回 packfile URI,用戶端則透過 Express 服務下載這些檔案;該服務會將 packfile 儲存在區域快取中。
這種設計依賴 Pando 架構中的三項核心服務:推送、抓取和壓縮整理。
擴展推送能力
用戶端推送時,傳統 Git 伺服器就像一名攤開所有底牌的撲克牌手,向用戶端公開自己擁有的每個分支。接著,用戶端會收集提交、樹狀物件和 blob,建立一個會排除用戶端已知伺服器擁有之物件的 packfile。
宣告更多 refs 可以縮小推送內容,但也可能令 pack 更難重用,因為其他用戶端可能沒有推送者省略的物件。
Pando 採用不同做法。它不會亮出所有底牌,而只會向用戶端宣告精選的一組分支。原因有二:
提供每個 ref 的一致檢視成本高昂。分支數量達數百萬時,僅 ref 宣告就可能超過 100 MiB。要做到這一點,每部伺服器都必須維持最新的對應表(需要大量協調開銷),或執行一項成本會隨 ref 數量增長的資料庫查詢。
只向用戶端提供部分 refs,便能令用戶端產生的 pack 僅依賴伺服器明確允許其排除的內容。追蹤這些依賴,可讓 Pando 重用用戶端在一次推送中產生的 packfile,並在之後的抓取操作中將其作為回應提供。
Git 伺服器宣告的 refs 越多,推送中的用戶端就有越多依據可排除物件,因而能產生更小的 pack。不過,要重用這些 packfile,伺服器必須能證明進行抓取的用戶端各自都擁有所有依賴項。否則,packfile 可能會省略用戶端需要的物件。
另一方面,如果 Git 伺服器完全不宣告 refs,用戶端就會建立包含整個儲存庫完整歷史記錄的 packfile。其他任何用戶端都可以使用這個 packfile,但代價是抓取任何分支實際上都等同完整複製整個儲存庫。
宣告更多 refs 會產生更小的 packfile。只宣告一個 ref,就能帶來大部分的體積縮減。
預設情況下,用戶端推送新的功能分支時,Pando 只會宣告一個指定的 trunk 分支作為基礎。這樣一來,即使儲存庫的分支數量增加,宣告內容的大小也維持不變,但這種做法亦有取捨。工程師多次推送至同一分支,或將一個功能分支疊加在另一個功能分支之上時,用戶端就必須重新傳送 trunk 以外的所有內容。
這會令後續分支推送的 packfile 較大,並可能包含重複物件,同時消耗額外的用戶端頻寬。在我們的 monorepo 中,以這種方式產生的 packfile,中位數不足 20 KiB,第 99 百分位數則不足 3 MiB。對我們而言,這是合理的取捨,因為讀取方只要已經有已宣告的主幹歷史紀錄,就可以透過該單一 packfile 擷取分支。
選定要向用戶端宣告的 refs 集合後,Pando 會接受 push 並更新其中繼資料存放區。Pando 將中繼資料更新分為同步和非同步部分。同步更新可確保用戶端立即擷取新更新的提交。這會建立新的 ref 版本,並將 ref 指標對應更新至該版本;該版本會儲存新的 tip、協商 packfile 時使用的相依項目,以及指向已推送 packfile 的連結。
同一筆交易亦會記錄非同步工作,例如排程 compaction。對於非常大型的 push,只有部分提交會同步寫入。這讓我們無論 push 的大小如何,都只需對中繼資料存放區執行固定數量的讀取和寫入操作。
重要的是,Pando 伺服器可以各自接受對互不相關 refs 的 push。唯一會發生爭用的鍵是 ref 指標,我們會透過類似原子比較並交換的操作加以保護。只要增加 Pando 伺服器、中繼資料存放區擴展單位或 blob 儲存庫,就能提升寫入處理量。
線速 fetch 和 clone 效能
用戶端執行 fetch 時,會向伺服器提供兩組提交 ID,在通訊協定中分別稱為 haves 和 wants。在傳統 Git 伺服器中,伺服器會計算由 haves 和 wants 可到達的物件之間的差集,將它們壓縮,再把結果傳送給用戶端。雖然開源 Git 伺服器設有多種本機索引來加快這項操作,但計算這些 packfile 的成本可能很高,而且通常不易快取。
Pando 則會利用 push 時記錄的提交對應資料查找用戶端的 haves 和 wants,並將它們解析為包含這些提交的一組 ref 版本。每個 ref 版本都連結至 Pando 先前接收的 packfile。伺服器會據此推算用戶端所需的內容,並選出涵蓋用戶端所需物件集合的 packfile。
Pando 的 fetch 規劃
Pando 會比較用戶端已有和所需的內容,再選出所需的已儲存 packfile,包括任何缺少的分支相依項目。
分支 A
分支 B
HAVE4WANT
已持有 擷取 缺少的分支相依項目
擷取 27 個主幹 packfile。
選取分支,以納入其主幹相依項目。
Pando 會將 Git 通訊協定中的 HAVE 和 WANT 對應至一連串 ref 版本。想要某個側分支,即代表也想要取得其在主幹上的相依項目。
Git 通訊協定只允許在 clone 或 fetch 的 HTTP 回應中傳送單一 packfile。不過,Git 通訊協定有一項未受充分重視的功能 packfile-uri,可讓伺服器傳回連結,以供用戶端分別下載額外的 pack。Pando 只使用這項功能,並在回應內嵌傳送空的 packfile。為支援這項功能,OpenAI 已向上遊 packfile-uri 貢獻多項改進。
OpenAI 的系統在多個地理區域運作,因此我們在各區域均設有 Pando 叢集,並配置本地快取。Express 是一項 packfile 傳送服務,會快取我們的 blob 儲存庫;另一項獨立服務則會快取 metadata 儲存庫。這些快取讓 Pando 可以完全依靠該叢集中的快取狀態,處理要求讀取 commit ID 或以特殊方式讀取 ref 的請求。我們使用一致性雜湊,將 packfile 請求路由至 Express 伺服器,快取命中率超過 99%。
這套快取系統的吞吐量極高,足以令用戶端與 Pando 之間的網絡頻寬達到上限。使用這些 packfile URI 及本地快取,我們已驗證,只用標準雲端供應商提供的虛擬機,複製及擷取操作便可令 10 Gbps 網絡連線達到飽和。
壓實與首字節時間
對大多數 Git 伺服器而言,重新打包及垃圾回收是維持不斷增長的儲存庫效能所必需的。這兩個步驟都能釋出空間,但需要大量計算及 I/O。
對於 OpenAI 的 monorepo 這類快速變動的儲存庫,要為上次擷取以來發生的每次推送各自提供 packfile URI,並不可行。用戶端每天都需要下載數以萬計個 packfile,才能追上主幹。
因此,Pando 會在背景整合 pack,以減少這項負擔;其設計與日誌結構合併樹中的壓實相似。每次推送都會產生 Pando 所稱的「L0」packfile,這是 Pando 可傳輸的最小資料單位。隨着時間推移,背景工作程序會將這些 packfile 收集成涵蓋一連串 ref 版本的涵蓋包,並從 L0 開始按幾何級數擴展。目前,我們的 monorepo 最多使用至 L4 的 packfile 涵蓋包。
這些涵蓋包有雙重用途。涵蓋包累積足夠內容後,壓實工作程序便會認領它,並產生單一 packfile。這會重新整理 packfile 內的物件,提升壓縮率;同時也能充當 ref 版本的跳躍表,提升擷取協商效能。Pando 同時掃描各層索引,便能以對數時間選出一組要傳送給用戶端的 pack。無論是否經過重新打包,這些 packfile 涵蓋包都能讓擷取規劃更有效率,並降低 Pando 對吞吐量及延遲的要求。
Pando 的壓實
小型 packfile 按層級整合成較大的涵蓋包,提升壓縮效果並加快擷取。
播放
在現有 pack 上發佈涵蓋包
L2
L1
L0
正在進行壓實 已發佈的涵蓋包
壓實會在背景整合來自多個 ref 版本的 packfile,讓擷取更有效率,所需檔案也更少。
不同於許多 Git 伺服器,所有層級的 packfile 都會保留,不會被丟棄或進行垃圾回收。各層級的 pack 都會持續用於服務用戶端,具體取決於用戶端在擷取時要求的 have 和 want。對於把 Pando 當作 API 使用的用戶端,從 packfile 讀取任意物件時,從 L0 讀取所需的資料通常少於從 L4 讀取,因為物件較不容易被壓縮。
Pando 的壓實大幅減少傳送給用戶端的總資料量,並令 pack 規劃的規模相對於用戶端落後的程度呈次線性增長。對於大型且快速變動的儲存庫,這點尤其重要;我們預期這類儲存庫將成為軟件開發的新常態。
建構 Pando
我們使用 Codex 建構 Pando,短短 10 週便由項目啟動推進至正式投入生產。Pando 的核心設計由一名工程師負責,由多個 agent 團隊實作,並由少數工程師部署。Pando 於今年 4 月開始承載正式環境流量,是 OpenAI 使用的大型源碼管理工具系統的一部分。
在驗證 Pando 的設計時,我們採用了多種方法。我們應用了建構更安全的軟件基礎的原則。由於這是一個全新項目,我們選擇使用 Rust 實作 Pando,並避免依賴舊有元件。我們編寫了一套相容性測試套件,讓 agent 可以按照標準 Git 行為測試其修改。我們亦開發了確定性模擬測試框架,用來檢查系統的不變量和屬性,藉此找出並修正協商和 pack 規劃中的細微競態條件及回歸問題。
多個模型都為這個系統作出貢獻;隨着新模型推出,我們便利用它們提升 Pando 的效能和可靠性。使用 GPT‑6 Astra 時,agent 編寫了 Lean 和 TLA+,將「Pando 理論」形式化,用以找出並修正實作中的錯誤,以及證明系統的屬性。多個持續執行時段長達數百小時,用來找出並修正 Pando 的邊界情況。我們亦全天候運行 agent,使用支援 Daybreak 的模型對 Git 和 Pando 進行紅隊測試。
部署 Pando
我們最初以讀取及寫入直通代理的方式部署 Pando,連接現有的源碼管理系統。這讓 Pando 與舊系統保持同步,令我們可以在不中斷服務的情況下,安全地將流量轉移至 Pando。
作為快取設計的一部分,Pando 亦可在轉發請求前將請求去重和合併。這讓我們逐一將系統遷移至 Pando 時,現有工具仍可正常運作。隨着我們將更多工作負載遷移至 Pando,這些工作負載的推送會填充 Pando 的快取,並提升舊有後端的可靠性。
為了在使用 Pando 時提升效能,我們開始將 Git 的公開發行版本部署至開發者的電腦,以及 ChatGPT 桌面應用程式內。為了將客戶端遷移至 Pando,我們使用 Git CLI 的內部包裝程式,分階段遷移客戶端。我們亦建立了許多其他工具,用來處理變更請求、程式碼審查,以及供自動化使用的 API。我們將數百個依賴舊有 API、webhook 和儲存庫下載的整合,連同 Git 流量一併遷移。
Codex 本身亦協助開發者和團隊完成遷移。我們建立了技能,用來準備現有的程式碼工作副本,以及引導 agent 使用新系統。因此,開發者只需要求 Codex 遷移其開發環境和服務即可。
轉用 Pando 後,合併的可靠性有所提升,推送延遲中位數亦減少近 80%。
Pando 每日只需少數 CPU 核心,便可傳送 1 petabyte 的 Git 資料。Pando 曾在一小時內傳送超過 57 terabytes 的 Git pack,其中超過 99% 的位元組來自本機磁碟快取。
重用 pack 讓許多客戶端可以下載相同的儲存庫歷史記錄,而服務節點只需進行少量運算。此例展示單一儲存庫,但 Pando 的設計支援更多儲存庫。隨着需求增加,我們可以獨立擴展資料傳送和 Git 協商。
回饋 Git 專案
以 Git 協定和用戶端為基礎建構 Pando,讓我們找出錯誤,以及提升 Git 本身效能的機會。
例如,Pando 會直接向用戶端提供可重用的 pack,因此下載及建立這些 pack 的索引,成為提升系統速度和可靠性的關鍵工作。這項工作揭示了並行下載、傳輸中斷後的清理,以及續傳已完成下載等邊緣情況。我們修正了相關流程,在 fetch 時加入接受現有 pack 的驗證,並將這些改進貢獻回上游。
我們亦需要了解時間和頻寬的使用情況,因此加入了 packfile-uri 下載的追蹤功能,以及 pack 產生時的位元組數統計,讓我們更容易判斷緩慢的 fetch 是耗時於產生 pack,還是傳輸 pack。
在OpenAI 的 Git 建置版本(在新視窗中開啟)中,用戶端可以並行下載及建立互不相關的 pack 索引。我們加入了「明確 haves」的概念,讓伺服器可以分開公佈分支目前的頂端提交,以及推送的用戶端可能不會放入 pack 的歷史記錄。在 Pando 上,這讓用戶端可以使用 --force-with-lease 確認功能分支沒有變動,並只以 trunk 為基礎建立 pack。任何擁有該 trunk 歷史記錄的人,之後都可以使用同一個 pack 取得該分支。
其中多項改進已納入上游 Git,還有不少改進已加入我們的公開儲存庫。我們會繼續與 Git 社羣合作,讓這些改進也能惠及 Pando 以外的用戶。你可以在我們的公開修補程式追蹤器(在新視窗中開啟)瞭解相關進展。
下一步
透過重新思考擴展 Git 儲存庫規模的傳統做法,Pando 讓我們得以擴展單一儲存庫,同時無須改變工程師和 Agent 已在使用的 Git 工作流程。在我們的單一儲存庫中,Pando 提升了並行處理能力和合併可靠性,亦縮短了發佈程式碼變更所需的時間。除了單一儲存庫,我們亦正在探索 Pando 在 OpenAI 內的其他用途,包括需要大量 Agent 在同一個 Git 儲存庫中協作的工作負載。
隨着工程師和 Agent 產生更多軟件,OpenAI 以外的更多團隊也會遇到我們在版本控制方面碰到的限制。我們目前正探索如何與用戶分享令 Pando 成為可能的技術。
Pando 建基於 Git 儲存庫用戶和營運者二十年來累積的經驗。我們會繼續開發 Pando,並與開源 Git 社羣合作,分享任何能廣泛惠及 Git 用戶的改進。
作者
OpenAI
來源:OpenAI News · openai.com