OpenAI

2026年11月2日

工程

在 OpenAI 擴展 Git

打造能支援智慧體工作規模的原始碼控制系統,同時持續投入 Git 生態系。

作者:技術團隊成員 Aaron Friel、Taylor Blau 與 Ted Nyman

載入中…

過去一年,我們的單一程式碼庫規模及其承受的需求,都以驚人的速度成長。顯然,我們必須改造原始碼控制系統,才能支援 Codex 所帶來的大規模並行工作。

我們打造了 Pando,這是 Git 程式碼庫的新後端,整個過程既未中斷任何工程工作,也未改變團隊使用 Git 的方式。如今,OpenAI 單一程式碼庫中的所有開發工作都由 Pando 支援。Pando 的設計旨在實現兩項雄心勃勃的目標:第一,讓寫入能力可水平擴展,並針對同時推送至多個分支的情境進行最佳化。第二,將讀取吞吐量提升至最大,讓用戶端能以充分利用可用網路頻寬的速度下載資料。

我們在達成這些目標的同時,也保留了與 Git 協定及用戶端的相容性,讓工程師與智慧體都能將 Pando 無縫納入現有工作流程。在正式環境中,Pando 將推送延遲的中位數降至原本的約五分之一。目前每天傳送的 Git 資料量接近 1 PB。

本文將介紹我們打造全新原始碼控制系統的動機,以及選擇保留 Git 相容性的原因。我們會將這套系統與常見的 Git 擴展方法相比,說明 Pando 為何能以獨特的方式滿足 OpenAI 的擴展需求。我們也會討論下一步計畫,以及如何持續為 Git 貢獻改進,以支援智慧體帶來的工作規模。

為何我們需要新的原始碼控制系統

Codex 改變了 OpenAI 的開發規模與步調,讓工程師能平行處理更多工作。這代表更多分支、推送與合併,以及來自開發環境、研究作業和 CI 的更多擷取與複製操作。

這些需求還會相互疊加。如今,一位工程師可能同時有多項變更準備好進入 CI,而每次推送又可能觸發多個作業擷取同一筆提交。

約從去年年底開始,支撐我們單一程式碼庫的系統就逐漸難以跟上需求。我們遇到擷取與推送緩慢、合併失敗等問題,可靠性有時甚至降至 90% 以下。我們碰到的是 Git 本身的極限,還是提供單一程式碼庫服務的方式出了問題?Git 起源於一個透過電子郵件交換核心修補程式的社群。二十多年來,隨著軟體開發方式改變,開發者也持續調整以 Git 為核心的工具。當我們自己的單一程式碼庫開始遇到阻力時,能否在這些既有成果上繼續發展,對我們來說非常重要。

我們找到一條路:保留熟悉的工作流程,同時重建後端,讓它能平行處理對不同分支的推送,並同時向多個讀取端提供相同的歷史記錄。以 Git 作為原始碼控制工作流程的基礎,也意味著用戶端的改進能讓 OpenAI 以外的使用者受益。我們希望將我們的 Git 副本⁠(在新視窗中開啟)中的這些改進回饋至上游,與更廣大的社群分享。

為大規模 Git 使用打造架構

擴展原始碼控制系統有兩個關鍵面向:大量寫入端向不同分支推送,以及大量讀取端擷取相同的歷史記錄。Pando 同時針對這兩點進行最佳化。

首先,Pando 會獨立協調每個分支的更新。互不相關的分支可平行更新,而讀取端擷取分支時,會取得該修訂版本所需的歷史記錄。代價是讀取端查看多個分支時,可能看到它們在不同時間點的狀態。

其次,Pando 會在不同擷取請求間重複使用封裝檔(Git 將多個物件一起壓縮的格式),而不是為每個用戶端個別產生封裝檔。用戶端可能會下載一些已有的物件,但伺服器只需極少運算,就能將相同資料提供給多個用戶端。

這些選擇讓我們能跨分支擴展寫入能力,並從快取提供讀取服務。要了解這些設計如何相互配合,先從 Git 儲存與傳輸歷史記錄的方式談起。

Git 物件儲存入門

Git 必須讓程式碼庫的兩個部分保持同步:物件與參照。

