GitHub 公佈新架構支持每秒 7200 次推送

一個深夜裏,某家科技巨頭的測試集羣中,兩萬個代碼補全與重構 Agent 在三秒鐘內同時發起了 git push,GitHub 的底層存儲節點隨後亮起了密集的超時告警。這不再是黑客發起拒絕服務攻擊,而僅僅是現代 AI 輔助研發系統的一個普通事件。、

上週在 GitHub Universe 上,我們宣佈了新的 Git 存儲架構,能夠處理每秒數千次寫入,並按需擴展讀取。通過內部測試,我們展示了支持 5000 個併發代理運行於倉庫的能力,每秒發送 7200 次推送到倉庫,同時以持續低延遲提供讀取流量。這個新架構是 GitHub 支持數億開發者代理開發的基礎,我們一直在 GitHub 下構建架構,同時它還在運行。

在過去一年裏,代理改變了 GitHub 上繁忙倉庫的樣貌。在本系列的第一篇文章中,我們闡述了下一代 Git 存儲架構背後的兩個原則:

最小化協調,以及將存儲與計算解耦。在我們上一篇文章中,我們講了橋樑:將 99.99999%的倉庫遷移到 Azure,將寫入羣體範圍限定到各個引用,並搭建由 Azure Blob Storage 支持的虛擬副本。

我們還提到了一個觀察:幾乎不需要寫入。Git 對象是不可變的,並以其內容命名,這意味着兩個作者可以同時存儲它們,而無需對任何內容達成一致。推送中需要同意的部分是引用更新,而引用更新相比之下非常微小。虛擬複製品是圍繞這一點構建的第一步,也是我們將倉庫引入新架構的方式。

其餘建築也基於這一觀察。只有參考資料更新需要雙方同意。這使得以前緊密耦合的 Git 基礎設施部分(存儲數據、服務請求和維護倉庫)能夠獨立擴展。對開發者來說,Git 依然是 Git:他們以相同的方式推送、取回和分支,並採用相同的保護措施。下面,我們將帶您瞭解設計背後的需求,按照系統中的請求,並深入探討其中兩個組成部分。

爲代理規模構建

我們將 GitHub 平臺的規模從兩個方向來思考:

  1. 橫向規模是指開發者和代理能跨越多個倉庫工作的數量。GitHub 目前在這一方向上的許多方法多年來經過實戰驗證,並被延續到新架構中:跨基礎設施劃分租戶、租戶流量的隔離和/或公平執行,以及負載感知路由等。

  2. 縱向規模是指一個倉庫中最多的開發者和代理可以工作。隨着代理編寫代碼的增加,我們預計單個倉庫會變得更大更忙,因此我們的大部分需求來自垂直軸。

如今,GitHub.com 最繁忙的倉庫最多在一秒內接收數十次寫入,通常是短時間段。隨着更多工作轉向全天候並行運行的代理,我們預計這些突發工作將在未來一年內持續增長。每次推送都會導致更多讀取:CI 運行,部署系統拉取新提交,其他代理獲取上下文。這種讀取扇出效應對整體流量產生了顯著放大效應,並隨着寫入次數而擴展。

我們已經看到這種趨勢在曲線頂端的倉庫上顯現。下面的圖表顯示了從三月到五月,GitHub 上一個倉庫的每日請求,期間流量增長了大約 4 倍。自那以後,其增長呈指數級增長。

我們必須爲一個代理驅動開發以這種速度持續加速的世界做規劃。我們沒有爲下一個增量做規劃,而是設定了一個至少大兩個數量級的目標。對於單個倉庫來說,這意味着新的架構需要:

  • 每秒接受成千上萬次寫入。

  • 支持多個寫入節點。這有兩個原因:

    • 經驗告訴我們,索引輸入包和檢查對象連接性是寫入中最耗費 CPU 的部分,成本會隨着寫入規模的增加而增加,因此單個節點很快就會被計算限制。

    • 將這些工作分散到多個節點可以提升容錯能力,縮短節點故障時的恢復時間。

  • 最小化提交延遲。每推送一次,特工都會等待,然後才進入下一步。

  • 按每個參考順序記錄變化,並能快速線性化不同引用。落後的虛擬副本需要通過下載滿足來請求所需的數據來趕上。

  • 當讀寫比例沒有變化時,保持讀取成本低廉,這也是讀寫比例的常見情況。

  • 讓每個組件獨立擴展。

此外,系統還必須能夠提升整體可靠性並具備全球擴展性。

介紹“魯比孔計劃”

