关于 Monorepo 的一点想法

Google、Facebook 的单一代码仓库已闻名遐迩,不过在没有真正接触过实际的 Monorepo 项目之前,可能很难想象 Monorepo 是怎么工作的。我在学校里的时候也没太理解,直到去了微信实习才看到了真正的 Monorepo,当然他们不是全 BG 共享代码,实际上单个 repo 的规模要比 Google 那种全公司共享要小,使用也不是特殊的客户端,就是普通的 git,也没有使用 git-vfs 之类的,而是把代码全拉下来开发。不过,虽然只是 Google 真正的 Monorepo 的模仿,也能管中窥豹,大致理解 Monorepo 的特点。

最大的感受是,量变引起质变,当代码仓库量级大到一定程度时,工作的流程和平时的小仓库相比就完全不同了。

Monorepo 的优点是:

  • 透明的依赖管理,上游模块很容易知道被哪些下游模块依赖,修改后能一次性全测试
  • 唯一的依赖版本,永远都是使用最新的依赖版本,避免了菱形依赖等麻烦的情况
  • 代码共享,所有开发者都能看到所有的代码,方便搜索学习以及重用
  • 方便的使用依赖,开发者只需要加入依赖的路径到自己的编译文件中就可以使用其他人的依赖,不需要另外引入包管理之类的
  • 统一的构建系统,开发者无需操心怎么编译,一条命令就可以把二进制构建出来
  • 方便进行大规模的重构

缺点也很明显:

  • 管理成本高,Monorepo 需要大量开发自研工具链才能使用
  • 引入外部依赖麻烦,这里的外部依赖是指开源项目等,一般需要适配好构建系统

代码版本管理系统

目前最主流的代码版本管理系统 Git 对于大规模的代码仓库支持并不算太好,尽管可以用,但速度上比较慢。所以,Google 开发了 Piper,Facebook 开发了 Mecurial,来解决规模变大之后的效率问题。

云端工作

Monorepo 存储了大量代码,容量可能数以 TB 计,这种量级的代码哪怕是在内网拉取也得耗费数小时,而且对于笔记本之类开发者常用的设备来说,还是太大了。所以,Monorepo 基本上是不可能全部代码都下载到本地的。也就是说,平时的开发都要依赖于开发服务器,在服务器上写代码,在服务器上提交代码。

当然了,如果大家都是挂载到共享的文件系统工作,那么肯定会发生大量冲突,所以云端开发也并不是这个意思。一般是每个开发者有各自的云端虚拟机或者本地工作站,使用专用的客户端来访问代码。一般来说,这种客户端需要支持按需下载文件的功能,避免占用本地过多资源。当然,如果 Monorepo 的体积比较小,比如几十 GB,现代的服务器或者工作站都还是可以直接全部拉取下来的。

编译系统的支持

Monorepo 的编译一般是使用单独的构建系统由专门的编译服务器集群负责构建,一来可以进行分布式编译,加速构建过程;二来可以全团队共享编译缓存,进一步加速。虽然个人开发服务器或者工作站也可以编译,但是基本上编译的时候什么都做不了了,而且还很慢。

Google 有 Blaze(开源版叫 Bazel),Facebook 有 Buck,微信有 Blade(ex-Googler 写的类似 Bazel 的工具),他们都没有用现成的 CMake 或者 Make 等,而是自己又开发了一套编译系统。针对 Monorepo 使用的编译系统一般都是用类似 Python 的 DSL(比如 Bazel 用了 Starlark),支持细粒度的 target 管理,能更好地利用编译缓存。同时,为了保证多次构建的产物完全一致,编译系统一般会使用“气密式环境”构建,也就是所有输入要通过哈希值确定版本,并且编译工具链也通过哈希确定版本,构建过程运行在沙盒环境里,例如使用容器或者虚拟机,这样就可以保证构建产物的一致性了。

当然,如果什么都是自己写的话,Monorepo 的构建系统就很好用,然而一旦需要引入没有使用这套构建系统的第三方项目,就需要手动编写构建文件适配,如果项目构建比较复杂,那么适配过程将很痛苦。同时,内部的项目需要开源的话,也需要编写例如 CMake 等其他人常用的构建文件。

编译系统是以代码仓库为基准进行编译的,在代码还没有合入代码仓库之前怎么编译呢?一种解决办法就是将本地修改过的文件打包发送到编译服务器,编译系统在编译前将修改临时应用到代码主干,再进行编译。

智能提示

Monorepo 因为体积过大,一些 IDE 或者编辑器插件的智能提示功能很可能分析不来,直接歇菜。一般来说可以用云 IDE 的方法,或者在编译服务器构建好索引下载下来,或者生成 Compilation Database,只让插件分析使用到的依赖,避免全库扫描。

Monorepo 还有一个重要的优势就是代码透明,这样开发者可以直接在整个代码库中搜索,比如一个 API,可以搜索它的实现,也可以搜索其他人是如何使用的,直接复制粘贴,提高编码效率。显然,基于文本的搜索准确率远远不够,而基于符号分析的搜索显然也在个人开发服务器上跑不动,所以,使用 Monorepo,还需要一个好用的 Code Search。Google 和微信都有这么一个 Code Search,可以基于符号搜索,搜索结果中的每一个符号都可以点击并进行 Cross Reference。当然,Code Search 怎么做好是很有技术含量的,首先搜索的响应要及时,其次代码变化得很频繁,如何及时更新索引也是一个技术挑战。Google 选择了部分增量索引加定时全量索引以及暴力搜索相结合得方式,同时复用了老本行 Google Search 的技术栈,给开发者提供了优质的代码搜索体验。

CI/CD

对于 Monorepo,大家都在一个分支上提交代码,传统的一个 commit 触发一次全量的 CI/CD 肯定是不行的。所以,CI/CD 也需要支持分子路径来运行,或者手动触发运行。这种功能可以参考 GitLab CI 里的 path 参数,可以在检测到对应路径的文件发生改变之后才运行对应的 CI。

代码权限管理

因为是 Monorepo,所以代码全部公开,也意味着每个人都有权查看修改,查看代码一般不是什么大问题,毕竟 Monorepo 的目的就是共享代码,但是如果每个人都能修改代码那就乱套了。Google 采用的办法是使用 OWNERS 文件,对于子目录而言,只有 OWNERS 文件里的成员才可以批准修改。