Git 程式碼庫以物件圖的形式,記錄檔案快照及串連這些快照的歷史。物件可以是 blob、樹、提交或標籤。blob 儲存單一檔案的內容,樹則將名稱與 blob 或其他樹關聯起來,形成目錄結構。提交指向一棵根樹,以及零個或多個父提交,共同構成程式碼庫的歷史記錄。

物件以其類型與內容的雜湊值識別。物件也不可變更:記錄變更過的檔案會建立新的 blob,未變更的檔案與目錄則可重複使用現有物件。

分支和標籤是指向提交的參照,也稱為「refs」。在分支上提交時,會建立新的提交,並更新該分支的參照,使其指向新提交。

分支 refs/heads/main 指向提交 C2,其父提交為 C1。C1 指向根樹 T1,其中 parser.py 參照 blob B1。C2 指向根樹 T2,其中 parser.py 參照 blob B2。在這兩棵根樹中,docs/ 都參照同一棵樹 T3。其中 README.md 項目指向 blob B3,usage.md 則指向 blob B4。

Git 物件透過 ID 互相參照:提交參照樹,樹則參照其他樹與 blob。分支 refs/heads/main 指向提交 C2,其父提交為 C1。C1 指向根樹 T1,其中 parser.py 參照 blob B1。C2 指向根樹 T2,其中 parser.py 參照 blob B2。在這兩棵根樹中,docs/ 都參照同一棵樹 T3。其中 README.md 項目指向 blob B3,usage.md 則指向 blob B4。

Git 以「鬆散」或「封裝」狀態儲存物件。鬆散物件儲存在個別檔案中,並以物件 ID 識別。封裝物件會一起儲存在封裝檔中,讓相似物件能以彼此之間的差異儲存。

$GIT_DIR/objects 下有兩個版本的 parser.py。以鬆散物件儲存時,blob A 包含 parse(),而 blob B 同時包含 parse() 與 parse_file()。封裝檔完整儲存 A,並將 B 表示為一組指令:複製 A 的全部內容,再附加 parse_file()。隨附的 *.idx 檔案透過物件 ID,定位兩個 blob 在封裝檔中的位置。

Git 可以將 blob 儲存為獨立檔案,也可以將它們一起封裝,並將一個 blob 編碼為相對於另一個 blob 的差異。

在 Git 中,如果能從某筆提交出發,沿著父提交、樹與 blob 的連結找到某個物件,該物件就是從這筆提交可到達的。擷取期間,用戶端會告訴伺服器想取得哪些提交,以及已擁有哪些提交。伺服器會找出從所需提交可到達的物件,排除已知用戶端擁有的部分,再將其餘物件以封裝檔傳送,格式與 Git 在磁碟上使用的格式相同。完整複製一開始沒有任何物件,因此需要取得從所複製歷史記錄可到達的所有物件。

推送則以相反方向傳輸物件,並要求伺服器更新一或多個參照。用戶端會根據伺服器公告的參照集合,避免傳送伺服器已有的物件。因此,傳送的封裝檔可能依賴現有物件:只收到封裝檔,並不足以重建新的歷史記錄。

建立 Git 程式碼庫的複本,意味著要讓物件與參照在多台伺服器上都保持可用。複本在提供參照的新值之前,必須能提供從該值可到達的物件。服務也需要協調互相競爭的參照更新,也就是嘗試將同一分支從同一個舊頂端移向不同新提交的推送。若系統對程式碼庫中的所有寫入強制採用單一順序,即使推送至互不相關的分支,也可能必須彼此等待。

現有的分散式 Git 服務模式

設計 Pando 時,我們評估了使用 Git 擴展原始碼控制的傳統方法:一種透過三階段提交(3PC,一種協調多個節點共同提交的協定)來協調程式碼庫複本間的參照更新;另一種則透過 blob 儲存區中的共用預寫日誌(WAL,在套用變更之前先寫入的變更記錄)來排列更新順序。

三階段提交(3PC)

在我們建模的三階段提交設計中,Git 服務會在多個複本節點上,保留程式碼庫的完整副本。任何伺服器收到推送時,都會複製傳入的封裝檔,並平行傳送至所有複本。要發布這次推送,必須有過半數複本接受參照更新。管理這項流程的伺服器稱為協調者,整個流程需要協調者與其他各複本進行多輪通訊。

