Meta 开源 Sapling 客户端,推动 monorepo 模式发展

1. Sapling 是什么?

Facebook Meta 悄悄 发布 新的版本管理工具 - Sapling 。网站首页打出 "A Scalable, User-Friendly Source Control System"Slogan , 直击当前最普遍使用的版本管理工具 Git 的痛点。

Git 作为全世界开发者都在使用的版本管理工具,也是有一定学习曲线。 当然如果知道 clonepullcommitpush 四个命令就可以完成日常 90% 以上的工作,所以少有人知道 Git 版本管理核心是 Snapshot ,很多人错误的以为 Git 的逻辑是 diffGitHub 这些年做出了很多努力帮助开发者使用 Git,像 LFS 协议 和它的实现就是一个突出的贡献。 GitHub 是通过自身的服务,降低 Git 的使用门槛,并形成了一个完备的生态系统 "绑架" "俘虏" 了开发者的心。 Sapling 是不是真的对开发者友好,我没有深入使用并没有发言权,目前我的研究方向是在版本管理工具的 "Scalable" 能力上。

谈起版本管理工具的 Scalable ,就要先讲 monorepo 的概念了。众所周知,"不存在" 的互联网三大巨头 GoogleFacebookTwitter 三家是采用 monorepo 模式,只是各家技术栈不同。

Google 的版本管理系统叫做 Piper ,在 Piper 的相关论文中提到截止到 2015 年已经存储了 9 百万个文件,使用了 86 TB 存储空间,估计现在存储的文件应该达到 billion 级别了。 PiperGoogle 通过修改 Perforce 并把它构建在其强大的基础设施之上,实现了海量的单一代码仓存储能力,同时 Google 还有一个自己的客户端 CitC ,能够兼容 Git 在本地使用。

Facebook 的版本管理系统 Sapling 是在 Mercurial 的基础做了修改,使其能够支持 monorepo 模式的开发,此次开源的就是这个系统的客户端。 而 Twitter 公司是在 Git 的基础上做增强使其能够支持 monorepo 模式。对于 FacebookTwitter 目前系统存储的容量少有公开的报道。

Sapling 的文档看,其存储逻辑、命令和使用方式已经是独立发展,我觉得可以和 Mercurial 一样,算作是一个独立的版本管理工具。期望 Meta 能尽快把配套的 服务端 开源,让我们一窥 Sapling 的真容。

2. Git、Mercurial 和 Sapling 底层存储逻辑的差异

当研究 MonorepoScalable 的问题时,我发现多数版本管理系统产生和发展都是在开源社区中。开源社区的代码仓库体量相对比较小,和任意一个中型公司的代码仓库相比都是几个数量级的差距。但是开源社区的另一个显著的特点就是分散性,贡献代码的开发者居住在世界各地,所以开源社区的版本管理系统的第一要解决的问题就是分布式。既要开发者本地有完整的版本可以进行开发,又要能够将本地的版本通过邮件、HTTP 等协议推送到远程的版本仓库中,这就是 GitMercurial"产品" 思路。

这几年我从事开发和运营开源项目都是按照 "产品" 思路去考虑,发现这样容易看懂开源项目的本质。

要保证开发人员本地拥有完整代码,所以多数版本管理工具会采用文件系统,通过各种编码等方式来存储源代码文件。Git 把所有文件保存在 .git 目录,Mercurial 把保存在 .hg 目录。这两个目录都是隐藏的,所以开发者在开发的时候是看不到的。这两个目录中存储的是 GitMercurial 的版本管理元数据,包括文件的版本信息、文件的变更信息、文件的历史版本等等。当开发的时候,GitMercurialcheckout 一个指定版本的代码到 工作目录 ,所有的工作都这这个目录下进行,当开发完成后,再将这个目录下的代码提交到 Git 或者 Mercurial 的版本仓库中。

2.1 Git Internals - Content-addressable FileSystem

这里引用 Git Internals 这个视频的内容和图片,这个系列的视频是我看到讲解最清楚的教程。

Git 把开发者提交文件的二进制完整保存在一个 blob 对象中。 不论是添加新文件还是修改文件的部分内容,都会 全量 保存文件的二进制数据。 Git 对每个 blob 对象都计算 SHA1 值做为这个对象的 ID

Git 把一个或者多个 blob 对象的 ID 保存在一个 tree 对象中,同时还在 tree 对象中记录了这个文件的名字, tree 对象就是每次开发者新加、修改的文件 清单Gittree 对象计算 SHA1 值做为对象的 ID

Git 最后把 tree 对象的 ID 保存在 commit 对象中, commit 对象同时包含了提交的作者名字、邮件地址、提交时间、描述和 parent tree 等信息。 Git 对每个 commit 对象计算 SHA1 值做为对象的 ID

这样形成了一个 Git 版本管理的数据链路,如下图所示:

重点的是,每次文件修改都会保存一个新的 blob 对象,通过这样的 link 方式,可以形成一个完整的仓库的历史版本。

2.2 Mercurial Internals

Mercurial 在存储上和 Git 完全不同, Mercurial 使用 revlog 的格式来存储对象,每个文件的不同版本都保存中一个文件中。

