代码管理之争:Monorepo 真的那么香吗?

2019年初,我忙于讨论将一个组织的所有代码存放到 monorepo 的优点(或缺点)。先给不熟悉这一概念的读者解释下,monorepo背后的想法是将所有代码存储在单个版本控制系统(VCS)仓库里。当然,另一种选择是将代码分割成许多不同的VCS仓库,通常按 服务/应用程序/库 这样划分。本文为了方便起见,我把多仓库解决方案称为“polyrepo”。

一些科技巨头正在使用 monorepo,包括谷歌、Facebook、Twitter等。当然,如果这些公司都使用 monorepo,好处一定是巨大的,我们也应该这样做,对吗? 错!正如标题所言:monorepo真的那么香吗?因为,对大规模项目,monorepo必须解决 polyrepo 要解决的所有问题,它的缺点是紧耦合,而解决VCS可伸缩性则需要付出巨大的努力。因此,中长期来看,monorepo不会给组织带来任何好处,同时不可避免地会让组织中最好的工程师患上严重的创伤后应激障碍(表现为流口水和对 git 内部性能的语无伦次的抱怨)。

一个简短的说明:我所说的“大规模”是什么意思?对于这个问题没有明确的答案,但是因为我知道有人会问我,所以为了便于讨论,我们假设“大规模”意味着有超过100名开发人员全职编写代码。

Monorepo 理论上的好处,以及为什么没有 polyrepo 式工具协助就无法实现(或者根本就是一个谎言)

理论上的好处 1: 更容易的协作和代码共享

Monorepo的支持者会认为,当所有代码都保存在一个仓库中时,代码重复的可能性很小,不同的团队更有可能在共享的基础设施上协作。

即使是中等大小的monorepo也存在一个丑陋的真相(这将是本节中反复出现的主题):开发人员将整个代码仓库放在他们的机器上,或者使用grep之类的工具进行搜索,这很快就变得不合理了。因此,任何希望扩大规模的monorepo都必须做到两点:

  1. 某些类型的虚拟文件系统(VFS),允许在本地显示部分代码。这可以通过类似Perforce这样的专有VCS来实现,它通过谷歌的” G3 “内部工具或者是微软的GVFS来内置这项功能。
  2. 将复杂的源代码索引/搜索/发现功能作为服务。由于没有哪个开发人员会让所有代码都处于可搜索状态,所以关键是要具备跨整个代码库执行搜索的能力。

假设开发人员一次只访问一小部分代码库,那么通过VFS检出树的一部分与检出多个仓库之间有什么真正的区别吗?没有区别

就源代码索引/搜索/发现功能而言,一个工具可以在许多代码仓库上进行简单的迭代并整理结果。事实上,这就是GitHub自身的搜索功能以及更新、更复杂的索引和协作工具(如Sourcegraph)工作的方式。

因此,在协作和代码共享方面,对于大规模项目,开发人员通过更高层工具接触代码的一部分。代码是monorepo 还是polyrepo 无关紧要,它们解决的问题是一样的。协作和代码共享的效率与工程文化有关,而与代码存储方式无关。

理论上的好处 2:单次构建/无需依赖管理

monorepo支持者通常会说的下一件事是,通过将所有代码放在一个仓库中,就不需要依赖管理,因为所有源代码都是同时构建的。这是谎言!对于大规模项目,根本没有办法重建整个代码库并在每次提交更改时运行所有自动化测试(特别是在CI中更频繁地提交更改,这种情况更普遍)。为了解决这个问题,所有的大型monorepos已经开发出复杂的构建系统(参考来自谷歌的Bazel 和Facebook 的巴克)。这种设计方式是在内部跟踪依赖关系和针对源码建立一个有向无环图(DAG)。此DAG允许高效的构建和测试缓存,以便只需要构建和测试更改的代码或依赖于此DAG的代码。

此外,由于构建的代码必须实际部署,同时并非所有软件都需要同时部署,所以必须仔细跟踪构建部分,以便根据需要将之前部署的软件重新部署到新主机上。这一现实意味着,即使在 monorepo 世界中,也同时存在多个版本的代码,也必须仔细跟踪和协调。

Monorepo的支持者会认为,即使需要进行大量的构建/依赖项跟踪,仍然有很大的好处,因为单个提交/SHA描述了整个源码的全部状态。我认为这种好处是存疑的;考虑到已经存在DAG,引入仓库的每个SHA作为DAG的一部分,只是个微不足道的优化。实际上,Bazel可以跨仓库或在单库中无缝工作,从开发人员那里抽象出底层布局。此外,可以很容易地创建自动化重构工具,从而在许多仓库之间自动地管理依赖库版本,从而弱化了monorepo和polyrepo在这方面的区别(更多信息见下)。

最终的结果是,无论使用monorepo还是polyrepo,大规模构建/部署管理的现实情况在很大程度上是相同的。工具不关心这个区别,开发人员写代码也不应该关心

理论上的好处 3:代码重构更容易 / 原子提交