為了接受參照更新,每個複本都會鎖定自己持有的參照,並確認它仍指向用戶端預期的提交。接著,協調者會向接受更新的複本傳送預先提交訊息。一旦過半數複本確認預先提交訊息,協調者就會通知它們執行更新。之後,任何已更新的複本都能從本機程式碼庫副本公告新的分支頂端。

保留完整複本能在本機提供讀取服務,也能保證無論用戶端選擇哪個複本,看到的所有參照狀態都一致。代價是參照必須逐一更新,而且每次推送都需要多次往返通訊,才能協調複本之間的參照更新。

等待法定數量的複本回應,會讓複本的磁碟效能與通訊延遲直接影響推送流程。隨著寫入量成長,複本除了處理讀取流量,也必須跟上持續傳入的封裝檔。

預寫日誌(WAL)

在我們建模的 WAL 設計中,Git 服務會使用 blob 儲存區中的共用資訊清單,為整個程式碼庫的更新排序。伺服器收到推送時,會將傳入的封裝檔上傳至 blob 儲存區,並在本機程式碼庫副本中準備參照更新。要發布推送,伺服器會在 blob 儲存區的共用資訊清單中新增項目,記錄參照更新並指向封裝檔。接著,Git 伺服器使用比較並交換操作來更新資訊清單。這項操作會以原子方式替換資訊清單,否則就拒絕變更。如果期間已有其他寫入端更新清單,伺服器就會讀取其間新增的項目,確認自己的參照更新仍然有效,再使用已上傳的封裝檔重試。

寫入端可以平行上傳封裝檔,但即使推送至互不相關的分支,仍必須更新同一份資訊清單。成功執行一次比較並交換所需的往返時間,限制了程式碼庫發布新資訊清單的頻率。低延遲的 blob 儲存區能加快這個步驟。批次處理可讓一份資訊清單同時更新多個參照,但代價是更新必須排隊,延遲也會增加。

blob 儲存區可以平行接收封裝檔,但共用資訊清單的更新仍會互相競爭。單靠增加 blob 儲存區的容量,並無法解決這項瓶頸。每個複本仍需從 blob 儲存區讀取封裝檔並建立索引,才能在本機提供它們。如果傳入寫入的速度超過這項工作的處理速度,複本就會落後,用戶端也得等更久才能擷取剛推送的提交。

設計 Pando 架構

這就帶我們來到 Pando。Pando 是提供 Git 程式碼庫服務的全新架構,目標是擴展到能支援大量用戶端同時推送。它的運作方式如下:

重新思考限制條件

在開發 Pando 之前,我們直覺認為,不必犧牲 Git 就能滿足需求,但確實得放寬一些大多數 Git 系統視為必要的條件:

  • 整個程式碼庫的一致性: 若要提供所有參照在同一時間點的一致快照,就必須在整個程式碼庫中進行協調。若系統將所有參照更新統一排序,即使推送至互不相關的分支,也會爭用同一個協調點。

  • 為每個用戶端產生專屬封裝檔: 若兩個本機歷史記錄不同的用戶端擷取同一分支,伺服器可能必須分別產生不同的封裝檔,以免傳送用戶端已擁有的物件。產生這些回應會耗用 CPU 和磁碟 I/O,因此很難擴展到支援大量讀取端。

Pando 則採用較寬鬆的一致性模型(因果一致性)來擴展寫入能力,並透過重複使用封裝檔來擴展讀取能力。

因果一致性(CC)

當用戶端推送至某個分支時,Pando 會將傳入的封裝檔以不可變的 blob 形式存入 blob 儲存區。每次分支移動時,Pando 都會在中繼資料儲存區中記錄一個參照版本,內容包含新的分支頂端、提供該頂端的封裝檔,以及該封裝檔所依賴的所有先前參照版本。blob 儲存區和中繼資料儲存區都能水平擴展。

從 Pando 擷取提交或參照時,必須向用戶端重播一組參照版本(封裝檔),以重建當初建立該參照版本的用戶端狀態。這種方式對整個程式碼庫提供較弱的一致性保證,同時確保個別參照的更新具備可序列化性。只有在同時推送至同一參照時,用戶端之間才會發生衝突。用戶端讀取多個分支時,不能假設這些讀取結果代表整個程式碼庫在同一時間點的狀態。