2026 年 5 月,緊接着最初的 Azure 遷移,我們開始着手設計能夠滿足這些需求的方案。我們嘗試了幾種方法。在所有這些中,包數據都存放在 Azure Blob 存儲中,有些我們還嘗試在那裏保留參考狀態。我們將在下面討論一些權衡,但我們最終確定了一個我們認爲既能最大化吞吐量,又能保證 GitHub 平臺所需的正確性、耐用性、可用性和延遲的設計。我們稱之爲“Rubicon”,向行業與自主開發跨越的無可挽回點致敬。

讓我們從上往下走一遍 Rubicon。許多內容保持不變:來自 Git 客戶端、API 和 Web UI 的請求都會送到 Spokes,Spokes 仍然負責路由和負載均衡,並服務於 GitHub 其他部分用於讀寫倉庫數據的內部 API。Spokes 還保持其作爲安置和平衡服務的角色,保持橫向規模所需的分區、隔離和租戶公平性。相比之下,Spokes 下方的數據層是新的。

Spokes 將每個請求路由到分配給該倉庫的一小組虛擬副本之一。虛擬副本是一組一次性計算工具,負責讀取、寫入或兩者兼有。虛擬副本的本地磁盤是一個可以隨時重建的緩存。

Spokes 會在可能的情況下將倉庫的流量路由到相同的虛擬副本,這樣可以保持緩存溫熱並降低延遲,但請求是否正確性並不取決於請求是否落入了某個特定的虛擬副本。當虛擬副本消失後,倉庫的其他虛擬副本繼續服務,隨着流量到達,替換副本會填滿緩存。

當倉庫變得忙碌時,我們會添加更多的虛擬副本。另外,一組壓縮節點在後臺直接對應 Blob Storage 和 Ref-CAS,用於合併每個倉庫的包並移除不可訪問的對象。這些工作從不在虛擬副本上運行,這意味着它們的 CPU 和內存幾乎可以完全用於處理請求。

持久狀態存在於兩個方面:

  1. Git 對象以包和輔助讀取加速數據的形式存在於 Azure Blob 存儲中。

  2. 引用和構成倉庫的包列表(包清單)存在一個獨立的複製日誌服務中,稱爲 Ref-CAS(代表“參考比較與交換”)。

讓我們從寫與讀的視角走過架構:

  • 寫入: 虛擬副本索引接收包並將其上傳到 Blob 存儲,然後向 Ref-CAS 提交一個事務,該事務在清單中列出包並更新引用。之後,壓縮節點會將該包與其他包合併,以保持讀取速度。

  • 讀取: 虛擬副本用 Ref-CAS 檢查其參考版本,將需要的包從 Blob Storage 拉入緩存,然後從那裏執行讀取。

在新架構中,我們採取了許多不同的方法,從虛擬副本上的軟件棧,到流量路由和工作負載調度如何針對不同用例進行調整(例如,多個小型倉庫與一個大型倉庫)。讓我們深入探討架構的兩個組成部分,並圍繞這兩項設計原則講述它們的故事。

最小化協調:在高通量下改變參考狀態

像 Azure Blob Storage 這樣的對象存儲服務非常出色,非常適合許多數據密集型應用,比如源代碼存儲。它們耐用且可複製,能容納我們所需的容量,每字節成本相對較低,並且只要流量分散在多個密鑰上,就能提供極高的彙總吞吐量。因此,幾乎所有在新 Git 存儲架構中具有持久性的存儲都存儲在 Azure Blob 存儲中,包括:

  • 通過推送、壓縮和垃圾回收編寫的包和索引。

  • 各種讀取加速器(比如多包索引、可達位圖和一些自定義格式),我們的維護流程會從這些包中構建出來。

  • 允許夯實和垃圾收集恢復或審計其工作的記錄,以及夯實節點用來申報工作的租賃合同。

  • 頻繁重啓的緩存響應。

  • 爲倉庫的引用狀態設置檢查點,但不是用於處理請求的權威狀態。

大部分數據寫入一次獨立鍵,之後不變,這也是它非常適合 Blob 存儲的原因。

推薦人則恰恰相反。繁忙倉庫的引用是相對較小的狀態部分,許多寫入會同時爭相更新。這種訪問模式更難從 Blob 存儲中提供,我們的首批原型就展示了原因。對於這種工作負載,對象存儲是一個直觀的選擇,並且在一定規模範圍內表現良好:條件寫入可以免費進行比較和交換,數據本身就已經很耐用且可複製。然而,我們遇到了一系列艱難的選擇,下面通過我們嘗試的兩種方法說明了這一點。

我們的第一個方法是先寫一個記錄倉庫所有引用的日誌,由一個 blob 索引。每次更新都是對那個 blob 的條件寫入。它是正確的,讀取很廉價:虛擬複製品會問斑點是否變了,如果沒有,那就已經變了。寫作部分就比較弱。一次只能對一個 blob 進行一次條件寫入,所以即使我們把每次寫入過程中到達的推送都批處理到下一個 blob,倉庫的寫入上限也是每次對單個 blob 每次往返一次寫入。實際上,這讓我們的寫入吞吐量達到了每秒數百。

第二種方法是將引用拆分到多個段塊,每個引用對其中一個片段進行哈希。不同段落的引用不再相互等待,這提高了上限。但現在讀者必須檢查每個片段,知道有沒有變化,而且速度也取決於最慢的部分。我們用寫延遲換取了讀延遲,而讀入數量遠遠超過寫入。我們可以通過並排展示多次寫入和讀取數據來可視化:

這兩種方案都無法滿足所有要求,因此我們構建了另一種方案:專門用於引用的複製日誌服務,或者 Ref-CAS 服務。這就是我們在上次帖子結尾說過的(小)共識小組,仍然存在於堆棧中。事情是這樣的。

Ref-CAS 中的每次引用更新都是由一個法定人數提交的比較與交換。事務只鎖定其接觸到的引用,檢查每個引用是否仍具備寫入者期望的值,並在日誌副本的大多數記錄後纔可見。代理大多推送到自己的分支,所以大多數更新不會爭用,並且並行複製。當兩次推送更新同一個引用時,日誌中只有一次會附加。

每個對 Ref-CAS 的提交都會獲得一個日誌序列號(LSN),而倉庫的版本就是其最新提交的 LSN。讀取開始時會對該版本進行版本檢查。虛擬副本發送它已經持有的參考狀態版本。如果該版本仍然有效,回覆中完全沒有引用,未更改的倉庫只需進行一次比較。如果虛擬副本落後,它會返回當前的引用和版本,以及打包數據和其他需要知道的信息,比如上一次垃圾回收週期發生的時間。

與我們遺留架構中的共識組不同,Ref-CAS 從不保存或驗證對象數據。它只存儲指向它的指針。虛擬副本會在提交引用事務之前,將推送包上傳到 Blob 存儲。參考點只指向已經耐用的物體。

這些方法使我們能夠以每秒數萬次操作完成引用狀態,併爲複製日誌中額外的註釋提供基線,幫助讀取決定爲特定請求下載什麼。

但支持單一倉庫每秒數千次寫入會帶來二階問題:維持讀寫性能。爲此,我們需要一種不同的壓實方法。

存儲與計算的解耦:每秒數千次寫入時的壓縮

每次推進都會給 Blob Storage 增加一個包。Git 可以從多個包中提供一個倉庫,但每增加一個包,查找成本會略高一些,所以沒有綁定包的數量無法增長。在我們的遺留架構中,壓縮運行在服務流量的副本上,並且必須在每個副本上運行。在新架構中,它完全在獨立節點後臺運行,直接對抗持久存儲,利用 Ref-CAS 服務通知虛擬副本演變狀態。

壓縮節點各自通過租賃協議聲稱對存儲庫數據的子集擁有所有權,這使我們能夠橫向擴展壓縮。以下是一個壓縮節點如何通過倉庫包的單個碎片工作:

節點首先在 Blob Storage 中租用分片,因此每次只有一個節點壓縮一個分片。與此同時,虛擬複製品不斷上傳包裝並在包裝清單中列出。節點讀取清單以尋找新的包,並將其收集到一個開放段,這實際上是即將壓縮數據的緩衝區。當分段達到基於包數、大小或年齡的定義上限時,會封閉分段,不接受新包,然後將包合併成更少、更大的包。在壓縮過程中,分片的下一個段可以開始捕獲其輸入包。在發佈任何內容之前,節點會檢查新包是否包含與輸入完全相同的對象。然後它會上傳新包裝並添加到清單中。第二個事務會從清單中移除輸入,清理則在後續刪除它們。

擴展時,該過程會同時獨立運行多個分片。每個包的哈希映射到一個分片,所以添加分片和節點會增加壓縮吞吐量。一個壓縮包如果仍然低於大小限制,會通過自己的哈希回到分片並再次合併,所以包會分層增長直到達到限制。最終結果是,在推車爬升時,實際上是平坦的包數:

