Enroute 的作者主張,把 Git 放上物件儲存的主流做法都做錯了,理由是不管 WAL 寫得多快,每一次 push 最後仍得落在磁碟上的單一 repository。作者自家 benchmark 報出單一 repo 每秒超過 7,600 次 push,這是廠商自測的數字,得連同條件一起讀。
Git 放上 S3,三條路線的取捨
Enroute 這篇文章比較的是三條把 Git 放上物件儲存(例如 S3)的路線。第一條是傳統的磁碟式 Git,文章附的例子是 GitHub 的 Spokes 架構;第二條是把 write-ahead log 放在 S3 上的做法,Cursor 在八月描述的 Continuity 與幾個開源實作都走這個方向;第三條是 Enroute 自己的設計,由 ledger、無狀態的 compute,以及一種按 commit 切開的 segment 格式組成。作者的立場很明確,他認為前兩條路線都沒有真正解決寫入。
讀這篇之前要先講清楚一件事。文章裡的效能數字,Enroute 與 GitHub 的部分作者明說是「our own forgemark runs」,也就是 Enroute 團隊自己用 forgemark 跑出來的;Cursor 與 Entire 的數字則是對方自己回報、由這篇文章轉引。沒有任何一個數字經過獨立第三方重現。下面每個數字我都會標出是誰量的,也不會把不同環境的數字排成同條件的對決。
先看一張總覽。格子內容取自文章對三條路線的描述,數字一律是該方自己提出的。
寫入:每次 push 最後落在哪裡
磁碟式 Git 的成本結構,文章用三句話交代。磁碟會壞,所以要 RAID 或備份;伺服器會壞,所以每個 repository 要有副本才能高可用;磁碟遲早會滿,所以 repository 得分片,還要在一群檔案伺服器之間重新平衡。作者說這類託管長年「relied heavily on SSDs as the primary storage mechanism」。這個選擇有它的理由,因為 Git 的 client 與資料模型本來就假設有快速的隨機存取儲存。
物件儲存剛好把這三件事都接走。文章的說法是「a bucket is durable, replicates itself and it never fills up」,對照上一段的三項成本,桶各有一個現成的答案。所以把 repository 放進桶裡是很自然的想法,難處在 Git 習慣的存取方式,這留到下一節談。先看第二條路線怎麼處理寫入。
WAL-on-S3 的代表是 Cursor 與 SpaceX 在八月描述的 Continuity,他們以 S3 為底的 Git 儲存。作者承認這類實作「successful in moving the burden of scalability and durability into object storage」,擴展性與耐久性的負擔確實移進了桶裡。他的批評有兩點。其一,這些做法讀取擴展得好,寫入密集時卻容易撐不住;其二,它們仍依賴會用 memory map 讀寫磁碟檔案的標準 Git CLI。
Cursor 回報的寫入數字是這樣:在 S3 Standard 上最高每秒 120 次 push,在 S3 Express One Zone 上超過 300 次。作者拿它當 WAL 路線的參照,並指出瓶頸不在 WAL 的速度:「no matter how fast the WAL is, every push still has to land in one repository on disk.」照他的推論,不管你開幾台伺服器,一個 repository 能吞下多少,仍取決於單一機器套用與壓縮的速度。文章對這條路線的整體評語,就是它實質上是單一寫入者的系統。
下面這個滑桿把三個「回報」數字放在同一條對數軸上。拖動它代表你預期的單一 repo push 速率,看哪些回報數字涵蓋得到。三個數字出自不同團隊、不同環境,這張圖只能顯示各家回報的量級,不能當成同條件的對決。
拖動滑桿設定目標 push 速率 · 3 個回報數字 · 對數軸
目標 209 次/秒:Cursor S3 Express(300 以上)、Enroute(7,600 以上)的回報數字涵蓋;Cursor S3 Standard(120)不足。
預設的每秒 209 次落在 S3 Standard 的 120 與 S3 Express 的 300 之間。哪些工作負載會逼近這個速率,文章只提到 forgemark 用來模擬「large numbers of concurrent agents」同時推向一個共享 repository 的分支,也就是大量 agent 並行寫入同一個 repo 的情境。如果你的 repo 主要是人在推,速率大概遠低於圖的左半段,這是我的推測,文章沒有提供任何人類使用者的 push 速率。
Enroute 的回應是只讓 ref 更新排隊,不讓整個 push 排隊。文章說一個 push 的大部分工作發生在提交之前,包括索引 pack、驗證每個物件並重新編碼供儲存,compute 節點可以平行做這些事而不必加鎖;真正按順序套用的只有 commit,方式是對該 push 要移動的 ref 做 compare-and-swap。推到不同分支的 push 彼此不衝突,讀取走不可變的快照,所以讀者不會餓死寫入者。照我的整理,序列化的範圍因此從整個 repository 縮小到一次 ref 的 CAS。
7,600 這個數字的來源值得講細一點。作者說在這些測試裡,Enroute 對單一 repository 的峰值超過每秒 7,600 次 push,圖片的替代文字(alt text)寫的是 7,619。環境只有一句「Running in the cloud on EC2 and S3」,我讀到的版本沒有給執行個體型號或叢集規模。作者還把數字放進脈絡,說它約是 Entire.io 回報數字的 18 倍,也是 GitHub 目前能處理的「more than 1,000 times」。
這兩個倍數的條件寫在同一段裡:作者承認「the numbers come from slightly different setups but give a good sense on the order of magnitude」。換句話說,18 倍與 1,000 倍是量級示意,不是排名。文章另有一張對 walgit(作者形容為「a write-ahead log on local disk」)的比較圖,從 1 個到 512 個並行 pusher,圖中數字沒有出現在內文,我不複述。
讀取:一個物件一個 key 的代價
把 Git 放上桶最直接的做法,是把每個物件當成一個 key。文章說這行不通,原因是「Each object is its own key, and each GET only tells you which keys to fetch next.」要走完一個 commit 的物件圖,得先讀 commit 才知道 tree,讀了 tree 才知道 blob,每一層都是一次往返。作者的估算是,如果只把 Git 當成物件儲存上的 content-addressed storage,這些步驟「every one of these steps would be a 30-40ms round trip.」另一處他也寫到,桶上的 conditional GET 或 PUT 要數十毫秒,物件儲存擅長的是靜態放著的資料。
Enroute 的解法是改變資料的切法。每個 commit 記成一個 segment,裡面放的是相對於其 parent 的差異物件;連續的 segment 組成 layer,也就是尾端帶目錄的不可變檔案。一個 commit 的 segment 加上它祖先的 segment,就握有從這個 commit 可達的全部資料。作者還重建了 delta:base 只能從該 segment 的祖先裡挑,而且「a delta only points to the same file in the parent commit」,所以手上已有 parent 的 client 可以直接套用。
伺服器的工作因此變成把從請求的 commit 可達、而 client 還沒有的資料全部送出。文章的例子是 client 缺 C3 到 C6,就拿到剛好那四個 segment,「with no tree walk」。我的整理是,這把原本要在物件圖上一步步走的讀取,換成按 commit 範圍讀幾段連續的資料,每一步往返的成本就不再按圖的深度累加。這句話是我對文章的歸納,文章沒有用同樣的措辭。
這個格式有代價,文章自己列了三項。同一個物件可能出現在好幾個 commit 的 segment 裡,但合法的 packfile 每個物件只能出現一次,Enroute 的做法是建物件清單時去重,串流 segment 時略過重複的。shallow clone 沒有任何 parent commit,在邊界上以 delta 存放的物件必須完整送出,Enroute 批次重建並快取結果,同一個邊界的重複 shallow clone 因此可以很快。用 --filter=blob:none 的 partial clone 會跳過檔案內容、之後再單獨取檔,layer 把同類物件放得更近,讓這種過濾比較便宜。
三項代價有一個共通點:都出在 segment 這種「按 commit 切開」的資料組織,與 Git 協定要求的 packfile 形狀之間的落差。重複物件要去重,是因為同一個物件被不同 commit 各存一份;shallow 邊界要重建,是因為 delta 的 base 在 client 手上根本不存在;partial clone 則要靠 layer 內的物件分群,才不必把不要的內容一起讀出來。文章的處理方式是去重、批次重建加快取、把同類物件放近,三者都是在伺服器端補洞,沒有要求 client 改變行為。合理的推測是,你的工作負載裡 shallow clone 與 partial clone 愈多,這幾個補洞機制就愈常被用到,不過文章沒有量測每一項補洞本身的成本,這點只能自己測。
讀路徑上,compute 節點先讀 refs 與 layer 的 metadata,只抓需要的 chunk,存在本機磁碟給下一次請求用;需要的資料不在磁碟時,它用平行的 ranged GET 去桶裡取。文章給了一個冷啟動的數字:磁碟全空的節點服務 linux.git 的完整 clone 用了 128.6 秒,暖快取則是 126.0 秒。這同樣是作者自測,差距約 2.6 秒,作者的結論是「even a cold cache costs less than you might think」。
狀態放在哪:ledger、無狀態 compute 與桶
Enroute 把狀態分到三處。桶存資料,compute 只放快取,中間夾一層 ledger:一小群節點,每個 repository 用 Raft 共識維護一份複製日誌,日誌裡記著 repository 的 refs 與組成它資料的 layer 清單。layer 的定義是「an immutable file of Git objects with its own index, much like a Git pack」。作者對整個設計的總括是「a new kind of git server that is based on modern database principles」。
為什麼要多一層 ledger?文章給的理由是延遲。桶上一次 conditional GET 或 PUT 要數十毫秒,而 ledger 提交一筆日誌通常只要 1-2 毫秒的網路與磁碟延遲,「not a bucket round trip」;group commit 讓同一瞬間到達的所有 push,跨所有 repository,共用一次 fsync。ref 更新在兩個 ledger 節點收到之後,就在毫秒內提交。合理的推測是,這就是 push 的關鍵路徑能避開桶的原因,不過這句因果是我整理的,文章沒有這樣寫成一句話。
無狀態這件事文章說得很絕:「Compute is fully stateless and holds nothing but cache.」節點不放任何不能由 ledger 與桶重建的東西。這讓 compute 可以隨時替換與增減,代價是冷節點慢。作者在未完成清單裡寫「The fetch performance of a cold compute node lies far below what is possible once the node is warmed up and the cache is hot.」,也承認目前並不協調哪個 compute 節點服務哪個 repository。
小 push 的處理是這個設計的另一半。小 push 先寫進 ledger 的日誌;約每秒一次,一個 compute 節點把日誌裡的 layer 合成一個檔案上傳,文章的說法是「one upload for about 300 pushes」。只有大於 1 MiB 的 layer 才直接上傳到桶,再以 ledger 項目提交。layer 累積到約八個大小相近時,會合併成一個大約八倍的 layer,合併只複製位元組、不重新壓縮,成本相對低。長期留在 ledger 節點上的,只有 refs 與 layer 清單。
下面的模擬把這條時間線畫出來。滑桿調每秒 push 次數,按開始後每 0.4 秒代表模擬的一秒:日誌裡的 push 累積,滿一秒就 flush 成一個 layer 上傳,同一層湊滿八個就合併成上一層的一個。這是照文章的「約」值畫的示意,假設只有一個 compute 節點,也沒有模擬 1 MiB 直傳與實際位元組大小,不是 Enroute 的實測。
調 push 速率並按開始,看 layer 如何上傳與合併 · 3 個層級
已模擬 0 秒,push 0 次,上傳 0 次,合併 0 次。
示意模擬,flush 間隔與八個合併取文章的「約」值,假設單一 compute 節點,未模擬 1 MiB 直傳與實際大小
小 push 先進 ledger 日誌,約每秒合成一個 layer 上傳,同層約八個再合併成大八倍的一個,所以桶內上傳次數只隨時間增加,與 push 速率無關。
在模擬裡把滑桿從 600 拉到 1 再比較,上傳次數都只隨時間增加,和 push 速率無關,因為模擬的前提就是每秒 flush 一次。這是模擬的設定造成的結果,不是 Enroute 的量測。真實系統裡還有多個 compute 節點、1 MiB 以上的直傳,以及合併發生的時機,文章只給了「約」值。這張圖要表達的只有一件事:桶上昂貴的往返次數,被設計成和 push 次數脫鉤。
作者自己的 benchmark 說了什麼
寫入的成績前面講過了,讀取側的結果比較不討好,這也是文章誠實的地方。對照組是 stock Git 2.56.0,由 Apache 後面的 git http-backend 服務,用的是 linux.git 重新 repack 過的副本;Enroute 則分別測暖快取與冷快取。作者寫 Enroute「managed to shine on the shallow benchmark」,shallow clone 快了兩倍以上;partial clone 持平;完整 clone 慢約 17%,增量 fetch 慢約 13%。他補充暖快取主要改善的是 shallow clone 與增量 fetch。
慢的原因,作者指向 client 端:他寫「Interestingly, the biggest performance gap is now on the client side.」,並說 Git client 花在驗證 pack 與解 delta 的時間,比伺服器送出它的時間還長。他把更聰明的 delta base 選擇列為待改善項目。合理的推測是,這和 segment 格式把 delta 限制在 parent 的同一檔案有關,但文章沒有把兩者連成因果,這只是我的推測。
文章結尾的「還沒做完」清單相當長,我挑幾項對採用者有影響的。layer 不可變,所以刪掉的分支或 force-push 會留下不可達的物件,作者說需要一套不阻塞讀取與 push 的方式去找出它們並重寫 layer。目前只講 Git over HTTP smart protocol,SSH 與一些較少用的協定部分仍缺。冷 compute 節點的 fetch 效能遠低於暖節點,現在每個節點的快取方式也都一樣,作者想讓快取內容與時間可以設定。他開頭就寫「But it is only the start」。文章最後說 Enroute 是開源的,repo 的 README 寫明採 Apache-2.0。
把這份清單攤開看,有兩組項目彼此牽動。第一組是垃圾回收:layer 不可變,刪除與 force-push 留下的不可達物件只能靠重寫 layer 來清,而且作者要求這件事不能阻塞讀取與 push,這比一般的離線整理嚴格。第二組是冷啟動、快取與路由:冷節點的 fetch 遠低於暖節點,目前每個節點的快取方式相同,又沒有協調哪個 compute 節點服務哪個 repository。合理的推測是,這三項疊在一起,意味著新加入的節點要花一段時間才會進入作者所說的暖狀態,但文章把「measure how quickly a new node gets fast on large repositories」列為待做,也就是這段時間目前還沒有數字。
怎麼選
三條路線回答的是不同問題,先看你的瓶頸在哪。若你的負載以讀為主,單一 repo 的寫入速率在 Cursor 回報的量級(每秒 120 到 300 以上)以內,文章沒有給出證據說現有的磁碟式託管或 WAL-on-S3 在這種負載下有問題;Enroute 在完整 clone 與增量 fetch 上,作者自己的數字還略慢。這是我對文章數字的讀法,不是作者的建議。
若你的瓶頸是大量 agent 並行推同一個 repo,也就是 forgemark 模擬的情境,這才是 Enroute 想解的問題,作者主張它的設計拿掉了單一寫入者的上限。但你不該直接採信 7,600 這個數字:它是廠商自測,環境只有 EC2 與 S3 一句話,GitHub 與 Entire 的對照又來自不同設定。文章連到 forgemark 的 repo,我沒有讀它,要驗證就該在自己的環境用它重跑。
若你要把它當作對外服務的託管底層,文章自己的清單就是風險清單:SSH 尚缺,垃圾回收還沒做,冷節點慢,快取行為不可設定。作者用「only the start」形容現況,合理的推測是它目前更適合拿來評估與壓測,不適合直接承載重要資料,這是我的判斷,不是文章的結論。
如果要實際評估,我會先回答四個問題,答案都得從你自己的環境取得,文章給不出來。第一,單一 repo 的 push 速率是多少,尖峰是否逼近 Cursor 回報的 120 到 300 以上這個量級。第二,你的讀取以哪種操作為主:作者自測裡 shallow clone 快兩倍以上、partial clone 持平、完整 clone 與增量 fetch 分別慢約 17% 與 13%,如果你的 CI 大量做完整 clone,這兩個百分比就是你最該重測的數字。第三,你是否依賴 SSH,因為文章明說目前只講 Git over HTTP smart protocol。第四,你怎麼處理被刪除的分支與 force-push,因為不可變 layer 留下的不可達物件目前沒有現成的回收辦法。四題裡有任何一題的答案落在文章尚未涵蓋的範圍,就該把 Enroute 放在評估名單而不是採用名單。
How to choose:先量你單一 repo 的寫入速率,若在每秒百次上下就不需要換架構;只有當大量 agent 並行寫入同一個 repo 成為瓶頸,Enroute 的 ledger 加 segment 設計才值得評估,而且所有數字都來自作者自己,請用文章附的 forgemark 在自己的環境重跑一次。