左側三個連續的主幹封裝檔分別包含提交 A、B、C;D、E;以及 F。右側的主幹與主題分支在 C 處分岔。主幹前進至 G,而主題分支依序經過 D、E、F。最新的主題分支封裝檔包含 D、E、F。虛線外框標示 D 和 E 也存在於先前的主題分支封裝檔中。藍色箭頭標示新封裝檔依賴主幹在 G 時的狀態。F 在 Git 中的父提交為 E。

推送至主幹時,會新增一個包含一或多筆提交的增量封裝檔。每次推送至側分支時,都會再次包含該主題分支先前的提交,因此擁有已公告主幹版本的用戶端只需取得一個封裝檔,就能擷取該主題分支。

這是因果一致性的一種形式。讀取端為了擷取某個參照,會觀察到產生該參照的寫入端歷史記錄。推送期間,Pando 會控制用戶端與伺服器認定為共有的歷史記錄,確保用戶端產生的封裝檔更容易共用。

因此,Pando 在 Git 操作期間不會提供所有參照在同一時間點的狀態,也不會在複製或擷取時提供參照更新的統一順序。舉例來說,假設在完整複製的協商期間,foo 和 bar 分支都發生了更新。協商期間,一個用戶端可能看到版本 1 的 foo 和版本 2 的 bar,另一個用戶端卻看到版本 2 的 foo 和版本 1 的 bar。實務上,使用者與智慧體並不在意參照更新的全域順序,而是依賴單一參照的更新順序。

比較不同方法

在這項比較三種一致性模型的模擬中,我們設定 1,000 個用戶端,讓它們各自選擇不同分支,然後反覆推送各自獨有的提交,並測量總實際經過時間,以及從推送到可擷取的延遲。後者是工作負載的重要指標,因為它衡量了使用者與智慧體協作,或使用其他依賴版本控制的系統時,所需的往返時間。這項模擬涵蓋從一般到高階的虛擬機器與 blob 儲存系統。在這項模擬中,我們放寬了三階段提交對整個程式碼庫一致性的要求。

Simulating Git Server Implementations

Follow each writer through repeated pushes, and separately measure when an independent reader can check out each commit.

Three-phase commit

Ready
Simulated time
0 s
Mean push to fetch
—
Writers done
0 / 1,000

Each square represents one writer. All selected architectures run the same workload. Squares lighten as pushes are acknowledged and turn blue when a writer finishes. The chart shows writers finished over simulated time; independent readers can continue fetching afterward. Use arrow keys to select a writer, Home or End to jump, and Escape to clear.

0 / 10,000 pushes acknowledged · 0 fetched

Select a square to inspect that writer.

Object-store WAL

Ready
Simulated time
0 s
Mean push to fetch
—
Writers done
0 / 1,000

Each square represents one writer. All selected architectures run the same workload. Squares lighten as pushes are acknowledged and turn blue when a writer finishes. The chart shows writers finished over simulated time; independent readers can continue fetching afterward. Use arrow keys to select a writer, Home or End to jump, and Escape to clear.

0 / 10,000 pushes acknowledged · 0 fetched

Select a square to inspect that writer.

Pando

Ready
Simulated time
0 s
Mean push to fetch
—
Writers done
0 / 1,000

Each square represents one writer. All selected architectures run the same workload. Squares lighten as pushes are acknowledged and turn blue when a writer finishes. The chart shows writers finished over simulated time; independent readers can continue fetching afterward. Use arrow keys to select a writer, Home or End to jump, and Escape to clear.

0 / 10,000 pushes acknowledged · 0 fetched

Select a square to inspect that writer.

Writer progress through verified fetches

  • Three-phase commit
  • Object-store WAL
  • Pando
05001,00000 s
  • Waiting
  • Pushing
  • All pushes acknowledged
0 sLater

Synthetic teaching models, not production benchmarks. Mean push to fetch covers topic pushes, from the first attempt through retries until an independent reader verifies that commit. It includes waiting for a reader slot and excludes builds and tests. With merges enabled, writer completion includes the required fetches and trunk merge.

Experiment settings

Preparing simulation…

Results

Play the comparison to see the recorded results.

Synthetic teaching models, not production benchmarks. Mean push to fetch covers topic pushes, from the first attempt through retries until an independent reader verifies that commit. It includes waiting for a reader slot and excludes builds and tests. With merges enabled, writer completion includes the required fetches and trunk merge.

