Git 巨型存储库的解决方案
Git 巨型存储库的解决方案
Force武装卫队 发表于1个月前
Git 巨型存储库的解决方案
  • 发表于 1个月前
  • 阅读 13
  • 收藏 0
  • 点赞 0
  • 评论 0
摘要: 对于容量较小,历史较少的存储库,git 丝毫没有压力,但当存储库规模达到一定程度时,为了能够让用户很好的使用 git,git 开发者要设计种种巨型存储库的解决方案,gvfs 或许是不错的一个。

前言

通常来说,分布式版本控制系统适合体积较小的存储库,分布式版本控制系统 意味着存储库和工作目录都放置在开发者自己的机器上,当开发者需要克隆一个巨大的存储库时,为了获得完整的拷贝,版本控制yahu999不得不从远程服务器上下载大量的数据。这是分布式版本控制系统最大的缺陷之一。 这种缺陷并不会阻碍 git 的流行,自 2008 年以来,git 已经成为事实上的版本控制yahu999的魁首,诸如 GCC<sup>1</sup>,LLVM<sup>2</sup> 这样的基础yahu999也已迁移到或者正在迁移到 git。那么 git 如何解决这种大存储库的麻烦呢?

浅克隆和稀疏检出

很早之前,我的构建 LLVM 的时候,都使用 svn 去检出 LLVM 源码,当时并不知道 git 能够支持浅克隆。后来从事代码托管开发,精通git 后,索性在 Clangbuilder<sup>3</sup> 中使用 git 浅克隆获取 LLVM 源码。 浅克隆意味着只克隆指定个数的 commit,在 git 克隆的时候使用 --depth=N 参数就能够支持克隆最近的 N 个 commit,这种机制对于像 CI 这样的服务来说,简直是如虎添翼。 --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. 与常规克隆不同的是,浅克隆可能需要多执行一次请求,用来协商 commit 的深度信息。 在服务器上支持浅克隆一般不需要做什么事。如果使用 git-upload-pack 命令实现克隆功能时,对于 HTTP 协议要特殊设置,需要及时关闭 git-upload-pack 的输入。否则,git-upload-pack 会阻塞不会退出。对于 Git 和 SSH 协议,完全不要考虑那么多,HTTP协议是 Request--Respone 这种类型的,而 Git 和 SSH 协议则没有这个限制。 而稀疏检出指得是在将文件从存储库中检出到目录时,只检出特定的目录。这个需要设置 .git/info/sparse-checkout。稀疏检出是一种客户端行为,只会优化用户的检出体验,并不会减少服务器传输。

Git LFS 的初衷

Git 实质上是一种文件快照系统。创建提交时会将文件打包成新的 blob 对象。这种机制意味着 git 在管理大文件时是非常占用存储的。比如一个 1GB 的 PSD 文件,修改 10 次,存储库就可能是 10 GB。当然,这取决于 zip 对 PSD 文件的压缩率。同样的,这种存储库在网络上传输,需要耗费更多的网络带宽。 对于 Github 而言,大文件耗费了他们大量的存储和带宽。Github 团队于是在 2015 年推出了 Git LFS,在前面的博客中,我介绍了如何实现一个 Git LFS 服务器<sup>4</sup>,这里也就不再多讲了。

GVFS 的原理

好了,说到今天的重点了。微软有专门的文件介绍了 《Git 缩放》<sup>5</sup> 《GVFS 设计历史》<sup>6</sup>,相关的内容也就不赘述了。 GVFS 协议地址为: The GVFS Protocol (v1) GVFS 目前只设计和实现了 HTTP 协议,我将其 HTTP 接口整理如下表:
MethodURLBodyAccept
GET/gvfs/configNAapplication/json, gvfs not care
GET/gvfs/objects/{objectId}NAapplication/x-git-loose-object
POST/gvfs/objectsJson Objectsapplication/x-git-packfile; application/x-gvfs-loose-objects(cache server)
POST/gvfs/sizesJOSN Arrayapplication/json
GET/gvfs/prefetch[?lastPackTimestamp={secondsSinceEpoch}]NAapplication/x-gvfs-timestamped-packfiles-indexes
GVFS 最初要通过 /gvfs/config 接口去判断远程服务器对 GVFS 的支持程序,以及缓存服务器地址。获取引用列表依然需要通过 GET /info/refs?service=git-upload-pack 去请求远程服务器。 //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 接口规范,我们知道,最初调用此接口时,只会获得特定的 commit 以及 tree 对象。引用列表返回的都是 commit id。拿到 tree 对象后,就可以拿到 tree 之中的 blob id。通过 POST /gvfs/sizes 可以拿到需要获得的对象的原始大小,通常而言,/gvfs/sizes 请求的对象的类型一般都是 blob,在 GVFS 源码的 QueryForFileSizes 正是说明了这一点。实际上一个完整功能的 GVFS 服务器实现这三个接口就可以正常运行。 POST /gvfs/objects 请求类型: { "objectIds":[ "e402091910d6d71c287181baaddfd9e36a511636", "7ba8566052440d81c8d50f50d3650e5dd3c28a49" ], "commitDepth":2 } struct GvfsObjects{ std::vector<std::string> objectIds; int commitDepth; }; POST /gvfs/sizes [ "e402091910d6d71c287181baaddfd9e36a511636", "7ba8566052440d81c8d50f50d3650e5dd3c28a49" ] 对于 Loose Object,目前的 git 代码托管平台基本上都不支持哑协议了,GVFS 这里支持 loose object 更多的目的是用来支持缓存,而 prefetch 的道理类似,像 Windows 源码这样体积的存储库,一般的代码托管平台优化策略往往无效。每一次计算 commit 中的目录布局都是非常耗时的,因此,GVFS 在设计之处都在尽量的利用缓存服务器。

