過去一年,我們的單一程式碼庫規模及其承載的需求,都以驚人的速度增長。顯然,我們需要革新原始碼控制系統,才能支援 Codex 所帶來的並行工作規模。
我們建立了 Pando,這是一個全新的 Git 程式碼庫後端。整個過程既沒有中斷任何工程工作,也沒有改變團隊使用 Git 的方式。如今,OpenAI 單一程式碼庫的所有開發工作都由 Pando 支援。Pando 的設計旨在達成兩項進取目標:第一,讓寫入能力可橫向擴展,並針對同時向多個分支推送的情況進行優化。第二,將讀取吞吐量提升至最高,讓用戶端下載數據的速度足以用盡可用網絡頻寬。
我們在實現這些目標的同時,保留了與 Git 協定及用戶端的相容性,讓工程師和智能代理都能將 Pando 無縫融入現有工作流程。在正式環境中,Pando 將推送延遲的中位數縮短至原來約五分之一。現在,它每天傳送近 1 PB 的 Git 數據。
本文將介紹我們建立新原始碼控制系統的動機,以及選擇保留 Git 相容性的原因。我們會將這個系統與擴展 Git 的常見方法作比較,說明 Pando 為何具備獨特能力,能滿足 OpenAI 的擴展需求。我們也會討論下一步計劃,以及如何繼續為 Git 作出貢獻,以支援智能代理帶來的工作規模。
Codex 改變了 OpenAI 的開發規模和節奏,讓工程師能同時處理更多工作。這意味著更多分支、推送和合併,以及來自開發環境、研究作業和持續整合(CI)的更多擷取與複製操作。
這些需求還會相互疊加。現在,一名工程師可同時有多項變更準備好進入 CI,而每次推送又可能觸發多個作業擷取同一提交。
大約從去年年底開始,支援我們單一程式碼庫的系統已難以應付需求。我們遇到擷取和推送緩慢、合併失敗等問題,可靠性有時跌至 90% 以下。我們究竟碰到了 Git 本身的極限,還是提供單一程式碼庫服務的方式有其限制?Git 源於一個透過電郵交換核心修補程式的社群。二十多年來,隨著軟件開發方式改變,開發者一直圍繞 Git 調整自己的工具。當我們開始在自己的單一程式碼庫中遇到阻力時,能否繼續在這些成果之上發展,對我們十分重要。
我們找到了一個方法:保留熟悉的工作流程,同時重建後端,讓它能並行處理不同分支的推送,並同時向多個讀取端提供相同歷史。以 Git 作為原始碼控制工作流程的基礎,意味著對用戶端的改進,也能惠及 OpenAI 以外的用戶。我們希望將自己維護的 Git 版本(在新視窗中開啟)中的改進貢獻至上游,與更廣泛的社群分享。
擴展原始碼控制系統有兩個關鍵面向:大量寫入端向不同分支推送,以及大量讀取端擷取相同歷史。Pando 針對兩者都作了優化。
首先,Pando 會獨立協調每個分支的更新。互不相關的分支可以並行更新,而讀取端擷取某個分支時,會取得該修訂版本所需的歷史。代價是,讀取端查看多個分支時,看到的可能是各分支在不同時間點的狀態。
其次,Pando 會在不同擷取操作之間重用封裝檔(Git 用來將多個物件一起壓縮的格式),而不是為每個用戶端產生獨立封裝檔。用戶端可能會下載一些已有的物件,但伺服器只需少量運算,就能向多個用戶端提供相同數據。
這些取捨讓我們能跨分支擴展寫入能力,並從快取提供讀取服務。要了解它們如何配合運作,先來看看 Git 如何儲存和傳輸歷史。
Git 必須能讓程式碼庫的兩個部分保持同步:物件和引用。
Git 程式碼庫以物件圖的形式,記錄檔案快照及連接這些快照的歷史。物件可以是 blob、樹、提交或標籤。blob 儲存個別檔案的內容;樹則將名稱與 blob 或其他樹關聯起來,組成目錄結構。提交指向一棵根樹,以及零個或多個父提交,共同構成程式碼庫的歷史。
物件以其類型和內容的雜湊值識別。物件也是不可變的:記錄已變更的檔案會建立新的 blob,而未變更的檔案和目錄則可重用現有物件。
分支和標籤都是指向提交的引用,簡稱「refs」。在分支上提交時,會建立新提交,並更新該分支的引用,使其指向新提交。
Git 物件透過 ID 互相引用:提交指向樹,樹則指向其他樹和 blob。分支 refs/heads/main 指向提交 C2,其父提交為 C1。C1 指向根樹 T1,其中 parser.py 指向 blob B1。C2 指向根樹 T2,其中 parser.py 指向 blob B2。在兩棵根樹中,docs/ 都指向同一棵樹 T3。T3 的 README.md 項目指向 blob B3,usage.md 則指向 blob B4。
Git 以「鬆散」或「封裝」狀態儲存物件。鬆散物件儲存在個別檔案中,並以物件 ID 識別。封裝物件會一起儲存在封裝檔中,讓相似物件可用彼此之間的差異來儲存。
Git 可以將 blob 儲存為獨立檔案,也可以將它們一起封裝,把一個 blob 編碼為相對於另一個 blob 的差異。
在 Git 中,如果能從某個提交出發,沿著連結經過其父提交、樹和 blob 找到某個物件,該物件便稱為從該提交可達。擷取期間,用戶端會告訴伺服器自己需要哪些提交,以及已有哪些提交。伺服器找出從所需提交可達的物件,排除已知用戶端擁有的物件,再將餘下物件製成封裝檔傳送,格式與 Git 在磁碟上使用的格式相同。完整複製開始時沒有任何物件,因此需要取得從待複製歷史可達的所有物件。
推送則沿相反方向傳輸物件,並要求伺服器更新一或多個引用。用戶端利用伺服器通告的一組引用,避免傳送伺服器已有的物件。因此,傳送中的封裝檔可能依賴現有物件:單靠收到的封裝檔,並不足以重建新的歷史。
複寫 Git 程式碼庫,意味著要讓物件和引用在多台伺服器上都保持可用。副本在提供引用的新值之前,必須能供應從該值可達的物件。服務也需要協調互相競爭的引用更新,即嘗試將同一分支從同一個舊頂端提交,移至不同新提交的推送。如果系統對程式碼庫的所有寫入強制採用單一順序,即使推送至互不相關的分支,也可能需要互相等待。
設計 Pando 時,我們評估了以 Git 擴展原始碼控制的傳統方法:一種利用三階段提交(3PC,一種跨多個節點協調交易提交的協定),在程式碼庫副本之間協調引用更新;另一種則透過 blob 儲存系統中的共用預寫日誌(WAL,在套用變更前先寫下的變更記錄),為更新排序。
在我們模擬的三階段提交設計中,Git 服務會在多個副本節點上,保留程式碼庫的完整副本。任何伺服器收到推送時,都會複製收到的封裝檔,並行傳送至所有副本節點。要發佈這次推送,大多數副本節點都必須接受引用更新。負責管理此流程的伺服器稱為協調者,整個過程需要協調者與其他每個副本節點進行多輪通訊。
每個副本節點在接受引用更新前,都會鎖定自己的引用副本,並檢查它是否仍指向用戶端預期的提交。協調者隨後向接受更新的副本節點傳送預提交訊息。一旦大多數副本節點確認收到預提交訊息,協調者就會通知它們執行更新。之後,任何已更新的副本節點,都能從本機程式碼庫副本通告新的分支頂端提交。
保留完整副本,就能在本機處理讀取,並確保用戶端無論選擇哪個副本節點,都會看到所有引用的一致視圖。因此,引用只能逐一更新,而每次推送都需要多次往返通訊,才能在副本節點之間協調引用更新。
由於要等待法定數量的副本節點確認,推送過程便會受到副本磁碟效能和通訊延遲的影響。隨著寫入量增加,副本節點既要及時處理傳入的封裝檔,也要應付讀取流量。
在我們模擬的 WAL 設計中,Git 服務利用 blob 儲存系統內的共用資訊清單,為整個程式碼庫的更新排序。伺服器收到推送時,會將收到的封裝檔上載至 blob 儲存系統,並在本機程式碼庫副本中準備引用更新。要發佈這次推送,伺服器會在 blob 儲存系統內的共用資訊清單中新增一個項目,記錄引用更新並指向該封裝檔。Git 伺服器隨後使用比較並交換操作更新資訊清單。這項操作要麼以原子方式替換資訊清單,要麼拒絕變更。如果其間已有其他寫入端更新了資訊清單,伺服器便會讀取期間新增的項目,檢查自己的引用更新是否仍然有效,再利用已上載的封裝檔重試。
寫入端可以並行上載封裝檔,但即使推送至互不相關的分支,也需要更新同一份資訊清單。完成一次成功的比較並交換所需的往返時間,限制了程式碼庫發佈新資訊清單的頻率。低延遲的 blob 儲存系統可以加快這一步。批次處理讓一次資訊清單更新可同時更新多個引用,但代價是更新需要排隊,延遲也會增加。
blob 儲存系統可以並行接收封裝檔,但共用資訊清單的更新仍會互相爭用。單靠增加 blob 儲存系統的容量,無法解決這個瓶頸。每個副本節點仍需先從 blob 儲存系統讀取封裝檔並建立索引,才能在本機提供相關內容。如果傳入寫入的速度超過這些工作的處理速度,副本就會落後,用戶端也要等待更久,才能擷取剛推送的提交。
這就帶出了 Pando。Pando 是提供 Git 程式碼庫服務的全新架構,目標是擴展至支援大量用戶端同時推送。其運作方式如下:
開發 Pando 之前,我們直覺認為,不必放棄 Git 也能滿足需求。不過,對於大多數 Git 系統視為必要條件的一些要求,我們確實需要作出取捨:
整個程式碼庫的一致性:要提供所有引用在同一時間點的一致快照,就必須在整個程式碼庫內協調。如果系統為所有引用更新統一排序,即使推送至互不相關的分支,也會爭用同一個協調點。
為每個用戶端建立專屬封裝檔:如果兩個本機歷史不同的用戶端擷取同一分支,伺服器可能需要為它們各自產生不同的封裝檔,以免傳送它們已有的物件。產生這些回應會消耗 CPU 和磁碟 I/O 資源,難以擴展至支援大量讀取端。
Pando 則採用較寬鬆的一致性模型(因果一致性)來擴展寫入能力,並透過重用封裝檔來擴展讀取能力。
當用戶端向分支推送時,Pando 會將收到的封裝檔作為不可變的 blob,儲存於 blob 儲存系統。每當分支指向改變,Pando 都會在元數據儲存系統中記錄一個引用版本,包含新的頂端提交、提供該提交的封裝檔,以及該封裝檔所依賴的任何較早引用版本。blob 儲存系統和元數據儲存系統都能橫向擴展。
從 Pando 擷取提交或引用時,需要向用戶端重播一組引用版本(封裝檔),以重建當初建立該引用版本的用戶端狀態。這種方式在整個程式碼庫層面提供較弱的一致性,同時確保個別引用的更新具備可序列化性。只有在同時向同一引用推送時,用戶端才會發生衝突。用戶端讀取多個分支時,不能假設這些讀取結果反映了整個程式碼庫在同一時間點的狀態。
向主幹推送時,會新增一個包含一或多個提交的增量封裝檔。每次向側分支推送時,都會再次包含該主題分支較早的提交,讓持有已通告主幹版本的用戶端,只需一個封裝檔便能擷取該主題分支。
這是因果一致性的一種形式。讀取端要擷取某個引用,就會看到產生該引用的寫入端所持有的歷史。推送期間,Pando 會控制用戶端與伺服器確認共有的歷史,確保用戶端產生的封裝檔更容易共用。
因此,Pando 不會在 Git 操作期間提供所有引用於同一時間點的狀態,也不會在複製或擷取操作中提供引用更新的整體順序。具體而言,假設在完整複製的協商過程中,分支 foo 和 bar 都有更新。協商期間,一個用戶端可能看到版本 1 的 foo 和版本 2 的 bar,另一個用戶端則看到版本 2 的 foo 和版本 1 的 bar。實際上,用戶和智能代理不依賴引用更新的全域順序,而是依賴單一引用的更新順序。
在這項針對三種一致性模型的模擬中,我們設定了 1,000 個用戶端,各自選取不同分支,然後反覆推送互不相同的提交,並量度總實際耗時及從推送到可擷取的延遲。後者是工作負載的一項重要指標,因為它衡量了用戶和智能代理協作,或使用其他依賴版本控制的系統時,所需的往返時間。模擬涵蓋從一般到高階的虛擬機器及 blob 儲存系統等硬件配置。在這項模擬中,我們放寬了三階段提交對整個程式碼庫一致性的要求。
模擬 Git 伺服器的不同實作
追蹤各個寫入端多次推送的過程,並分開記錄獨立讀取端能夠檢出每項提交的時間點。
執行這項模擬,比較 Pando 與另外兩種方法。選擇從一般到高階硬件的基礎設施、用戶端數量及其他設定,看看它們的表現有何不同。
模擬的三階段提交系統在負載下,會因磁碟達到飽和而出現明顯較高的寫入延遲;預寫日誌系統則因同樣原因,令伺服器難以及時讀取日誌,導致讀取延遲明顯增加。無論是在單一程式碼庫的實際測試,還是在這項模擬中,Pando 都能支援數以千計的用戶端同時推送,而不影響負載下的效能。
處理推送時,Pando 會一邊驗證收到的封裝檔,一邊將其串流傳送至 blob 儲存系統,無需將其解包至本機程式碼庫。用戶端開始擷取時,Pando 會選取讀取端所需的已儲存封裝檔,再由讀取端下載。任何伺服器都無需將封裝檔套用至本機程式碼庫,也無需執行耗費資源的解包和封裝操作。由於伺服器主要負責協商和選取封裝檔,Pando 伺服器可以在一般硬件上運行。
這讓 Pando 可以持續擴展,直至達到用戶端、用戶端與 Pando 之間的網絡,以及 blob 儲存服務的容量上限。只要容量充足,在獨立分支上工作的用戶端,都能以各自連線容許的最高速度傳輸封裝檔。要以同樣速度服務更多用戶端,我們可以增加 Pando 伺服器,並同步擴展元數據儲存系統和 blob 儲存系統。
在 Pando 中推送及擷取分支
Pando 將寫入端的封裝檔存入 blob 儲存系統,並在元數據服務中記錄引用版本。讀取端擷取分支時,Pando 會選出所需的封裝檔,再由 Express 傳送給讀取端。
智能代理在本機編輯檔案並建立提交,然後執行 git push,將這些變更製成封裝檔,用來更新分支。Pando 接收封裝檔,並將其串流傳送至 blob 儲存系統。透過單次解碼驗證收到的推送後,Pando 會使用多鍵交易,在元數據儲存系統中記錄引用版本,並以原子方式更新引用指標。這讓 Pando 能提供「讀取自身寫入」的一致性保證。整個寫入操作只需 次元數據讀寫。
另一個用戶端擷取該分支時,Pando 會根據用戶端已有的歷史,選取一組已儲存的封裝檔。這項操作需要 次元數據讀取,其中 是用戶端已有與所需內容之間,以引用版本數計算的最短距離。伺服器傳回封裝檔 URI,用戶端再透過 Express 服務下載;該服務會將封裝檔儲存在區域快取中。
這個設計依賴 Pando 架構的三項核心服務:推送、擷取及合併壓實。
用戶端推送時,傳統 Git 伺服器就像亮出所有底牌的啤牌玩家,向用戶端揭示自己擁有的每個分支。用戶端隨後收集提交、樹和 blob,建立封裝檔,並排除已知伺服器擁有的物件。
通告更多引用,可以減少推送的數據量。但這也可能讓封裝檔更難重用,因為其他用戶端可能缺少寫入端省略的物件。
Pando 採用了不同方法。它不會亮出所有底牌,而是只向用戶端通告選定的一組分支。這樣做有兩個原因:
提供所有引用的一致視圖,成本高昂。當分支數量達數百萬時,單是引用通告就可能超過 100 MiB。要提供這種視圖,每台伺服器都必須維護最新的對應表,承擔大量協調開銷;或執行成本隨引用數量增加的資料庫查詢。
只向用戶端提供部分引用,就能讓它產生的封裝檔,僅依賴伺服器明確允許它排除的內容。透過追蹤這些依賴關係,Pando 可以重用用戶端在某次推送中產生的封裝檔,將其作為後續擷取的回應。
Git 伺服器通告的引用越多,推送的用戶端就有越多依據可以排除物件,從而產生更小的封裝檔。不過,要重用這些封裝檔,伺服器必須能確認擷取的用戶端具備每一項依賴內容。否則,封裝檔可能會缺少用戶端所需的物件。
另一方面,如果 Git 伺服器完全不通告任何引用,用戶端就會產生包含程式碼庫完整歷史的封裝檔。其他任何用戶端都能使用這個封裝檔,但代價很高:擷取任何分支,實際上都等同完整複製。
通告的引用越多,產生的封裝檔就越小。只通告一個引用,往往已能達到大部分縮減效果。
預設情況下,用戶端推送新的功能分支時,Pando 只會通告一個指定的主幹分支作為基礎。這讓通告大小不會隨程式碼庫的分支數量增加,但也有其代價。當工程師多次向同一分支推送,或在一個功能分支之上再建立另一個功能分支時,用戶端必須重新傳送主幹以外的所有內容。
這會令後續分支推送的數據量更大,可能包含重複物件,並消耗額外的用戶端頻寬。在我們的單一程式碼庫中,以這種方式產生的封裝檔大小中位數低於 20 KiB,第 99 百分位數則低於 3 MiB。對我們而言,這是合理的取捨,因為已持有通告中主幹歷史的讀取端,只需這一個封裝檔就能擷取分支。
選定向用戶端通告的引用後,Pando 會接受推送並更新元數據儲存系統。Pando 將元數據更新分成同步和非同步兩部分。同步元數據更新確保用戶端能立即擷取剛更新的提交。這個步驟會寫入新的引用版本,並更新引用指標的對應關係,使其指向新版本,同時儲存新的頂端提交、協商封裝檔時使用的依賴內容,以及指向已推送封裝檔的連結。
與此同時,非同步工作也會記錄在同一筆交易中,例如安排合併壓實。對於非常大型的推送,只有部分提交會同步寫入。因此,無論推送大小如何,我們都只需對元數據儲存系統進行固定次數的讀寫。
重要的是,Pando 伺服器可以獨立接受對互不相關引用的推送。唯一會出現爭用的鍵是引用指標,我們使用類似原子比較並交換的操作來保護它。我們可以透過增加 Pando 伺服器、元數據儲存系統的擴展單元,或 blob 儲存系統,來提升寫入吞吐量。
用戶端執行擷取時,會向伺服器提供兩組提交 ID,在協定中分別稱為 have 和 want。傳統 Git 伺服器會計算從 want 可達、但從 have 不可達的物件集合,將其壓縮,再把結果傳送給用戶端。雖然開源 Git 伺服器在本機設有多種索引來加速這項操作,但產生這些封裝檔可能需要大量運算,而且通常難以有效快取。
Pando 則利用推送時記錄的提交對應關係,查找用戶端的 have 和 want,將它們解析為一組包含相關提交的引用版本。每個引用版本都連結至 Pando 先前收到的一個封裝檔。伺服器利用這些資訊判斷用戶端所需內容,選取能涵蓋所需物件集合的封裝檔。
在 Pando 中規劃擷取作業
Pando 會比較用戶端已有的資料與要擷取的資料,再從已儲存的封裝檔中選出所需檔案,當中包括用來補足分支依賴項目的封裝檔。
擷取 27 個主幹封裝檔。
選取分支,即可納入該分支所依賴的主幹項目。
Pando 將 Git 協定中的 have 和 want 對應至一連串引用版本。請求側分支,也意味著需要取得主幹歷史,直至該分支所依賴的位置。
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 在吞吐量和延遲方面的要求。
Pando 的合併壓實作業
系統會逐層將小型封裝檔組成較大的覆蓋集合,以改善壓縮效果並加快擷取速度。
合併壓實會在背景合併多個引用版本的封裝檔,讓擷取操作只需較少檔案即可更有效率地完成。
與許多 Git 伺服器不同,每一層的封裝檔都會保留,不會丟棄或透過垃圾回收移除。系統會根據用戶端擷取請求中的 have 和 want,持續使用各層封裝檔提供服務。對於透過 API 使用 Pando 的用戶端,從封裝檔讀取任意物件時,讀取 L0 通常比 L4 所需的數據更少,因為物件在 L0 中被壓縮的機會較低。
Pando 的合併壓實不但大幅減少傳送給用戶端的總數據量,也讓封裝檔規劃成本隨用戶端落後程度的增加,維持次線性增長。這對大型且快速變動的程式碼庫尤其重要,而我們預期這類程式碼庫將成為軟件開發的新常態。
我們藉助 Codex 打造 Pando,從項目啟動到正式上線,只用了 10 星期。Pando 的核心由一名工程師設計,多個智能代理團隊負責實作,再由少數幾名工程師部署。Pando 於今年 4 月開始處理正式環境流量,是 OpenAI 所用更大型原始碼控制管理工具體系的一部分。
我們採用了多種方法驗證 Pando 的設計。我們運用了建立更安全軟件基礎的原則。由於 Pando 是從零開始的項目,我們選擇以 Rust 實作,並避免引入舊有依賴。我們編寫了一套符合性測試,讓智能代理可以對照標準 Git 行為測試自己的變更。我們也開發了用於確定性模擬測試的任務執行框架,以檢查系統的不變量和屬性,從而找出並緩解協商及封裝檔規劃中細微的競爭條件和功能退化問題。
多個模型都為這個系統作出了貢獻;每當新模型推出,我們便利用它們提升 Pando 的效能和可靠性。智能代理使用 GPT‑6 Astra 編寫 Lean 和 TLA+,將一套「Pando 理論」形式化,讓我們能找出並修正實作中的錯誤,以及證明系統的屬性。多個工作階段持續運行數百小時,尋找並修正 Pando 的邊界情況問題。我們也讓智能代理運用具備 Daybreak 能力的模型,全天候對 Git 和 Pando 進行紅隊測試。
最初部署時,Pando 是現有原始碼控制系統的讀寫穿透代理。這讓 Pando 與原有系統保持同步,我們因而能在不停機的情況下,安全地將流量轉移至 Pando。
作為快取設計的一部分,Pando 也可以在代理轉送前,對請求去重並合併。這讓我們逐一將系統遷移至 Pando 時,仍能維持現有工具正常運作。隨著更多工作負載遷移至 Pando,它們的推送逐步充實了 Pando 的快取,也提高了原有後端的可靠性。
為優化使用 Pando 時的效能,我們開始在開發者的電腦及 ChatGPT 桌面應用程式中,提供我們公開發佈的 Git 版本。為將用戶端遷移至 Pando,我們透過內部的 Git CLI 包裝工具,分階段進行遷移。我們還建立了許多工具來處理變更請求和程式碼審查,並提供自動化所需的 API。在遷移 Git 流量的同時,我們也遷移了數百項依賴舊有 API、webhook 和程式碼庫下載的整合功能。
Codex 本身也協助開發者和團隊完成遷移。我們建立了相關技能,用來為現有工作副本做好遷移準備,並指引智能代理使用新系統。因此,開發者只需請 Codex 遷移自己的開發環境和服務即可。
切換至 Pando 後,合併可靠性有所提升,推送延遲的中位數亦降低了近 80%。
Pando 只需少數幾個 CPU 核心,每天就能傳送 1 PB 的 Git 數據。Pando 曾在一小時內傳送超過 57 TB 的 Git 封裝檔,其中超過 99% 的數據來自本地磁碟快取。
重用封裝檔,讓大量用戶端能下載同一份程式碼庫歷史,而服務節點只需進行少量運算。這個例子只涉及一個程式碼庫,但 Pando 的設計能支援更多程式碼庫。隨著需求增長,我們可以獨立擴展數據傳送和 Git 協商的處理能力。
以 Git 協定和用戶端為基礎打造 Pando,幫助我們找出了 Git 本身的錯誤和效能改進機會。
例如,Pando 會直接向用戶端提供可重用的封裝檔,因此下載這些封裝檔並建立索引,成為確保系統快速可靠的關鍵環節。這項工作揭示了並行下載、傳輸中斷後清理,以及恢復已完成下載等流程中的邊界情況問題。我們修正了這些處理流程,為擷取操作接受現有封裝檔時加入驗證,並將這些成果貢獻至上游。
我們也需要了解時間和頻寬花在哪裏,因此為 packfile-uri 下載加入追蹤,並記錄封裝檔產生時的位元組數,以便判斷擷取緩慢究竟是耗時於產生封裝檔,還是傳輸封裝檔。
在 OpenAI 的 Git 建置版本(在新視窗中開啟)中,用戶端可以並行下載獨立封裝檔,並為它們建立索引。我們加入了「明確指定的 have」概念,讓伺服器可以分別通告分支目前的頂端提交,以及推送用戶端可從封裝檔省略的歷史。在 Pando 上,這讓用戶端能使用 --force-with-lease 檢查功能分支是否仍停留在原處,同時只以主幹為基礎建立封裝檔。任何持有該主幹歷史的用戶端,都能使用同一封裝檔擷取該分支。
其中幾項改進已納入上游 Git,還有許多改進已收錄於我們的公開程式碼庫。我們正持續與 Git 社群合作,讓這些改進在 Pando 以外也能發揮作用。你可以透過我們的公開修補程式追蹤器(在新視窗中開啟),了解相關工作進展。
我們重新思考擴展 Git 程式碼庫的傳統做法,藉助 Pando 擴展單一程式碼庫,而無需改變工程師和智能代理現有的 Git 工作流程。Pando 讓我們的單一程式碼庫能以更高的並行度運作,提高了合併可靠性,並縮短了發佈程式碼變更所需的時間。除了單一程式碼庫,我們也在探索 Pando 在 OpenAI 內部的其他用途,包括需要大量智能代理在共用 Git 程式碼庫上協作的工作負載。
隨著工程師和智能代理開發更多軟件,OpenAI 以外也會有更多團隊,遇到我們在原始碼控制方面碰到的限制。我們目前正探索如何與用戶分享支撐 Pando 的技術。
Pando 建基於 Git 程式碼庫用戶和營運者二十年來的經驗。我們會繼續開發 Pando,同時與 Git 開源社群合作,分享能讓更多 Git 用戶受惠的改進。