執行這項模擬,比較 Pando 與另外兩種方法。選擇從一般到高階硬體的基礎架構、用戶端數量及其他設定,看看各種方法的表現差異。

模擬的三階段提交系統在負載增加、磁碟達到飽和時,寫入延遲明顯上升;同時,預寫日誌系統也因相同原因出現明顯更高的讀取延遲,因為伺服器難以及時讀取日誌。無論是在我們單一程式碼庫的實際測試中,還是在這項模擬中,Pando 都能支援數千個以上的用戶端同時推送,且不影響負載下的效能。

處理推送時,Pando 一邊驗證傳入的封裝檔,一邊將其串流傳送至 blob 儲存區,不必將它解包到本機程式碼庫。當用戶端開始擷取時,Pando 會選出讀取端需要的已儲存封裝檔,再由讀取端下載。任何伺服器都不必將封裝檔套用至本機程式碼庫,也不必執行成本高昂的解包與封裝作業。由於這些伺服器主要負責協商和選取封裝檔,Pando 伺服器可在一般硬體上執行。

因此,Pando 可以持續擴展,直到達到用戶端、用戶端與 Pando 之間的網路,以及 blob 儲存服務的上限。只要容量充足,在獨立分支上工作的用戶端都能以各自連線允許的最高速度傳輸封裝檔。若要以同樣速度服務更多用戶端,我們可以新增 Pando 伺服器,並同步擴展中繼資料儲存區和 blob 儲存區。

Pushing and Fetching a Branch in Pando

Pando stores the writer’s packfile in blob storage and records the ref version in metadata service. On fetch, Pando selects the packs the reader needs, and Express serves them.

Stream the push

  • Packfile bytes
  • Metadata
  • Request / acknowledgment
  • Packfile URIs

智慧體在本機編輯檔案並建立提交,接著執行 git push,將這些變更製成封裝檔,以更新分支。Pando 接收封裝檔,並將它串流傳送至 blob 儲存區。Pando 以單次解碼驗證傳入的推送後,會使用多鍵交易,在中繼資料儲存區中記錄參照版本,並以原子方式更新參照指標。這讓 Pando 能提供「讀取自己寫入的資料」一致性保證。整個寫入作業需要 O(1)O(1) 次中繼資料讀寫。

另一個用戶端擷取該分支時,Pando 會根據用戶端已有的歷史記錄,選出一組已儲存的封裝檔。這項作業需要 O(log⁡Δ)O(\log \Delta) 次中繼資料讀取,其中 Δ\Delta 是用戶端已有版本與想取得版本之間,以參照版本數計算的最短距離。伺服器會傳回封裝檔 URI,用戶端再透過 Express 服務下載;Express 會將封裝檔儲存在區域快取中。

這項設計仰賴 Pando 架構中的三項核心服務:推送、擷取與壓實。

擴展推送能力

用戶端推送時,傳統 Git 伺服器就像攤開所有底牌的撲克牌玩家,向用戶端揭露它擁有的每個分支。接著,用戶端會收集提交、樹與 blob,建立封裝檔,並排除已知伺服器擁有的物件。

公告更多參照可以縮小推送的資料量。但這也可能讓封裝檔更難重複使用,因為其他用戶端可能缺少寫入端省略的物件。

Pando 採取了不同做法。它不會攤開所有底牌,而是只向用戶端公告一組選定的分支。這麼做有兩個原因:

  1. 提供所有參照的一致檢視成本高昂。當分支數達到數百萬時,光是參照公告就可能超過 100 MiB。要提供這些資訊,每台伺服器都必須維護最新對應表,產生大量協調負擔;或是執行成本隨參照數量增加的資料庫查詢。

  2. 只向用戶端提供部分參照,就能讓用戶端產生的封裝檔,僅依賴伺服器明確允許它排除的內容。藉由追蹤相依關係,Pando 能重複使用用戶端在某次推送中產生的封裝檔,將它作為後續擷取的回應。

Git 伺服器公告的參照越多,推送端就有越多依據可排除物件,因此能產生更小的封裝檔。然而,要重複使用這些封裝檔,伺服器必須能確認擷取端已擁有所有相依內容。否則,封裝檔可能會漏掉用戶端需要的物件。