使用 Libgit2

据我所知,国内最早实现 gvfs 服务器的是华为开发者庄表伟,具体介绍在简书上: 《GVFS协议与工作原理》。我在实现 gvfs 的过程也参考了他的实现。与他的基于 rack 用 git 命令行实现的服务器不同的是,我是使用 libgit2 实现一个 git-gvfs 命令行,然后被 git-srv 和 bzsrv 调用。采取这种机制一是使用 git 命令行需要多个命令的组合,无论是 git-srv 还是基于 go 的 bzsrv 还要处理各种各样的命令,不利于细节屏蔽。二来是我对 libgit2 已经比较熟,并且也对 git 的存储协议,pack 格式比较了解。 git-srv 是码云分布式 git 传输的核心组件,无论是 HTTP 还是 SSH 还是 Git 协议,其传输数据都由其前端转发到 git-srv,最后通过 git-* 命令实现,支持的命令有 git-upload-pack git-upload-archive git-receive-pack git-archive,如果直接使用 git 命令实现 gvfs 功能不吝于重写 git-srv,很容易对线上的服务造成影响。简单的方法就是使用 libgit2 实现一个 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 命令用于显示服务器配置,在 brzo 或者 bzsrv 就可以被拦截,这里保留。 git-gvfs sizes 命令对应 POST /gvfs/sizes 请求,请求体写入到 git-gvfs 的 stdin ,git-gvfs 使用 nlohmann::json 解析请求,然后使用 git_odb 去查询所有输入对象的未压缩大小。 pack-objects 命令对应 POST /gvfs/objects 请求,输入的对象是 commit 时,使用 commitDepth 的长度回溯遍历,取第一个 parent commit。如果对象的类型不是 blob,则向下解析,直到树没有子树。构建 pack 可以使用 git_packbuilder,写入文件使用 git_packbuilder_write,直接写入 stdoutgit_packbuilder_foreach。为了支持缓存,要先写入磁盘,然后从磁盘读取再写入到 stdout loose-object 即读取松散对象写入到标准输出。 prefetch 对应 GET /gvfs/prefetch[?lastPackTimestamp={secondsSinceEpoch}]| 这里核心是扫描 gvfs 临时目录。将所有某个时间点之后创建的 pack 文件打包成一个 pack。这里需要对 pack 对象进行遍历,最初的 pack 遍历我是使用 Git Native Hook 的机制,但后来发现 odb 边界导致性能不太理想,于是我使用 git_odb_new 新建 odb,然后使用 git_odb_backend_one_pack 创建 git_odb_backend 打开一个个的 pack 文件,使用 git_odb_add_backendodb_backend 添加到 odb,这时候就可以对 odb 进行遍历,获得所有的对象,要创建 packbuilder 需要 git_repositroy 对象,因此,可以使用 git_repository_warp_odb 创建一个 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 产生,旧的几个 pack 文件就可以被删除了。 在 git-gvfs 稳定后,或许会提供一个亚虎娱乐官方app跨平台版本。

GVFS 应用分析

GVFS 有哪些应用场景? 实际上还是很多的。比如,我曾经帮助同事将某客户的存储库由 svn 迁移到 git,迁移的过程很长,最后使用 svn-fast-export 实现,转换后,存储库的体积达到 80 GB。就目前码云的线上限制而言,这种存储库都无法上传上去,而私有化,这种存储库同样会给使用者带来巨大的麻烦。如果使用 GVFS,这就相当于只下载目录结构,浅表的 commit,然后需要时才下载所需的文件,好处显而易见。随着码云业务的发展,这种拥有历史悠久的存储库的客户只会越来越多,GVFS 或许必不可少了。

相关信息

在微软的 GVFS 推出后,Google 开发者也在修改 Git 支持部分克隆<sup>7</sup>,用来改进巨型存储库的访问体验。代码在 Github 上 <sup>8</sup> 目前还处于开发过程中。部分克隆相对于 GVFS 最大的不足可能是 FUFS。而 GVFS 客户端仅支持 Windows 10 14393 也正是由于这一点,GVFS 正因这一点才被叫做 GVFS (Git Virtual Filesystem)。FUFS 能够在目录中呈现未下载的文件,在文件需要读写时,由驱动触发下载,这就是其优势。

最后

回过头来一想,在支持大存储库的改造上,git 越来越不像一个分布式版本控制系统,除了提交行为还是比较纯正。yahu999的发展正是如此,功能的整合使得界限变得不那么清晰。

链接

  1. Moving to git
  2. Moving LLVM Projects to GitHub
  3. Checkout LLVM use --depth
  4. Git LFS 服务器实现杂谈
  5. Git at scale
  6. GVFS Design History
  7. Make GVFS available for Linux and macOS
  8. jonathantanmy/git
标签: git gvfs 码云 gitee
共有 人打赏支持
Force武装卫队
粉丝 170
博文 35
码字总数 69446
作品 3
×
Force武装卫队
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
* 金额(元)
¥1 ¥5 ¥10 ¥20 其他金额
打赏人
留言
* 支付类型
微信扫码支付
打赏金额:
已支付成功
打赏金额: