Git 通常被视为一个文件系统问题:`.git` 目录看起来像一个文件夹,所以在云端托管 Git 的自然方式就是给它一个文件系统,然后把这个文件系统放在对象存储之上。一位开发者正是这样尝试的,结果发现它无法在真实世界的仓库中站住脚。其结果是 objgit,一个开源的 Git 服务器,它将 Git 的对象直接存储在对象存储中——具体来说是 Tigris——并用一个专为远程、字节可寻址存储设计的自定义格式替换了 Git 自带的 packfile 机制。
要理解为什么这是必要的,有助于从 Git 实际存储什么开始讲起。一个仓库从根本上来说是两样东西:一片对象之海,以及指向其中单个对象的具名引用。每个提交、树和文件 blob 都是一个独立的、内容寻址的、压缩的对象,其文件名是其字节的哈希——同样的内容始终是同一个对象,处处如此。在一个很小的仓库中这并不起眼:磁盘上有三个对象,每个都是一个普通的 zlib 压缩文件。但 Linux 内核的检出大约包含 1180 万个对象,而 Git 并不会把它们作为 1180 万个文件来保存。它将它们打包进 packfile——压缩归档,将许多对象存储在一个单一文件中——正是因为把它们作为松散文件存储会触及文件系统 inode 限制。

文章解释说,仓库从根本上来说是一片内容寻址对象的海洋,加上指向它们的具名引用,而 Git 将这些对象打包进 packfile 中,而不是保留数百万个松散文件。这个迷因捕捉了其中反直觉的部分:小脑假设认为 Git 为每个版本保留完整文件是错误的,这正是 packfile 格式对对象存储至关重要的原因。
Packfile 正是对象存储转换崩溃的地方。Git 是围绕本地存储设计的:它不断将 packfile 写入磁盘并立即读回,并且对它们进行内存映射,使操作系统的页面缓存像服务 RAM 一样服务热数据。在那个世界中,一次文件系统读取大约需要十纳秒。一次到对象存储的网络往返至少需要十毫秒——作者描述这至少慢了一百万倍。Git 还依赖于一个 Unix 惯用写法,即写入一个文件然后直接读回以计算其哈希,而对象存储无法做到这一点,因为你无法获取一个尚未写完的对象。
更深层的障碍是 packfile 索引。每个 packfile 都有一个伴随的 `.idx` 文件,它告诉 Git 每个对象在 pack 中的哪个位置。索引为每个对象记录其偏移量和解压后的大小——但不记录其压缩后的大小。这一区别对对象存储是致命的。对象存储通过 HTTP 范围请求来寻址:要从一个 128 兆字节的 pack 中取出一个单一对象,客户端需要知道该对象的起始字节和长度,然后向存储桶请求恰好这一段。由于索引省略了压缩长度,你无法仅从索引计算出这一段,也无法以低成本通过其他方式发现它。作者认为,这就是更早的将 Git 存储在对象存储中的尝试之所以艰难的核心原因——不是因为范围请求不可用,而是因为 Git 的索引格式没有提供使用它们所需的信息。
作者的解决方案借鉴了物理媒体的一个想法。CD cue sheet 是一个纯文本文件,列出了音轨以及它们在一个更大的 WAV 文件中开始的偏移量,因此播放器可以直接跳转到任何音轨,而无需扫描整张光盘。Objgit 的替代格式对 Git 对象做了同样的事情,但采用二进制、固定宽度的布局。一个小的 `objects.cue` 文件包含一个 16 字节的头部——魔数、版本、记录大小和记录数量——后面跟着每条 58 字节的记录,因此记录 N 位于字节偏移量 `16 + N * 58` 处。每条记录携带对象的哈希、类型、压缩方法、大小,以及其字节在单个大 `objects.bin` 文件中的偏移量和长度。因此,检索任何对象只需要对 cue 文件进行一次寻道、一次查找,以及对二进制 blob 的一次 HTTP 范围请求——线上传输 366 字节,而不是整个 pack。
回报是这在生产规模上可以工作,而无需更改客户端的任何东西。作者报告说,这种方法在真实世界规模的仓库中表现良好,该设计被描述为让 Git 对象感觉像对象存储对象的最不突兀的方式。底层存储层在 FoundationDB(一个分布式键值存储)中保存指向那片对象海洋中各个点的具名引用,作者将其称为一种递归:对象存储保存对象,而对这些对象的引用则存储在另一个类似对象的系统中。
博客文章背后的项目 objgit 更完整地展示了这作为产品会是什么样子。它托管在 tigrisdata/objgit,是一个 Git 服务器——不是现有 forge 的代理——它支持 SSH、HTTP 和 Git 协议,在用户态沙箱中运行 post-receive 钩子,并通过预签名传输支持 Git LFS,使大文件直接在客户端和 Tigris 之间移动,而不是通过服务器。它还按树获取 EROFS 快照,暴露基本的 Prometheus 指标,并通过 Kustomize 提供 Kubernetes 部署。它目前没有身份验证,作者将其列为一个已知的缺口,如果项目证明有用就需要填补。
两点注意事项应与此相伴。首先,性能声明——文件系统 shim 对真实世界仓库来说太慢,以及新格式在生产规模下工作良好——来自项目自己的作者及其博客,而不是来自独立的基准测试;提供的仓库页面证实了项目的功能集,但并未验证测量结果。其次,作者指出的生态系统差距是真实的:诸如 GitLab、Forgejo 和 Gitea 之类的主流 Git 服务器仍然期望其仓库使用本地文件系统,因此 objgit 目前是一个自托管的、早期阶段的替代方案,而不是一个即插即用的替代品。cue-sheet 这个想法是否被证明足够通用以至于可以在其他地方被采用,目前还是一个悬而未决的问题。
这项工作所展示的,较少是 Git 可以被迁移到云端——那在之前已经被尝试过——而更多是特定的瓶颈从来不是网络或协议,而是 Git 从以磁盘为中心的时代继承的索引格式。替换那一个部分,而不是将 Git 包裹在文件系统层中,才使对象存储作为 Git 后端成为可行。