另一方面,如果 Git 伺服器完全不公告參照,用戶端就會產生包含程式碼庫完整歷史記錄的封裝檔。任何其他用戶端都能使用該封裝檔,但代價很高:擷取任何分支,實際上都等同於完整複製。

曲線顯示公告的參照如何影響封裝檔大小:完全不公告時包含完整歷史記錄,接著是僅公告主幹,直到公告所有參照時只包含新物件。

公告越多參照,產生的封裝檔就越小。只公告一個參照,就可能達到大部分的縮減效果。

預設情況下,用戶端推送新的功能分支時,Pando 只會公告指定的主幹分支作為基礎。如此一來,即使程式碼庫的分支增加,公告大小也不變,但這仍有取捨。當工程師多次推送至同一分支,或在一個功能分支上再疊加另一個功能分支時,用戶端必須重新傳送主幹以外的所有內容。

這會讓後續的分支推送變大,包含可能重複的物件,並耗用額外的用戶端頻寬。在我們的單一程式碼庫中,以這種方式產生的封裝檔,大小中位數低於 20 KiB,第 99 百分位數低於 3 MiB。對我們而言,這是合理的取捨,因為已擁有公告主幹歷史記錄的讀取端,只需這一個封裝檔就能擷取分支。

選定要向用戶端公告的參照集合後,Pando 就會接受推送,並更新中繼資料儲存區。Pando 將中繼資料更新分為同步與非同步兩部分。同步中繼資料更新確保用戶端能立即擷取剛更新的提交。這個步驟會寫入新的參照版本,並將參照指標對應更新至該版本,同時儲存新的分支頂端、協商封裝檔時使用的相依內容,以及推送封裝檔的連結。

同時,同一筆交易也會記錄非同步工作,例如排程壓實作業。對於非常大的推送,只有部分提交會以同步方式寫入。因此,無論推送大小如何,我們對中繼資料儲存區都只需執行固定次數的讀寫。

重要的是,Pando 伺服器能獨立接受對互不相關參照的推送。唯一會發生爭用的鍵是參照指標,我們以類似原子比較並交換的操作來保護它。我們可以透過新增 Pando 伺服器、中繼資料儲存區擴展單位,或 blob 儲存區,提高寫入吞吐量。

達到線速的擷取與複製效能

用戶端執行擷取時,會向伺服器提供兩組提交 ID,在協定中稱為 have 和 want。在傳統 Git 伺服器中,伺服器會計算從已擁有提交與想取得提交可到達的物件集合之間的差集,將其壓縮後傳送給用戶端。雖然開源 Git 伺服器有各種本機索引可加速這項操作,但計算這些封裝檔仍可能成本高昂,而且通常不易快取。

Pando 則利用推送時記錄的提交對應,查找用戶端的 have 和 want,將它們解析為一組包含這些提交的參照版本。每個參照版本都連結至 Pando 先前收到的一個封裝檔。伺服器據此判斷用戶端所需的內容,選出能涵蓋所有所需物件的封裝檔。

Planning fetches in Pando

Pando compares what a client has with what it wants, then selects the stored packfiles needed—including any missing branch dependencies.

Already heldFetchMissing branch dependency

Fetch 27 trunk packs.

Select a branch to include its trunk dependencies.

Pando 將 Git 協定中已擁有與想取得的提交,對應至一串參照版本。想取得某個側分支,也意味著必須取得主幹中截至該分支依賴位置的歷史記錄。

Git 協定只允許在複製或擷取的 HTTP 回應中傳送單一封裝檔。不過,Git 協定有一項較少受到關注的功能 packfile-uri,可讓伺服器傳回連結,供用戶端另外下載其他封裝檔。Pando 完全採用這種方式,而在回應中只直接傳送空的封裝檔。為了支援這項功能,OpenAI 已向上游貢獻多項 packfile-uri 改進。

OpenAI 的系統在多個地理區域運作,因此我們在各區都維護了具備本地快取的 Pando 叢集。封裝檔傳送服務 Express 負責快取 blob 儲存區,另一項獨立服務則負責快取中繼資料儲存區。這些快取讓 Pando 能完全依據叢集內的快取狀態,處理指定提交 ID 的請求,或對參照的特殊讀取請求。我們使用一致性雜湊,將封裝檔請求路由至 Express 伺服器,快取命中率超過 99%。