壓縮合幷包,但保留所有對象。垃圾回收是移除不再有引用的對象的步驟,比如刪除分支和強制推送留下的對象。在我們的新架構中,垃圾回收的工作方式與壓縮非常相似,使用專門的工人來處理計算量大的部分。它還有一些用於安全移除數據的特殊行爲,值得單獨討論。

通過這些方法,我們能夠持續展示寫入量增加時的平坦延遲曲線,並且我們還計劃進行其他優化,以提升壓縮和垃圾回收過程的性能。

接下來會發生什麼

我們最初採用的兩個原則是讓架構的每個部分獨立擴展。我們通過添加虛擬副本來擴展讀寫。Ref-CAS 獨立排序每個引用,而非整個倉庫。我們通過添加壓縮節點來擴展維護。我們的目標是讓每個倉庫,從一個開發者的副項目到最忙碌的單倉庫,都能在同一架構上運行,忙碌的倉庫無需特殊處理。倉庫不會一直保持小規模。一個副項目可以一夜之間吸引一批代理,不需要我們進行遷移或特殊處理來跟上。一種架構也意味着我們所做的每一次改進都會同時覆蓋所有倉庫。

這個基礎讓我們能做更多事情。接下來我們將重點關注三個領域:將架構擴展到超出我們目前預測需求的範圍,提高代理在 GitHub 上的工作效率,以及推動 Git 本身以實現代理開發。

改進架構

我們計劃將這種架構進一步發展。這些改進讓倉庫能夠處理更多活動,同時減少每個請求背後的工作:

  • 只讀必要的內容。 從 Blob Storage 裏抓一個對象,而不是整個裝着它的包。

  • 按類型組織包。 將提交、樹狀結構和 blobs 分成自己的包。許多請求因此觸及的數據更少。

  • 部分複製品。 在本地磁盤上不包含所有打包即可提供請求。

  • 具象化的視角。 將倉庫數據保持在現成型表單中,比如準備好的歸檔、幾乎實時的默認分支淺副本,以及與每次提交一起存儲的代理會話上下文。

  • 全球複製品。 將副本放置在任何區域,靠近使用它們的工作負載。閱讀保持在最新寫作的有限陳舊範圍內。

  • 歸零。 用實際活躍的倉庫來擴充和縮小艦隊。

GitHub 上的高效代理操作

擴展基礎設施是支持代理的一個方面。另一方面是讓每個代理操作更便宜:更少的字節取用,減少往返次數,減少等待變更的時間。這些快速跟蹤項目都是在本帖中存儲工作的基礎上進行的:

  • 不用克隆體工作。 許多代理任務從未需要完整的工作樹。他們需要少量文件和足夠的背景信息來做出改變。他們可以直接從服務器讀取兩者。

  • 原子提交 API。 一次完成多文件變更。整個變更要麼提交,要麼在衝突時淨利落地拒絕。

  • 活動流。 代理及其周邊系統在實時信息中運作最佳。通用事件流允許他們訂閱變化並實時響應。

改進代理的 Git

我們也需要在 Git 本身中工作。GitHub 長期以來一直是 Git 項目的貢獻者,我們現在不會停止。以下是我們期待繼續開展的工作示例:

  • 輕量級克隆體。 部分克隆、淺克隆和稀疏結賬都讓代理獲得更少的資源。每個都有空隙。它們也不總是配合得很好。我們希望改進每一部分,使其契合。

  • 移除隱藏的擴展成本。Git 的代碼路徑速度比倉庫增長快得多。最近的版本修復了其中幾個,但我們預計還會發現更多。

  • 歷史步行更快。 像提交圖這樣的元數據可以回答合併基和可達性的問題,而無需每次提交都走動。當代理的 API 調用轉化爲服務器上的 Git 工作時,這一點尤爲重要。

  • 避免常見的陷阱。 代理犯的錯誤和開發者一樣。Git 可以捕捉更多常見的,並在它們造成傷害前發出警告。

代理改變了軟件的構建方式。GitHub 和 Git 也在跟着變化。我們很高興能將這個新架構帶過起點線。目前已提供有限的私人預覽,明年初將有更廣泛的推廣。我們迫不及待想看到當代理數量受限於他們的想法時,團隊會做出什麼樣的作品。我們會與客戶和 Git 社區一起不斷發展 Git 和 GitHub。


更多遊戲資訊請關註:電玩幫遊戲資訊專區

電玩幫圖文攻略 www.vgover.com