$ hg debugindex .hg/store/data/src/pb.c.i
rev   offset  length   base linkrev nodeid       p1           p2
0        0     467      0    10  a7bdd2379025 000000000000 000000000000
1      467     168      0    12  692932a95c0d 000000000000 a7bdd2379025
2      635     173      0    15  f1d9cb4201e4 692932a95c0d 000000000000
3      808     476      0    17  d238a6113e4c f1d9cb4201e4 000000000000
4     1284     491      0    18  b71d299270a5 f1d9cb4201e4 000000000000
5     1775     470      0    19  4a7ebb32f962 b71d299270a5 d238a6113e4c
6     2245      64      0    20  6b99ca4dde14 4a7ebb32f962 000000000000
7     2309     177      0    21  33557d969679 d238a6113e4c 000000000000
8     2486     213      0    22  e4d67566afd0 6b99ca4dde14 33557d969679
9     2699     102      0    23  ab4bcfb966f8 33557d969679 000000000000
10    2801     384      0    24  86d19e47e6d0 e4d67566afd0 000000000000
11    3185      88      0    25  4969c00e0bc8 86d19e47e6d0 ab4bcfb966f8

revlog 格式定义两个文件,.d 文件包含实际的文件数据,.i 文件是索引文件,记录不同版本在 .d 文件中 offset ,使得检索起来速度更快。 在 .d 文件中即可以存储文件的完整内容(这跟 Gitblob 对象的逻辑是一样的),也可以存储和上一个版本的二进制差异(这与 GitPack 文件存储逻辑相通,关于 Pack 文件的格式请自行 Google 之)。为了提升检索速度,每隔一定版本 Mercurial 会保存一个全量文件的快照,这样版本回溯的时候就大大节省了时间。

对应 Gittree 对象,Mercurial 也有类似的概念,叫做 manifestmanifest 保存了文件的名字和 revlog 的索引文件的对应关系,这样就可以通过文件名字来找到对应的 revlog 文件。

对应 GitCommit 对象,Mercurial 使用的是 changeset ,也是包含了一次提交的各种信息。

虽然存储方式和 Git 完全不同,但是 Mercurial 也是可以形成一个完整的版本管理的数据链路。

   .--------linkrev-------------.
   v                            |
.---------.    .--------.    .--------.
|changeset| .->|manifest| .->|file    |---.
|index    | |  |index   | |  |index   |   |--.
`---------' |  `--------' |  `--------'   |  |
    |       |      |      |     | `-------'  |
    V       |      V      |     V    `-------'
.---------. |  .--------. |  .---------.
|changeset|-'  |manifest|-'  |file     |
|data     |    |data    |    |revision |
`---------'    `--------'    `---------'

我对 Mercurial 的研究不多,如果以上描述有错误请 TG 联系我,ID genedna

2.3 Sapling Internals

Sapling 采用了一个叫 IndexedLog 的格式来存储对象,这个格式是 Sapling 自己设计的,目的是为了提升 Sapling 的性能,解决 MercurialGit 的各种问题。 它的目标:

  1. O(log N) 查找,不需要重新打包以保持性能。这个主要是针对 Git 面对大仓库时需要把对象打包成 Pack 文件的问题,这样会导致查找性能下降。
  2. 通过哈希值插入,没有拓扑顺序限制。 这个主要是针对 Mercurial.d 文件是 append 的方式修改,无法在中间插入新的对象。

SaplingIndexedLog 对应的是 Mercurialrevlog ,结构上采用了一个 log 文件和多个 index 文件的方式。log 文件对应 Mercurial.d 文件,index 文件对应 Mercurial.i 文件,但是 Sapling 有多种 index

  1. Index 是对应 Rust 的函数,是没有办法序列化使用,相当于应用在内存中的索引。
  2. Standalone Index 是相当于应用在磁盘上的索引,类似于 RustBTreeMap<Vec<u8>, LinkedList<u64>>结构,可以序列化和反序列化。

总结

Sapling 的介绍文章和文档来看,目前放出的只是一个客户端,如果面对大仓库的话,还差虚拟文件系统和服务端。虚拟文件系统的思路有点像微软解决 Git 超大仓库采用的方式;延迟下载 的逻辑其实 Git 也可以采用 Shallow Clone --depth=1 --single-branch --branch=<branch> 的方式获取 HEAD 指向的 Snapshot,而不是全部下载。

对于提交,不管是那种版本管理工具都对提交的内容进行了哈希,有的存储全量快照,有的存储二进制的差异。如果以全量快照的方式来看,哈希值是 Key ,提交的文件或者内容是 Value ,形成了一组唯一的 Key-Value。如果采用大型的分布式数据库对这些 Key-Value 对进行存储,那么就可以实现一个分布式的大型版本管理系统。其实 Google 的版本管理系统 Piper 正是采用这种逻辑将数据存储到它的全球一致性数据库 Spanner 中。

仅仅进行存储还不能解决我们生产中的问题,其中最主要的有两个:

  1. 对于 monorepo 需要采用 Trunk Based Development 的研发模型,这对很多公司都是不可想象的,尤其是是很多开发者已经习惯 GitHub PR 或者基于 Git Flow 的开发模型,同时也对研发管理、代码 Review、生产部署等带来巨大的冲击。
  2. 对于 monorepo 必须有一个超大型的 构建系统 进行支撑。Google 开源了 BazelFacebook 开源了 BuckTwitter 开源了 Pants 。 没有这样大型的构建系统能力, monorepo 也是水中望月而已。

Sapling 的底层存储使用 Rust 开发,Mercurial 的底层存储也启动了 Rust 的改造计划,gitoxide 是目前最完善的 RustGit 客户端实现。 加上还不被大家所知晓的使用 Rust 开发,号称下一代版本管理工具的 Pijul,可以看到版本管理系统在用 Rust 重构底层存储的趋势,Rust 的明星项目有望会在这个领域脱颖而出。