這套快取系統提供極高的吞吐量,能充分用盡用戶端與 Pando 之間的網路頻寬。我們已驗證,透過這些封裝檔 URI 與本地快取,僅使用雲端供應商的標準虛擬機器,複製與擷取作業就能用滿 10 Gbps 網路連線。

壓實與首位元組時間

對大多數 Git 伺服器而言,要維持持續成長的程式碼庫效能,就必須重新封裝及回收無用資料。這兩個步驟都能回收空間,但也都需要大量運算與 I/O。

在像 OpenAI 單一程式碼庫這樣快速變動的程式碼庫中,針對上次擷取以來的每次推送,分別傳送一個封裝檔 URI 並不可行。用戶端每天都得下載數萬個封裝檔,才能讓主幹跟上最新狀態。

因此,Pando 採用類似日誌結構合併樹壓實機制的設計,在背景合併封裝檔,以降低這項額外負擔。每次推送都會產生一個 Pando 稱為「L0」的封裝檔,這是 Pando 可傳輸的最小資料單位。隨著時間推進,背景工作程序會將這些封裝檔收集成涵蓋一串參照版本的覆蓋集,並從 L0 開始以等比方式擴大範圍。目前,我們的單一程式碼庫使用最高到 L4 的封裝檔覆蓋集。

這些覆蓋集有雙重用途。當覆蓋集累積到足夠大小時,壓實工作程序就會接手,將其合併成單一封裝檔。透過重新組織封裝檔內的物件,這能提高壓縮率。同時,它也像參照版本上的跳躍串列,能改善擷取協商的效能。透過同時掃描各層索引,Pando 能以對數時間選出要傳送給用戶端的一組封裝檔。無論是否重新封裝,這些封裝檔覆蓋集都能提高擷取規劃的效率,降低 Pando 對吞吐量與延遲的要求。

Compaction in Pando

Small packfiles are grouped into larger covers across levels, improving compression and speeding up fetches.

Compaction in progressPublished cover

壓實作業會在背景合併多個參照版本的封裝檔,以更少的檔案提升擷取效率。

與許多 Git 伺服器不同,Pando 會保留每一層的封裝檔,不會丟棄或當作無用資料回收。各層封裝檔都會持續用於服務用戶端,具體取決於用戶端在擷取請求中提供的 have 與 want。對於透過 API 使用 Pando 的用戶端,從封裝檔讀取任意物件時,從 L0 讀取通常比從 L4 需要更少資料,因為該物件在 L0 中被壓縮的可能性較低。

Pando 的壓實機制大幅減少傳送給用戶端的總資料量,也讓封裝檔規劃的成本隨用戶端落後程度,以低於線性的速度成長。這對大型且快速變動的程式碼庫尤其重要,而我們預期這將成為軟體開發的新常態。

打造 Pando

我們運用 Codex 打造 Pando,從專案啟動到正式上線,只花了 10 週。Pando 的核心由一位工程師設計,多組智慧體團隊負責實作,再由少數幾位工程師部署。Pando 在今年 4 月開始處理正式環境流量,是 OpenAI 所使用的一套更大型原始碼控制管理工具系統的一部分。

我們採用了各種方法來驗證 Pando 的設計。我們落實了打造更安全軟體基礎⁠的原則。由於這是從零開始的專案,我們選擇使用 Rust 實作 Pando,且不沿用舊有相依套件。我們撰寫了一套一致性測試,讓智慧體能對照標準 Git 行為,測試自己的變更。我們也開發了確定性模擬測試的任務執行框架,用來檢查系統的不變量與性質,進而找出並緩解協商與封裝檔規劃中細微的競爭條件及回歸問題。

許多模型都參與了這套系統的開發;每當新模型推出,我們就運用它們來改善 Pando 的效能與可靠性。智慧體運用 GPT‑6 Astra 撰寫 Lean 與 TLA+,將「Pando 理論」形式化,讓我們能用來找出並修正實作中的錯誤,以及證明系統的性質。多個工作階段各自連續執行數百小時,找出並修正 Pando 中的邊界情況。我們也讓智慧體全天候執行,使用具備 Daybreak 能力的模型,對 Git 和 Pando 進行紅隊測試。

部署 Pando

最初部署時,Pando 是現有原始碼控制系統的讀寫直通代理。這讓 Pando 與舊系統保持同步,因此我們能在不停機的情況下,安全地將流量移轉至 Pando。

