本文個(gè)人站點(diǎn)鏈接:http://forcemz.net/git/2017/12/06/MassiveRepositoriesAndGit/
前言
通常來說厦幅,分布式版本控制系統(tǒng)適合體積較小的存儲(chǔ)庫幸斥,分布式版本控制系統(tǒng) 意味著存儲(chǔ)庫和工作目錄都放置在開發(fā)者自己的機(jī)器上艺智,當(dāng)開發(fā)者需要克隆一個(gè)巨大的存儲(chǔ)庫時(shí)贿衍,為了獲得完整的拷貝菇用,版本控制軟件不得不從遠(yuǎn)程服務(wù)器上下載大量的數(shù)據(jù)杰刽。這是分布式版本控制系統(tǒng)最大的缺陷之一。
這種缺陷并不會(huì)阻礙 git 的流行蔫缸,自 2008 年以來腿准,git 已經(jīng)成為事實(shí)上的版本控制軟件的魁首,諸如 GCC1拾碌,LLVM2 這樣的基礎(chǔ)軟件也已遷移到或者正在遷移到 git释涛。那么 git 如何解決這種大存儲(chǔ)庫的麻煩呢?
淺克隆和稀疏檢出
很早之前倦沧,我的構(gòu)建 LLVM 的時(shí)候唇撬,都使用 svn 去檢出 LLVM 源碼,當(dāng)時(shí)并不知道 git 能夠支持淺克隆展融。后來從事代碼托管開發(fā)窖认,精通git 后,索性在 Clangbuilder3 中使用 git 淺克隆獲取 LLVM 源碼告希。
淺克隆意味著只克隆指定個(gè)數(shù)的 commit扑浸,在 git 克隆的時(shí)候使用 --depth=N
參數(shù)就能夠支持克隆最近的 N 個(gè) commit,這種機(jī)制對(duì)于像 CI 這樣的服務(wù)來說燕偶,簡直是如虎添翼喝噪。
--depth <depth>
Create a shallow clone with a history truncated to the specified number of commits. Implies
--single-branch unless --no-single-branch is given to fetch the histories near the tips of all branches.
與常規(guī)克隆不同的是,淺克隆可能需要多執(zhí)行一次請(qǐng)求指么,用來協(xié)商 commit 的深度信息酝惧。
在服務(wù)器上支持淺克隆一般不需要做什么事。如果使用 git-upload-pack 命令實(shí)現(xiàn)克隆功能時(shí)伯诬,對(duì)于 HTTP 協(xié)議要特殊設(shè)置晚唇,需要及時(shí)關(guān)閉 git-upload-pack 的輸入。否則盗似,git-upload-pack 會(huì)阻塞不會(huì)退出哩陕。對(duì)于 Git 和 SSH 協(xié)議,完全不要考慮那么多,HTTP協(xié)議是 Request--Respone 這種類型的悍及,而 Git 和 SSH 協(xié)議則沒有這個(gè)限制闽瓢。
而稀疏檢出指得是在將文件從存儲(chǔ)庫中檢出到目錄時(shí),只檢出特定的目錄心赶。這個(gè)需要設(shè)置 .git/info/sparse-checkout
扣讼。稀疏檢出是一種客戶端行為,只會(huì)優(yōu)化用戶的檢出體驗(yàn)园担,并不會(huì)減少服務(wù)器傳輸届谈。
Git LFS 的初衷
Git 實(shí)質(zhì)上是一種文件快照系統(tǒng)枯夜。創(chuàng)建提交時(shí)會(huì)將文件打包成新的 blob 對(duì)象弯汰。這種機(jī)制意味著 git 在管理大文件時(shí)是非常占用存儲(chǔ)的。比如一個(gè) 1GB 的 PSD 文件湖雹,修改 10 次咏闪,存儲(chǔ)庫就可能是 10 GB。當(dāng)然摔吏,這取決于 zip 對(duì) PSD 文件的壓縮率鸽嫂。同樣的,這種存儲(chǔ)庫在網(wǎng)絡(luò)上傳輸征讲,需要耗費(fèi)更多的網(wǎng)絡(luò)帶寬据某。
對(duì)于 Github 而言,大文件耗費(fèi)了他們大量的存儲(chǔ)和帶寬诗箍。Github 團(tuán)隊(duì)于是在 2015 年推出了 Git LFS癣籽,在前面的博客中,我介紹了如何實(shí)現(xiàn)一個(gè) Git LFS 服務(wù)器4滤祖,這里也就不再多講了筷狼。
GVFS 的原理
好了,說到今天的重點(diǎn)了匠童。微軟有專門的文件介紹了 《Git 縮放》5 《GVFS 設(shè)計(jì)歷史》6埂材,相關(guān)的內(nèi)容也就不贅述了。
GVFS 協(xié)議地址為: The GVFS Protocol (v1)
GVFS 目前只設(shè)計(jì)和實(shí)現(xiàn)了 HTTP 協(xié)議汤求,我將其 HTTP 接口整理如下表:
Method | URL | Body | Accept |
---|---|---|---|
GET | /gvfs/config | NA | application/json, gvfs not care |
GET | /gvfs/objects/{objectId} | NA | application/x-git-loose-object |
POST | /gvfs/objects | Json Objects | application/x-git-packfile; application/x-gvfs-loose-objects(cache server) |
POST | /gvfs/sizes | JOSN Array | application/json |
GET | /gvfs/prefetch[?lastPackTimestamp={secondsSinceEpoch}] | NA | application/x-gvfs-timestamped-packfiles-indexes |
GVFS 最初要通過 /gvfs/config
接口去判斷遠(yuǎn)程服務(wù)器對(duì) GVFS 的支持程序俏险,以及緩存服務(wù)器地址。獲取引用列表依然需要通過 GET /info/refs?service=git-upload-pack
去請(qǐng)求遠(yuǎn)程服務(wù)器扬绪。
//https://github.com/Microsoft/GVFS/blob/b07e554db151178fb397e51974d76465a13af017/GVFS/FastFetch/CheckoutFetchHelper.cs#L47
GitRefs refs = null;
string commitToFetch;
if (isBranch)
{
refs = this.ObjectRequestor.QueryInfoRefs(branchOrCommit);
if (refs == null)
{
throw new FetchException("Could not query info/refs from: {0}", this.Enlistment.RepoUrl);
}
else if (refs.Count == 0)
{
throw new FetchException("Could not find branch {0} in info/refs from: {1}", branchOrCommit, this.Enlistment.RepoUrl);
}
commitToFetch = refs.GetTipCommitId(branchOrCommit);
}
else
{
commitToFetch = branchOrCommit;
}
拿到引用列表后才能開始 GVFS clone寡喝。分析 POST /gvfs/objects
接口規(guī)范,我們知道勒奇,最初調(diào)用此接口時(shí)预鬓,只會(huì)獲得特定的 commit 以及 tree 對(duì)象。引用列表返回的都是 commit id。拿到 tree 對(duì)象后格二,就可以拿到 tree 之中的 blob id劈彪。通過 POST /gvfs/sizes
可以拿到需要獲得的對(duì)象的原始大小,通常而言顶猜,/gvfs/sizes
請(qǐng)求的對(duì)象的類型一般都是 blob沧奴,在 GVFS 源碼的 QueryForFileSizes
正是說明了這一點(diǎn)。實(shí)際上一個(gè)完整功能的 GVFS 服務(wù)器實(shí)現(xiàn)這三個(gè)接口就可以正常運(yùn)行长窄。
POST /gvfs/objects
請(qǐng)求類型:
{
"objectIds":[
"e402091910d6d71c287181baaddfd9e36a511636",
"7ba8566052440d81c8d50f50d3650e5dd3c28a49"
],
"commitDepth":2
}
struct GvfsObjects{
std::vector<std::string> objectIds;
int commitDepth;
};
POST /gvfs/sizes
[
"e402091910d6d71c287181baaddfd9e36a511636",
"7ba8566052440d81c8d50f50d3650e5dd3c28a49"
]
對(duì)于 Loose Object滔吠,目前的 git 代碼托管平臺(tái)基本上都不支持啞協(xié)議了,GVFS 這里支持 loose object 更多的目的是用來支持緩存挠日,而 prefetch 的道理類似疮绷,像 Windows 源碼這樣體積的存儲(chǔ)庫,一般的代碼托管平臺(tái)優(yōu)化策略往往無效嚣潜。每一次計(jì)算 commit 中的目錄布局都是非常耗時(shí)的冬骚,因此,GVFS 在設(shè)計(jì)之處都在盡量的利用緩存服務(wù)器懂算。
使用 Libgit2
據(jù)我所知只冻,國內(nèi)最早實(shí)現(xiàn) gvfs 服務(wù)器的是華為開發(fā)者莊表偉,具體介紹在簡書上: 《GVFS協(xié)議與工作原理》计技。我在實(shí)現(xiàn) gvfs 的過程也參考了他的實(shí)現(xiàn)喜德。與他的基于 rack 用 git 命令行實(shí)現(xiàn)的服務(wù)器不同的是,我是使用 libgit2 實(shí)現(xiàn)一個(gè) git-gvfs 命令行垮媒,然后被 git-srv 和 bzsrv 調(diào)用舍悯。采取這種機(jī)制一是使用 git 命令行需要多個(gè)命令的組合,無論是 git-srv 還是基于 go 的 bzsrv 還要處理各種各樣的命令涣澡,不利于細(xì)節(jié)屏蔽贱呐。二來是我對(duì) libgit2 已經(jīng)比較熟,并且也對(duì) git 的存儲(chǔ)協(xié)議入桂,pack 格式比較了解奄薇。
git-srv 是碼云分布式 git 傳輸?shù)暮诵慕M件,無論是 HTTP 還是 SSH 還是 Git 協(xié)議抗愁,其傳輸數(shù)據(jù)都由其前端轉(zhuǎn)發(fā)到 git-srv馁蒂,最后通過 git-* 命令實(shí)現(xiàn),支持的命令有 git-upload-pack git-upload-archive git-receive-pack git-archive蜘腌,如果直接使用 git 命令實(shí)現(xiàn) gvfs 功能不吝于重寫 git-srv沫屡,很容易對(duì)線上的服務(wù)造成影響。簡單的方法就是使用 libgit2 實(shí)現(xiàn)一個(gè) git-gvfs cli.
git-gvfs 命令的基本用法是:
git-gvfs GVFS impl cli
usage: [command] [args] gitdir
config show server config
sizes input json object ids, show those size
pack-objects pack input oid's objects
loose-object --oid; send loose object
prefetch --lastPackTimestamp; prefetch transfer
git-gvfs config
命令用于顯示服務(wù)器配置撮珠,在 brzo 或者 bzsrv 就可以被攔截沮脖,這里保留。
git-gvfs sizes
命令對(duì)應(yīng) POST /gvfs/sizes
請(qǐng)求,請(qǐng)求體寫入到 git-gvfs 的 stdin 勺届,git-gvfs 使用 nlohmann::json
解析請(qǐng)求驶俊,然后使用 git_odb
去查詢所有輸入對(duì)象的未壓縮大小。
pack-objects
命令對(duì)應(yīng) POST /gvfs/objects
請(qǐng)求免姿,輸入的對(duì)象是 commit 時(shí)饼酿,使用 commitDepth 的長度回溯遍歷,取第一個(gè) parent commit胚膊。如果對(duì)象的類型不是 blob故俐,則向下解析,直到樹沒有子樹紊婉。構(gòu)建 pack 可以使用 git_packbuilder
药版,寫入文件使用 git_packbuilder_write
,直接寫入 stdout
用git_packbuilder_foreach肩榕。為了支持緩存刚陡,要先寫入磁盤惩妇,然后從磁盤讀取再寫入到 stdout株汉。
loose-object
即讀取松散對(duì)象寫入到標(biāo)準(zhǔn)輸出。
prefetch
對(duì)應(yīng) GET /gvfs/prefetch[?lastPackTimestamp={secondsSinceEpoch}]|
這里核心是掃描 gvfs 臨時(shí)目錄歌殃。將所有某個(gè)時(shí)間點(diǎn)之后創(chuàng)建的 pack 文件打包成一個(gè) pack乔妈。這里需要對(duì) pack 對(duì)象進(jìn)行遍歷,最初的 pack 遍歷我是使用 Git Native Hook 的機(jī)制氓皱,但后來發(fā)現(xiàn) odb 邊界導(dǎo)致性能不太理想路召,于是我使用 git_odb_new
新建 odb,然后使用 git_odb_backend_one_pack
創(chuàng)建 git_odb_backend
打開一個(gè)個(gè)的 pack 文件波材,使用 git_odb_add_backend
將 odb_backend
添加到 odb股淡,這時(shí)候就可以對(duì) odb 進(jìn)行遍歷,獲得所有的對(duì)象廷区,要?jiǎng)?chuàng)建 packbuilder 需要 git_repositroy
對(duì)象唯灵,因此,可以使用 git_repository_warp_odb
創(chuàng)建一個(gè) fake repo. 代碼片段如下:
class FakePackbuilder {
private:
git_odb *db{nullptr};
git_repository *repo{nullptr};
git_packbuilder *pb{nullptr};
std::vector<std::string> pks;
bool pksremove{false};
std::string name;
/// skip self
inline void removepkidx(const std::string &pk) {
if (pk.size() > name.size() &&
pk.compare(pk.size() - name.size(), name.size(), name) != 0) {
auto idxfile = pk.substr(0, pk.size() - 4).append("idx");
std::remove(pk.c_str()); ///
std::remove(idxfile.c_str());
}
}
public:
FakePackbuilder() = default;
FakePackbuilder(const FakePackbuilder &) = delete;
FakePackbuilder &operator=(const FakePackbuilder &) = delete;
~FakePackbuilder() {
if (pb != nullptr) {
git_packbuilder_free(pb);
}
if (repo != nullptr) {
git_repository_free(repo);
}
if (db != nullptr) {
git_odb_free(db);
}
if (pksremove) {
for (auto &p : pks) {
removepkidx(p);
}
}
}
std::vector<std::string> &Pks() { return pks; }
const std::vector<std::string> &Pks() const { return pks; }
/// packbuilder callback
static int PackbuilderCallback(const git_oid *id, void *playload) {
auto fake = reinterpret_cast<FakePackbuilder *>(playload);
git_odb_object *obj;
if (git_odb_read(&obj, fake->db, id) != 0) {
return -1;
}
if (git_odb_object_type(obj) != GIT_OBJ_BLOB) {
if (git_packbuilder_insert(fake->pb, id, nullptr) != 0) {
git_odb_object_free(obj);
return 1;
}
}
git_odb_object_free(obj);
return 0;
}
std::string Packfilename(const git_oid *id) {
return std::string("pack-").append(git_oid_tostr_s(id)).append(".pack");
}
bool Repack(const std::string &gvfsdir, std::string &npk) {
if (git_odb_new(&db) != 0) {
fprintf(stderr, "new odb failed\n");
return false;
}
for (auto &p : pks) {
auto idxfile = p.substr(0, p.size() - 4).append("idx");
git_odb_backend *backend = nullptr;
if (git_odb_backend_one_pack(&backend, idxfile.c_str()) != 0) {
auto err = giterr_last();
fprintf(stderr, "%s\n", err->message);
return false;
}
/// NOTE backend no public free fun ?????
if (git_odb_add_backend(db, backend, 2) != 0) {
// backend->free(backend);///
if (backend->free != nullptr) {
backend->free(backend);
}
return false;
}
}
if (git_repository_wrap_odb(&repo, db) != 0) {
fprintf(stderr, "warp odb failed\n");
return false;
}
if (git_packbuilder_new(&pb, repo) != 0) {
fprintf(stderr, "new packbuilder failed\n");
return false;
}
if (git_odb_foreach(db, &FakePackbuilder::PackbuilderCallback, this) != 0) {
return false;
}
if (git_packbuilder_write(pb, gvfsdir.c_str(), 0, nullptr, nullptr) != 0) {
return false;
}
auto id = git_packbuilder_hash(pb);
if (id == nullptr) {
return false;
}
pksremove = true;
name = Packfilename(id);
npk.assign(gvfsdir).append("/").append(name);
return true;
}
};
上述 FakePackBuilder 還支持刪除舊的 pack隙轻,新的 pack 產(chǎn)生埠帕,舊的幾個(gè) pack 文件就可以被刪除了。
在 git-gvfs 穩(wěn)定后玖绿,或許會(huì)提供一個(gè)開源跨平臺(tái)版本敛瓷。
GVFS 應(yīng)用分析
GVFS 有哪些應(yīng)用場景?
實(shí)際上還是很多的斑匪。比如呐籽,我曾經(jīng)幫助同事將某客戶的存儲(chǔ)庫由 svn 遷移到 git,遷移的過程很長,最后使用 svn-fast-export 實(shí)現(xiàn)狡蝶,轉(zhuǎn)換后宙刘,存儲(chǔ)庫的體積達(dá)到 80 GB。就目前碼云的線上限制而言牢酵,這種存儲(chǔ)庫都無法上傳上去悬包,而私有化,這種存儲(chǔ)庫同樣會(huì)給使用者帶來巨大的麻煩馍乙。如果使用 GVFS布近,這就相當(dāng)于只下載目錄結(jié)構(gòu),淺表的 commit丝格,然后需要時(shí)才下載所需的文件撑瞧,好處顯而易見。隨著碼云業(yè)務(wù)的發(fā)展显蝌,這種擁有歷史悠久的存儲(chǔ)庫的客戶只會(huì)越來越多预伺,GVFS 或許必不可少了。
相關(guān)信息
在微軟的 GVFS 推出后曼尊,Google 開發(fā)者也在修改 Git 支持部分克隆7酬诀,用來改進(jìn)巨型存儲(chǔ)庫的訪問體驗(yàn)。代碼在 Github 上 8 目前還處于開發(fā)過程中骆撇。部分克隆相對(duì)于 GVFS 最大的不足可能是 FUFS瞒御。而 GVFS 客戶端僅支持 Windows 10 14393 也正是由于這一點(diǎn),GVFS 正因這一點(diǎn)才被叫做 GVFS (Git Virtual Filesystem)神郊。FUFS 能夠在目錄中呈現(xiàn)未下載的文件肴裙,在文件需要讀寫時(shí),由驅(qū)動(dòng)觸發(fā)下載涌乳,這就是其優(yōu)勢蜻懦。
最后
回過頭來一想,在支持大存儲(chǔ)庫的改造上夕晓,git 越來越不像一個(gè)分布式版本控制系統(tǒng)宛乃,除了提交行為還是比較純正。軟件的發(fā)展正是如此运授,功能的整合使得界限變得不那么清晰烤惊。