Git 巨型存儲(chǔ)庫的解決方案

本文個(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,直接寫入 stdoutgit_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_backendodb_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ā)展正是如此运授,功能的整合使得界限變得不那么清晰烤惊。

鏈接

  1. Moving to git
  2. Moving LLVM Projects to GitHub
  3. Checkout LLVM use --depth
  4. Git LFS 服務(wù)器實(shí)現(xiàn)雜談
  5. Git at scale
  6. GVFS Design History
  7. Make GVFS available for Linux and macOS
  8. jonathantanmy/git
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市吁朦,隨后出現(xiàn)的幾起案子柒室,更是在濱河造成了極大的恐慌,老刑警劉巖逗宜,帶你破解...
    沈念sama閱讀 218,607評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件雄右,死亡現(xiàn)場離奇詭異空骚,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)擂仍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,239評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門囤屹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人逢渔,你說我怎么就攤上這事肋坚。” “怎么了肃廓?”我有些...
    開封第一講書人閱讀 164,960評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵智厌,是天一觀的道長。 經(jīng)常有香客問我盲赊,道長铣鹏,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,750評(píng)論 1 294
  • 正文 為了忘掉前任哀蘑,我火速辦了婚禮诚卸,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘绘迁。我一直安慰自己合溺,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,764評(píng)論 6 392
  • 文/花漫 我一把揭開白布脊髓。 她就那樣靜靜地躺著辫愉,像睡著了一般栅受。 火紅的嫁衣襯著肌膚如雪将硝。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,604評(píng)論 1 305
  • 那天屏镊,我揣著相機(jī)與錄音依疼,去河邊找鬼。 笑死而芥,一個(gè)胖子當(dāng)著我的面吹牛律罢,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播棍丐,決...
    沈念sama閱讀 40,347評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼误辑,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了歌逢?” 一聲冷哼從身側(cè)響起巾钉,我...
    開封第一講書人閱讀 39,253評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎秘案,沒想到半個(gè)月后砰苍,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體潦匈,經(jīng)...
    沈念sama閱讀 45,702評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,893評(píng)論 3 336
  • 正文 我和宋清朗相戀三年赚导,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了茬缩。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,015評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡吼旧,死狀恐怖凰锡,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情圈暗,我是刑警寧澤寡夹,帶...
    沈念sama閱讀 35,734評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站厂置,受9級(jí)特大地震影響菩掏,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜昵济,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,352評(píng)論 3 330
  • 文/蒙蒙 一智绸、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧访忿,春花似錦瞧栗、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,934評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至卧斟,卻和暖如春殴边,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背珍语。 一陣腳步聲響...
    開封第一講書人閱讀 33,052評(píng)論 1 270
  • 我被黑心中介騙來泰國打工锤岸, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人板乙。 一個(gè)月前我還...
    沈念sama閱讀 48,216評(píng)論 3 371
  • 正文 我出身青樓是偷,卻偏偏與公主長得像,于是被迫代替她去往敵國和親募逞。 傳聞我的和親對(duì)象是個(gè)殘疾皇子蛋铆,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,969評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容