作為快取設計的一部分,Pando 也能在代理轉送之前,先將請求去重並合併。這讓我們在逐一將系統移轉至 Pando 時,仍能維持現有工具正常運作。隨著更多工作負載移轉至 Pando,它們的推送逐步填入 Pando 快取,也提升了舊後端的可靠性。

為了最佳化使用 Pando 時的效能,我們開始將公開發行的 Git 版本部署到開發者電腦,並納入 ChatGPT 桌面應用程式。我們使用包覆 Git CLI 的內部工具,分階段將用戶端移轉至 Pando。我們也建立了許多其他工具,用於處理變更請求、程式碼審查,以及供自動化使用的 API。除了 Git 流量,我們也移轉了數百項依賴舊版 API、Webhook 和程式碼庫下載的整合。

Codex 本身也協助開發者與團隊完成移轉。我們建立了技能,用來準備現有的簽出工作副本,並引導智慧體使用新系統。因此,開發者只要請 Codex 移轉開發環境和服務即可。

切換至 Pando 後,合併可靠性提高,推送延遲的中位數也降低了近 80%。

Pando 只需少數幾個 CPU 核心,每天就能傳送 1 PB 的 Git 資料。Pando 曾在單一小時內傳送超過 57 TB 的 Git 封裝檔,其中超過 99% 的資料來自本機磁碟快取。

重複使用封裝檔,讓許多用戶端能下載相同的程式碼庫歷史記錄,而服務節點只需進行極少運算。這個例子呈現的是單一程式碼庫,但 Pando 的設計能支援更多程式碼庫。隨著需求成長,我們可以分別擴展資料傳送與 Git 協商能力。

回饋 Git 專案

以 Git 協定與用戶端為基礎打造 Pando,協助我們找出 Git 本身的錯誤及效能改善機會。

例如,Pando 會直接向用戶端提供可重複使用的封裝檔,因此下載這些封裝檔並建立索引,就成為提升系統速度與可靠性的關鍵。這項工作揭露了並行下載、傳輸中斷後清理,以及恢復已完成下載等流程中的邊界情況。我們修正了這些處理流程,在擷取時接受既有封裝檔的步驟中加入驗證,並將成果回饋至上游。

我們也需要了解時間與頻寬耗用在哪裡,因此加入了 packfile-uri 下載追蹤,以及封裝檔產生時的位元組計數,讓我們更容易判斷緩慢的擷取究竟是耗時在產生封裝檔,還是傳輸封裝檔。

在 OpenAI 的 Git 建置版本⁠(在新視窗中開啟)中,用戶端能平行下載互相獨立的封裝檔並建立索引。我們加入「明確指定已擁有內容」的概念,讓伺服器能將分支目前的頂端,與推送端可從封裝檔省略的歷史記錄分開公告。在 Pando 上,這讓用戶端能使用 --force-with-lease 確認功能分支未移動,同時僅以主幹為基礎建立封裝檔。如此一來,任何擁有該主幹歷史記錄的用戶端,都能使用同一個封裝檔擷取該分支。

其中多項改進已納入上游 Git,還有許多改進已放在我們的公開程式碼庫中。我們持續與 Git 社群合作,讓這些改進不只適用於 Pando。你可以透過我們的公開修補程式追蹤器⁠(在新視窗中開啟)關注進度。

下一步

透過重新思考擴展 Git 程式碼庫的傳統做法,Pando 讓我們能在不改變工程師與智慧體現有 Git 工作流程的情況下,擴展單一程式碼庫。在我們的單一程式碼庫中,Pando 提高了平行處理程度、改善合併可靠性,並縮短發布程式碼變更所需的時間。除了單一程式碼庫,我們也在探索 Pando 在 OpenAI 內部的其他用途,包括需要大量智慧體在共用 Git 程式碼庫中協作的工作負載。

隨著工程師與智慧體產出更多軟體,OpenAI 以外的更多團隊也會遇到我們在原始碼控制方面碰到的限制。我們目前正在探索如何與使用者分享支撐 Pando 的技術。

Pando 建立在 Git 程式碼庫使用者與維運人員累積二十年的經驗之上。我們會持續開發 Pando,並與 Git 開源社群合作,分享能讓更多 Git 使用者受益的改進。

作者

OpenAI