monorepo支持者通常吹捧的最后一个好处是,当所有代码都在一个仓库中时,由于搜索容易和单个原子提交可跨越整个仓库,它让代码重构更加容易。这是一个谬论,原因有很多:

  1. 如上所述,在大规模项目中,开发人员将不能轻松地编辑或搜索本地机器上的整个仓库。因此,克隆所有代码并简单地执行grep/replace的想法在实践中并不轻松。
  2. 如果我们假设开发人员可以通过复杂的VFS克隆和编辑整个代码库,那么下一个问题是,这种情况实际发生的频率有多高?我并不是在讨论修复共享库中的bug,因为这种修复无论使用monorepo还是polyrepo都是相同的(假设使用与前一节中描述的类似的构建/部署工具)。我说的是共享库 API 变动,它对其他代码具有后续构建破坏效果。在非常大的代码库中,在合并冲突迫使流程重新开始之前,不太可能对基本API进行更改,并能通过每个受影响团队的 cdoe review。开发人员面临两个现实的选择。首先,他们可以放弃,并解决API问题(这种情况发生的频率比我们愿意承认的要高)。其次,他们可以弃用现有的API,实现一个新的API,然后在整个代码库中经历各个不同的弃用更改。无论哪种方式,这都是在polyrepo中进行的完全相同的过程。
  3. 在面向服务的世界中,应用程序现在由许多松耦合的服务组成,这些服务之间用某种定义良好的API交互。较大的组织不可避免地会迁移到诸如Thrift或Protobuf之类的IDL,这些IDL允许类型安全的API和向后兼容的更改。正如前面关于构建/部署管理的小节所述,代码不是同时部署的。它的部署时间可能是几个小时、几天或几个月。因此,现代开发人员必须考虑向后兼容。这是现代应用程序开发的一个简单现实,许多开发人员想要忽略它,但却不能。因此,当涉及到服务时,与库API相比,开发人员必须使用上面描述的两个选项之一(放弃更改API或经历一个弃用周期),这一点上,使用monorepo或polyrepo没有区别。

就跨大型代码库进行重构更改而言,许多组织最终都开发了自动化的重构工具,比如最近由Facebook发布的fastmod。与其他地方一样,这样的工具可以在单个仓库中或跨仓库进行简单的操作。Lyft内部有一个叫做“refactorator”的工具,就是做这个的。它的工作方式像fastmod,但能自动跨多仓库作出改动,包括发起PR,跟踪审查状态等。

monorepo 独特的缺点

在前一节中,我列出了monorepo提供的所有理论上的好处,并解释了为什么为了实现这些好处,必须开发出与polyrepo所要求的一样的超复杂工具。在本节中,我将介绍monorepos的两个独特缺点。

缺点1:紧耦合与OSS(开源软件)

在组织上,monorepo导致紧耦合和脆弱软件的开发。它给开发人员一种感觉,即他们可以很容易地修复抽象错误,而在现实世界中,由于错综复杂的构建/部署流程以及要求开发人员跨整个代码库进行更改时所固有的人员/组织/文化因素,他们实际上无法做到。

Polyrepo代码管理方式提供了清晰的团队/项目/抽象/所有权边界,并鼓励开发人员仔细考虑约定。这是一个微妙但非常重要的好处:它为组织的开发人员提供了一种更具可伸缩性和更长期的思维方式。此外,使用polyrepo并不意味着开发人员不能跨越代码仓库边界。能否跨越取决于工程师文化,而不是组织使用monorepo还是polyrepo。

紧密耦合对代码开源也有实质性的影响。如果组织希望创建或轻松使用OSS(开源软件),则需要使用polyrepo。大型的monorepo组织所进行的别扭工作(反向导入/导出、私有/公共问题跟踪、抹平抽象标准库差异等)不利于高效的OSS协作和社区构建,同时也给组织内的工程师带来了大量成本。

缺点 2: VCS 可伸缩性

将一个VCS扩展到数百个开发人员、数亿行代码和快速提交是一个巨大的任务。大约5年前,Twitter推出了monorepo(基于git),这是我职业生涯中所见过的最大的软件工程浪费之一。运行简单的命令(如git status)都要花费 好几分钟*。如果某个人的本地克隆落后太多,需要 *几个小时 才能赶上(有一段时间,甚至有一种做法是把装有最新代码克隆的硬盘寄给异地办公的员工)。我提出这个问题不是为了取笑Twitter 的软件工程,而是为了说明这个问题有多难。5年之后我被告知,Twitter的monorepo的性能仍然不是开发工具团队所希望的,也不是因为缺乏尝试。

当然,过去5年在这方面也有了发展。微软内部用于开发Windows的git VFS ,已经致力于为 git 创建一个真正的VFS,作为 monorepo 可伸缩性的需要。(随着微软对 Github 的收购,这种级别的 git 可伸缩性可能会出现在GitHub的企业级产品上)。当然,谷歌和Facebook继续在他们的内部系统上投入大量的资源来保持它们的运行,尽管这些工作都没有对外发布。

但是,如前一节所说的,既然开发工具跟 polyrepo 所需的没有任何区别,为什么要花力气解决VCS的可伸缩性问题呢?没有什么好理由。

总结

就像软件工程经常出现的情况一样,我们倾向于参考技术领域最成功的公司的最佳实践,而不去理解那些让这些公司在规模上获得成功的不凡工程。在我看来,Monorepos就是一个反面教材。谷歌、Facebook和Twitter已经在他们的代码存储系统上进行了大量的投入,但最终得到的解决方案却与使用polyrepo时所需要的解决方案并无二致,还导致了紧密耦合,并需要在VCS的可伸缩性上进行大量的投入。

坦率的现实是,对于大规模项目,一个组织在代码共享、协作、紧密耦合等方面做得如何是工程师文化和领导力的直接结果,而与使用monorepo 还是 polyrepo无关。对开发人员来说,这两个解决方案最终看起来是一样的。既然如此,当初为什么要用monorepo呢?可以休矣!

原文链接

Kayson Li wechat
欢迎扫码关注我的微信公众号,订阅更多文章
坚持原创技术分享,您的支持将鼓励我继续创作!