帶你了解Git大倉庫
前言
Git 是目前世界上最為廣泛使用的軟件版本控制系統(tǒng)(Version Control System),同時也是一個成熟及活躍的開源項目。

在git-scm.com點擊git圖標,你可以看到如下幾句話:
--fast-version-control (快捷的版本控制) --local-branching-on-the-cheap (廉價的本地分支) --distributed-is-the-new-centralized (分布式是新的集中式) --distributed-even-if-your-workflow-isnt(分布式,且與你的工作流無關(guān)) --everything-is-local(一切本地化)
Git 最初是由 Linux 之父 Linus Torvalds 在 2005 年創(chuàng)建,截至目前共有1500多名貢獻者參與其中。
Git使用范圍雖廣,但仍有一個普遍的共識:Git并不適合用于維護巨型的倉庫。接下來,讓我們一起來深入了解一下其中的原因,以及有哪些可以優(yōu)化的手段。
1.什么是大倉庫?
提到大倉庫,大家通常會聯(lián)想到單倉Monorepo。
業(yè)內(nèi)最出名的幾家monorepo實踐分別是:
Google,基于 Perforce[1] 開發(fā)而來的Piper。 Facebook,基于 Mercurial[2] 定制化實現(xiàn)。 Microsoft,基于 Windows 虛擬文件系統(tǒng)及 Git 開發(fā)而成的 GitVFS[3] 。
以上這些 Monorepo 實踐,無一不依賴內(nèi)部/特定的基礎(chǔ)設(shè)施;并且,想要落地并不是解決了代碼托管這一個問題就可以萬事大吉,還需要整個研發(fā)生態(tài)能力的支持。
在Monorepo基礎(chǔ)設(shè)施建設(shè)尚不完善的今天,大多數(shù)廠商都選擇基于Git來嘗試探索Monorepo實踐。那么,讓我們把焦點聚焦在Git單體大倉庫(即單體大于100GB)。
2.Git大倉庫帶來哪些挑戰(zhàn)?
2.1 存儲挑戰(zhàn)
我們第一個要面臨的就是如何存一個巨大的Git單倉。
在通常的認知當(dāng)中,Git倉庫是一個完整的個體(不考慮shallow clone的情況下)。Git 之所以稱為分布式版本控制系統(tǒng),也正是由于它區(qū)別于 SVN 等中央版本控制系統(tǒng), 其最大差異點在于,在每一個倉庫的使用者那里,都維護了完整的 Git 倉庫數(shù)據(jù),任何人都可以選擇將自己本地的內(nèi)容作為中心副本來維護。
在今天,不論是我們的個人電腦還是服務(wù)器,存儲容量都已經(jīng)大幅升級;所以看來,將100GB的倉庫存儲到本地,似乎并不是什么難題。那假如,類比Google PB(1PB=1024TB)級別的代碼資產(chǎn),都存儲到Git當(dāng)中,你是否仍然可以把它下載到本地呢(??)?答案顯然是否定。
有人會說,那使用共享存儲(如NAS),是否可以解決存儲的問題呢?
答案是該方案會遇到較大的性能挑戰(zhàn),讓我們一起往下看。
2.2 性能挑戰(zhàn)
性能挑戰(zhàn)主要在讀和寫兩個方面,讓我們先來看看寫性能挑戰(zhàn)。
2.2.1 并發(fā)協(xié)作(寫)
如果一個倉庫只是幾個人維護,那即使這個倉庫的體積變得非常大,有些問題還是可以忍受的;但挖大坑(??) 的,往往是一個很大的團隊(數(shù)千人,甚至數(shù)萬人)。
以千人實踐主干模式為例,你可能需要:
清晰的管理方案:上千個松散引用(引用 reference refs/*,常見的分支branch refs/heads/*及標簽tag refs/tags/*都屬于引用的范疇)來維護每個人的特性。 更為合理的多副本架構(gòu),來解決單點負載不足問題。 一個良好的Checkin機制,來解決root tree爭奪問題。

更為順暢的垃圾回收機制,避免倉庫陷入無限GC循環(huán)(默認6700個松散對象觸發(fā)autogc,可能一個GC還未結(jié)束,又一次新的GC在路上了)。
2.2.2 大范圍查找(讀)
在前面的圖中,我們也可以看到,Git是通過commits樹來串聯(lián)整個版本歷史的。在不借助額外的索引的情況下,可以認為git的對象是離散存儲在文件當(dāng)中的。
Git的對象存儲包含兩個部分:
松散對象:存儲在 objects/xx 目錄下,每個文件就是一個對象。 包文件(packfile):存儲在 objects/pack 目錄下,相較于松散對象,pack是一組對象的集合,擁有一個索引文件 pack-xxx.idx;當(dāng)然,在倉庫較大的情況下,還會包含多個打包文件。
? objects git: tree
.
├── 03
│ └── 273f5843529db977846d7c6fd28dc790123d38
├── 7f
│ ├── ec94d35df31a1deb570f8b863526a27f148f48
│ └── ff37186bcf8a8f5428aa168f981c9094bef2e6
├── info
└── pack
├── pack-0c63ce8bd48a11517c3f1775d9060d45c088afc5.idx
├── pack-0c63ce8bd48a11517c3f1775d9060d45c088afc5.pack
├── pack-47155f8be24f5b6666bf849d681f831d5f34bffe.idx
└── pack-47155f8be24f5b6666bf849d681f831d5f34bffe.pack查找指定對象的過程,如 git cat-file -t xxx。

如圖所示,在上述沒有多包索引的倉庫中,如果我們想要根據(jù)一個hash值來查找指定對象,首先需要遍歷松散對象目錄查找是否存在于松散對象,而后再逐個查詢打包文件,在打包文件較多的情況下,逐個遍歷索引的效率也并不高。
如果你說,性能問題我忍了,那我是不是可以高枕無憂(??)?答案也是否定的。
2.3 穩(wěn)定性挑戰(zhàn)
二八定律同樣也適用在代碼托管領(lǐng)域。
而極為少數(shù)的單體大倉庫,往往能得到額外的優(yōu)待:
獨享的機器資源 更多的技術(shù)支持占用 更高的技術(shù)關(guān)注度
人肉炸彈??——某次生產(chǎn)環(huán)境代碼存儲節(jié)點上的內(nèi)存使用變化:

而類似問題,可能還需要在Git當(dāng)中進行優(yōu)化才能得以解決,參考《unpack-objects: support streaming blobs to disk》[4]。
2.4 可靠性挑戰(zhàn)
讓我們再來思考一個問題,對于存儲類的服務(wù),我們要守住的底線是什么?
我認為,必須要守住的底線是數(shù)據(jù)完整性。我們可以接受一定時間的服務(wù)不可用,我們可以通過各種手段,提升我們的服務(wù)可用率;但若是出現(xiàn)數(shù)據(jù)完整性問題,那對用戶帶來的影響是更加大的。
雖然Git自帶的hashsum,可以解決數(shù)據(jù)完整性校驗問題,但解不了所有問題,因為:
Git是IO密集 大倉庫加劇了IO消耗
3.如何應(yīng)對Git單體大倉庫?
看完了問題,讓我們再來看看有哪些解決方案。
3.1 事前預(yù)防
3.1.2 如何控制倉庫膨脹?
在不考慮 submodule 及 Git-repo[5] 進行邏輯拆分的情況下,如何控制倉庫的體積?
我們先來看,導(dǎo)致倉庫體積超過預(yù)期的原因都有哪些?
大型的二進制文件(如圖片資源、可執(zhí)行程序、Office文檔等)。
# 取top20的大文件
git rev-list --objects --all | grep "$(git verify-pack -v .git/objects/pack/*.idx | sort -k 3 -n | tail -20 | awk '{print$1}')"
# 取大于500k的大文件
git rev-list --objects --all | grep "$(git verify-pack -v .git/objects/pack/*.idx | awk '{if($3>500000)print $1}')"大量的無用引用(如早先版本的gitlab在添加文件等場景下會創(chuàng)建tmp引用,并缺少清理機制)。
git的GC機制僅會移除不可達的對象(不存在于任何一個引用上)。 龐大的文件數(shù)據(jù)及版本記錄(針對這個問題,我們需要區(qū)分對待)。
例如:iOS的應(yīng)用依賴管理工具Cocoapods。
對癥下藥:
對于二進制文件,可以借助 https://github.com/git-lfs/git-lfs[6],將文件上傳到對象存儲。
Git-lfs 的落地,依賴客戶端的安裝,存在一定的成本,但確是上選。 對于非預(yù)期的提交,添加 pre-commit hook 做本地攔截也是一個好選擇。 活用.gitignore,排除編譯產(chǎn)物、非必要依賴等的提交。 引用清理:
本地經(jīng)常性進行開發(fā)分支清理并GC是一個不錯的選擇。 選擇更合理的存儲服務(wù):
對版本要求不高的場景,對象存儲(Object Storage,如火山云產(chǎn)品TOS[7])的成本更為低廉。
3.1.3 如何高效識別用戶大文件提交?
行業(yè)內(nèi)的普遍做法
通過pre-receive hook,對隔離區(qū)(Quarantine,objects/incoming-xxxx)中的對象大小進行識別,其中松散對象可以通過文件頭中的size來判斷,packfile則通過git verify-pack 。
但是這個方案的效率并不高:
需要遍歷隔離區(qū)的所有對象,事先并不知道哪些對象是commit、哪些是blob。 verify-pack的核心用途是校驗packfile的完整性,對讀取完整的數(shù)據(jù);而我們的場景,只需求文件大小。
更優(yōu)的方案
在2021年11月與來自Github的Peff(Jeff King)的交流中,得到了新的啟發(fā):
https://lore.kernel.org/git/[email protected]/
We also set GIT_ALLOC_LIMIT to limit any single allocation. We also have custom code in index-pack to detect large objects (where our definition of "large" is 100MB by default): - for large blobs, we do index it as normal, writing the oid out to a file which is then processed by a pre-receive hook (since people often push up large files accidentally, the hook generates a nice error message, including finding the path at which the blob is referenced) - for other large objects, we die immediately (with an error message). 100MB commit messages aren't a common user error, and it closes off a whole set of possible integer-overflow parsing attacks (e.g., index-pack in strict-mode will run every tree through fsck_tree(), so there's otherwise nothing stopping you from having a 4GB filename in a tree).
我們可以在執(zhí)行 index-pack / unpack-objects 的過程中,將對象的oid、類型、大小記錄在額外的文件中,在后續(xù)pre-receive hook執(zhí)行的時候,就可以根據(jù)已有的結(jié)果來做展示信息加工。在這個過程中,無需再遍歷所有的對象及packfile整體,復(fù)用了數(shù)據(jù)接收過程,這對大型倉庫的效率提升是顯著的。
3.2 事中提效
3.2.1 如何下載一個Git大倉?
通常會遇到如下問題:
引用發(fā)現(xiàn)慢 對象計算久 網(wǎng)絡(luò)不穩(wěn)定中斷
可以怎么做:
使用 protocol version 2
以 https://github.com/kubernetes/kubernetes 為例,如果下載該倉庫的全量引用,總共有近10w,而如果僅關(guān)心 branches 及 tags,那么僅有不到 1k。
10:39:01.435687 pkt-line.c:80 packet: clone> ref-prefix HEAD
10:39:01.435692 pkt-line.c:80 packet: clone> ref-prefix refs/heads/
10:39:01.435696 pkt-line.c:80 packet: clone> ref-prefix refs/tags/Git 在 2.18以后就開始支持新的協(xié)議,在更新的版本中,更是將 v2 作為默認協(xié)議,在這個協(xié)議下可以有更好的表達空間。
如果你的Git版本不高,可以考慮增加設(shè)置使用v2:
git config --global protocol.version=2使用 shallow clone
如果你不那么關(guān)心歷史版本,shallow clone是一個不錯的選擇。
git clone --depth=100 [email protected]:kubernetes/kubernetes.git使用 partial clone[8]
比如我也想試試本地維護一個linux,同時也想看看這個擁有100w個commits倉庫的演進歷史,我可以這么做:
git clone --filter=blob:none [email protected]:torvalds/linux.git使用bundle
Git當(dāng)中,提供了將所有對象及引用打包的能力 git bundle,借助對象存儲及CDN,可以對文件進行分段讀取,在網(wǎng)絡(luò)條件不好的情況下,真的可以救命。
目前Git社區(qū)的 Derrick Stolee 及 ?var Arnfj?re Bjarmason 正在推進bundle uri能力的落地,這將更好地改善大倉庫的下載體驗。
https://lore.kernel.org/git/[email protected]/
3.2.2 如何在減小本地工作空間
好不容易把一個Git的倉庫下載下來,往往檢出又成了難題。
Git倉庫中的文件都是經(jīng)過壓縮的,而解壓縮之后,體積往往成倍膨脹開來;而對于一個大庫,我們可能只關(guān)心其中的某個路徑,這時候,就輪到 git sparse-checkout 登場了。
下圖摘自:[《Bring your monorepo down to size with sparse-checkout》9]

順帶一提,微軟的大倉庫客戶端scalar目前也已經(jīng)進入git的子項目進行孵化,后續(xù)可以在git當(dāng)中使用。
https://github.com/git/git/tree/master/contrib/scalar
Scalar is an add-on to Git that helps users take advantage of advanced performance features in Git. Originally implemented in C# using .NET Core, based on the learnings from the VFS for Git project, most of the techniques developed by the Scalar project have been integrated into core Git already:
partial clone,
commit graphs,
multi-pack index,
sparse checkout (cone mode),
scheduled background maintenance,
etc
3.2.2 如何提升訪問效率?
在前文“2.2 性能挑戰(zhàn)”當(dāng)中,我們已經(jīng)了解了Git如何存儲及查找對象的方式。
對于Git訪問來說,影響訪問效率的核心在于需要解決兩個問題:
這個對象是什么? 這個對象和其他對象的關(guān)系是什么?
Git大倉庫的性能優(yōu)化,一直也來也是社區(qū)非常關(guān)注的問題,為此,社區(qū)引入了幾個特性:
1. 回答關(guān)系的問題:
Git當(dāng)中引入了commit-graph[10],通過記錄commit的root tree、parents、date等信息,加速了commits遍歷的效率。Commit-graph 可以通過
core.commitGraph配置進行開啟。
2. 回答是什么的問題:
Git當(dāng)中引入了bitmap[11],通過 Commits、Trees、Blobs、Tags 4個位圖,在無需讀取對應(yīng)對象的頭信息的情況下,就可以知道對象的類型及位置信息。
這里有人會說,bitmap 只能解決 單個packfile的場景,多個packfile就失效了。
在多個packfile的情況下,
core.multiPackIndex開啟多包索引,進行packfile的索引合并,也可以加速對象索引的過程。此外,在Git的 v2.34.0 當(dāng)中,引入了multi-pack-bitmap,至此針對于多包場景下的幾何打包策略(geometric repack,參見《Scaling monorepo maintenance》[12])開始登上舞臺。
3.3 事后優(yōu)化
3.3.1 優(yōu)化存量倉庫
坦言,今天對于合理發(fā)展的存量大倉庫,并沒有太多可以瘦身的手段。
而對于“3.1.2 如何控制倉庫膨脹?”中提到的不合理的場景,我們可以借助 git filter-branch 等手段進行歷史版本重寫,移除其中的大對象。
這個操作本身存在較大的風(fēng)險,并且在版本重寫之后,所有用戶本地的副本也要重新從遠端拉取,在執(zhí)行成本上也是非常高的。
3.3.2 增量冷備
bundle冷備無論是為了bundle uri能力預(yù)設(shè),還是預(yù)防非預(yù)期問題上,都是代碼安全能力建設(shè)的一道重要防線。而在傳統(tǒng)的全量備份方案上,Git單體大倉極大提升了備份的周期及難度。
而隨著增量bundle能力的支持,針對大倉庫的冷備也不再那么困難,我們可以基于先前的備份做增量補丁。
增量bundle支持:https://lore.kernel.org/git/[email protected]/
小結(jié)
我們從Git單體大倉庫入手,了解了其帶來的存儲、性能、可靠性等方面的挑戰(zhàn),并且我們從事前、事中、事后三個角度提出了針對一些問題的解決/優(yōu)化方案。
當(dāng)然,今天所提到的也只是幾個比較經(jīng)典的問題,還有更多的問題及解決方案等待我們在實踐中持續(xù)探索。
附錄
Perforce:https://www.perforce.com/
Mercurial:https://www.mercurial-scm.org/
VFSForGit:https://github.com/microsoft/VFSForGit
《unpack-objects: support streaming blobs to disk》:https://lore.kernel.org/git/[email protected]/
git-repo:https://gerrit.googlesource.com/git-repo
git-lfs:https://github.com/git-lfs/git-lfs
TOS:https://www.volcengine.com/product/tos
partial clone:https://git-scm.com/docs/partial-clone
《Bring your monorepo down to size with sparse-checkout》:https://github.blog/2020-01-17-bring-your-monorepo-down-to-size-with-sparse-checkout/
commit-graph:https://github.com/git/git/blob/master/Documentation/technical/commit-graph.txt
bitmap:https://github.com/git/git/blob/master/Documentation/technical/bitmap-format.txt
Scaling monorepo maintenance:https://github.blog/2021-04-29-scaling-monorepo-maintenance/
