Monorepo 是什么?你可能需要了解一下

Monorepo (单体仓库) 是一种代码的管理方式,这种方式可以加速你的开发工作流,本文将讲述 Monorepo 的优缺点,它和 Multirepo (多体仓库) 的区别,如何使用工具提高开发效率,以及判断你的开发团队是否需要切换到 Monorepo。

什么是 Monorepo

Monorepo 中文:单体仓库 (单个仓库)。就是单个项目仓库 (repository),其中包含多个开发项目 project (模块 module,或包 package)。虽然这些 project 也许是相关联的,但它们通常在逻辑上相互独立,并被不同的团队负责编写,运行。

有些公司组织他们的代码在单个仓库里,并共享给每一个人。Monorepos 能容纳非常大的代码量,举个例如 Google 也许每天都会产出成百上千的 commits 大小超过 80TB。其他知名公司也在以 monorepo 方式管理自己的代码,如微软、Facebook、Twitter。

Monorepos 有时也被称作 Monolithic repositories,但它不能和 Monolithic architecture (单体架构) 混淆。单体架构是一种用于编写自包含应用程序 (Self-contained applications) 的软件开发实践。一个例子就是 Ruby on Rails monolith 能够处理许多个网站,API Endpoints 以及 Background Jobs.

Monorepos 和 Multirepos 的对比

与 Monorepo 相反的是 Multirepo (多体仓库:多个 project 保存在互相完全独立的代码仓库内)。Multirepo 来的很自然 — 也就大多数人都知道如何创建一个新的 project。毕竟,谁不喜欢一个干净的崭新的开始呢?

从 multi 迁移到 monorepo 就是将所有 project 移到单个代码仓库里。

当然,这只是开始。当我们进入重构和整合阶段时,艰难的工作就来了。

1
2
3
4
5
$ mkdir monorepo
$ git init
$ mv ~/src/app-android10 ~/src/app-android11 ~/src/app-ios .
$ git add -A
$ git commit -m "My first monorepo"

multirepo 不是 microservices (微服务) 的同义词,两者互不相干。实际上,稍后我们会讨论结合 monorepo 和 microservices 两种模式的公司,monorepo 可以管理任意数量的 microservice,只要仔细的设置用于部署的 Continuous Integration and Delivery (CI/CD) pipeline 就行。

Monorepos 的优点

乍眼一看,选择 Monorepos 和 Multirepos 似乎没什么大不了的,但这一步选择将深远的影响你公司的开发工作流。关于它们的好处,我们可以列出一些:

  • 透明度:所有人都能看到所有人写的代码,这一特点能够更好的协作和跨团队贡献 (collaboration and cross-team contributions),另一个团队中的开发人员可以修复代码中你甚至不知道存在的 bug。
  • 简化依赖管理:共享依赖是轻而易举的。几乎不需要用到包管理器,因为所有 module 都保存在在同一个代码仓库中。
  • Single 的真正原因:所有依赖仅有一个版本,意味着没有版本冲突和 Dependency hell (相依性地狱、依赖地狱)。
  • 一致性:当你将所有代码库集中在一个地方时,执行代码质量标准和统一风格会更容易。
  • 共享时间线:当 API 或共享的库中有重大变更 (breaking changes) 时就会立刻暴露出来,迫使不同的团队提前沟通并联合起来。每个人都致力于跟上变化。
  • Atomic CommitsAtomic Commits 使大规模重构更容易。开发人员可以在一次 commit 中更新多个包或项目。
  • Implicit CI: continuous integration is guaranteed as all the code is already unified in one place.
  • Unified CI/CD: you can use the same CI/CD deployment process for every project in the repo.
    Unified build process: we can use a shared build process for every application in the repo.

Monorepos 的缺点

随着 monorepos 越来越庞大,我们在版本控制工具、构建系统和 CI pipelines 方面会面临设计极限。这些问题会让公司决定重新用回 multirepo。

  • 糟糕的性能:monorepos 很难扩大规模。诸如 git blame 之类的命令可能会花费非常长的时间,IDE 开始卡顿,生产力受到影响,而且在每次提交后的测试阶段变得苦不堪言。
  • 污染主线分支 main/master:一个烂的 master branch 会影响所有在 monorepo 工作的人。这可被视为导致灾难,或者是保持测试 tests clean 和 up to date 的诱因。
  • 学习曲线:如果项目仓库中有跨越许多紧密耦合的 project,那么新开发人员的学习曲线将会更加陡峭。
  • 大量数据:monorepos 每天会处理大量的数据和 commit。
  • 文件权限管理:管理文件权限更具挑战性,因为像 Git 或 Mercurial 这样的系统没有内置的权限管理机制。
  • 代码审查:通知 (notifications) 会变得非常嘈杂。例如,GitHub 的通知功能,当一连串的 pull requests 和 code reviews 时并不友好。

你可能已经注意到,这些问题大多是技术性的 (technical)。在以下部分中,我们将了解坚持 monorepos 的公司如何通过投入精力到工具 (tooling)、添加集成 (integrations) 和 编写自定义解决方案 (writing custom solutions) 来解决大部分问题。

这不(仅)是技术性问题

决定选择何种布局不仅是一个技术性问题,还涉及到人们如何交流。正如 康威定律 (Conway’s Law) 所言,沟通对于打造优秀产品至关重要:

“设计系统的架构受制于产生这些设计的组织的沟通结构。”

“Any organization that designs a system (defined broadly) will produce a design whose structure is a copy of the organization’s communication structure.”

—— Melvin E. Conway

虽然 multirepo 允许每个团队独立管理他们的项目,但这也为协作带来了障碍。通过这种方式,他们可以当作自己是盲人,使开发人员只关注他们管理的部分,而忘记了整体情况。

另一方面,monorepo 作为一个中心枢纽 (central hub),一个集市广场 (market square),每个开发人员、工程师、测试人员和业务分析师都会在这里见面和交谈。Monorepos 鼓励我们交流并帮助我们破除孤岛 (bring silos down)。

Monorepo 的历史

Monorepos 已经存在很长时间了。三十年来,FreeBSD 一直使用 CVS 和后来的 subversion monorepos 进行开发和包的分发。

许多开源项目已经成功地使用了 monorepos。例如:

  • Laravel:一套 PHP Web 开发框架
  • Symfony:另一个用 PHP 编写的 MVC 框架。有趣的是,他们为每个 Symfony 工具和库创建了只读仓库。这种方法称为 split-repo (仓库分割?)。
  • NixOS:这个 Linux 发行版使用 monorepo 来发布 packages。
  • Babel:一种用于 Web 开发的流行 JavaScript 编译器。一个 monorepo 管理整个项目及其所有插件。
  • 此外,ReactPEmberMeteor 等前端框架都使用 monorepos。

然而,真正的问题是商业软件是否可以从 monorepo 布局中受益。考虑到优缺点,让我们听听一些公司的经验。

Segment: 再见 multirepos

Alex Noonan 讲述了一个关于 告别 multirepos 的故事。他工作的公司 Segment.com 提供事件收集和转发服务。他的每个客户都需要以特殊格式使用数据。因此,工程团队最初决定混合使用 microservices 和 multirepos。

该策略运作良好 —— 随着客户群的增长,他们毫无问题地扩大规模。但是,当转发目标的数量超过一百个大关时,事情开始变得崩溃。维护、测试和部署 140+ 个仓库(每个仓库都有数百个日益分散的依赖项)的管理负担太重了。

“最终,该团队发现自己无法取得进展,三名全职工程师大部分时间都在保持系统运作。”

“Eventually, the team found themselves unable to make headway, with three full-time engineers spending most of their time just keeping the system alive.”

对于 Segment 而言,补救措施是合并 (consolidation)。该团队将所有服务和依赖项迁移到单个 monorepo 中。虽然迁移成功,但它非常繁重,因为他们每次都必须协调共享库并测试所有内容。尽管如此,最终结果还是降低了复杂性并提高了可维护性。

“证据是速度的提高。 […] 在过去的 6 个月中,我们对库的改进比 2016 年全年都要多。”

“The proof was in the improved velocity. […] We’ve made more improvements to our libraries in the past 6 months than in all of 2016.”

许多年后,当一个小组询问她在 microservices 方面的经验时,Alex 解释了转向 monorepo 的原因:

“结果并没有像我们想象的那样具有优势。我们改变它的主要动机是测试失败会影响不同的东西 [..] 将它们拆分成单独的 repos 只会让情况变得更糟,因为你去接触六个月都没有碰过的东西。这些测试完全被破坏了,因为你没有被迫花时间修复它。我见过的东西成功拆分为单独的 repo 而不是 services 的唯一情况之一是,当我们有一大块代码在多个 services 之间 shared 时,我们希望将其变成一个共享库。除此之外,我认为我们已经发现,即使我们已经转向 microservices,我们仍然更喜欢 mono repo 中的东西。”

“It didn’t turn out to be as much of an advantage as we thought it was going to be. Our primary motivation for breaking it out was that failing tests were impacting different things [..] Breaking them out into separate repos only made that worse because now you go in and touch something that hasn’t been touched in six months. Those tests are completely broken because you aren’t forced to spend time fixing that. One of the only cases where I’ve seen stuff successfully broken out into separate repos and not services is when we have a chunk of code that is shared among multiple services, and we want to make it a shared library. Other than that, I think we’ve found even when we’ve moved to microservices, we still prefer stuff in a mono repo.”

Airbnb 和 Monorail

Airbnb 的基础设施工程师 Jens Vanderhaeghe 还讲述了microservices 和 monorepos 如何帮助他们在全球范围内扩展

Airbnb 的最初版本被称为 “the monorail”。它是一个庞大而单一(monolithic) 的 Ruby on Rails 应用程序。当公司开始呈指数级增长时,代码库也随之而来。当时,Airbnb 运行了一项新的发布政策,称为 democratic releases (民主发布),这意味着任何开发人员都可以随时发布到生产环境。

随着 Airbnb 的扩张,民主过程的限制受到了考验。合并变更变得越来越难。Jens 的团队实施了 palliative measures (缓解措施),例如 merge queue (合并队列) 和 (enhanced monitoring) 增强监控。这些在一段时间内有所帮助,但从长远来看还不够。

Airbnb 的工程师为 monorail 的运行进行了英勇的斗争,但最终,经过数周的辩论,他们决定将应用程序拆分为 microservices。因此,他们创建了两个 monorepos:一个用于前端,一个用于后端。两者都包含数百个 services (服务)、documentation (文档)、用于部署的 Terraform 和 Kubernetes resources,以及所有 maintenance tools (维护工具)。

当被问及 monorepo 布局的优点时,Jens 说:

“我们不想处理所有这些 microservices 之间的版本依赖性。 [使用 monorepo] 你可以通过一次提交跨越两个 microservices 进行更改 [..] 我们可以围绕单个仓库构建所有工具。最大的卖点是你可以一次对多个 microservices 进行更改。我们运行一个脚本,检测 monorepo 中的哪些程序受到影响,然后部署这些程序。我们的主要获利是代码控制。”

“We didn’t want to deal with version dependencies between all of these microservices. [With monorepo] you can make a change across two microservices with a single commit [..] We can build all of our tooling around a single repository. The biggest selling point is that you can make changes on multiple microservices at once. We run a script, and we detect which apps in the monorepo are impacted, and these get deployed. Our main benefit is source control.”

Uber:来来回回,循环往复

来自 Uber 的 Aimee Lucido 描述了从 monorepo 到 multirepo 再放弃的过程。

发生这种情况时,她正在 Android 客户端团队工作。他们从一开始就使用 monorepos。但经过五年的积极发展,monorepo 的问题开始出现。

“我们开始遇到令人窒息的 IDEs lockdowns。我们甚至无法在没有冻结代码的情况下在 Android Studio 中滚动鼠标。”

“We started to get the dreaded IDEs lockdowns. We got to the point where we couldn’t even scroll in Android Studio without the code freezing up.”

问题并没有在 IDE 结束。速度慢也影响了 Git,并不断变得越来越慢。更糟糕的是,他们经常污染主线分支,这使他们无法开发任何东西。

“公司越大,你就越会遇到一个烂 master。”

“The bigger the company gets, the more frequent you’ll experience a broken master.”

当 Uber 达到中等规模时,团队决定采用 multirepo。这解决了很多问题。Uber 工程师喜欢这样一个事实,他们可以获得部分代码的所有权并且只对其负责。

“如果你只 build 地图程序,那么你 build 的速度会更快。这是让人愉快至极的。”

“If you only build the maps app, then what you build is faster. It’s lovely.”

但故事并没有就此结束。又过了一段时间,multirepo 策略开始显示其弱点。这一次不仅仅是关于技术的问题,而是关于人们如何合作。团队正在分裂成孤岛,管理数千个仓库的消耗了大量宝贵的时间。

每个小组都有自己的 coding styles (代码风格)、frameworks (框架) 和 testing practices (测试实践)。管理依赖也变得更加困难,dependency hell monster (依赖地狱怪) 抬起了它丑陋的脑袋。这使得最终很难将所有内容整合到单个产品中。

正是在这一点上,Uber 工程师重组并决定再给 monorepo 一次机会。有了更多的资源并提前知道他们将面临的问题,他们选择在工具上下功夫:他们换了 IDE,实施了 merge queue (合并队列),并使用 differential builds (差异构建) 来加速部署。

“当你达到一个大公司规模时,你可以投入精力,让你的大公司感觉像一个小公司,把缺点变成优点。”

“When you get to a big company size, you can invest your resources to make your big company feel like a small company, to make the cons into pros.”

Pinterest: 全速转向 monorepo

最后,一家迁移工作进行三年之久的公司:Pinterest。他们的努力是两方面的。首先,将 1300 多个仓库移动到仅四个 monorepos 中。其次,将数百个依赖项整合到一个单一的 Web 程序中。

他们为什么这样做?Eden JnBaptiste 解释说 multirepos 使他们难以复用代码。这是同一个故事:代码过于分散,每个团队都有自己的代码库,具有不同的代码风格和结构。代码构建过程质量标准变化很大,因此构建和部署太难搞。

Pinterest 发现 trunk-based development (主干开发模型) 与 monorepos 相结合有助于提高生产力。主干开发模型的精髓是只使用生命周期较短的分支,并尽可能频繁地合并到主分支,减少合并冲突的情况。

“将所有代码放在一个仓库中帮助减少了 [在我们的构建系统中的] feedback loop (反馈循环)。”

“Having all the code in one repository helped us reduce the feedback loop [in our build systems].”

对于 Pinterest 来说,monorepo 布局提供了一致性的开发工作流。自动化,简单化,和 release practices (发布实践) 的标准化使他们能够减少 boilerplate (样板代码),让开发人员专注于编写业务代码本身。

优秀的工具

如果我们必须从所有这些故事中吸取一个教训:适当的工具是有效 monorepos 的关键 —— 需要重新思考 building 和 testing。我们可以使用聪明的 building tool,只对自上次提交以来发生变化的部分采取行动,而不是在每次更新时重建一整个的 repo,这个构建系统能理解项目的结构,只在自上次提交以来发生变化的部分起作用。

我们大多数人都没有 Google 或 Facebook 的内部资源。我们可以做什么?幸运的是,许多大公司已经开源了他们的 build systems (构建系统):

  • Bazel:Google 发布的,部分基于他们自己开发的构建系统 (Blaze)。Bazel 支持多种语言,并且能够大规模构建和测试。
  • Buck:Facebook 的开源快速构建系统。支持多种语言和平台上的差异化构建。
  • Pants:Pants 构建系统是与 Twitter、Foursquare 和 Square 合作开发的。目前,它仅支持 Python,未来将支持更多语言。
  • RushJS:微软用于 JavaScript 的可扩展 monorepo 管理器,能够从单个仓库构建和部署多个 packages。

Monorepos 越来越受到关注,尤其是在 JavaScript 中,如以下项目所示:

  • Lerna:JavaScript 的 monorepo 管理器。 与 React、Angular 或 Babel 等流行框架集成。
  • Yarn Workspaces:在一个仓库的根目录下安装和更新多个 Node.js 依赖。
    ultra-runner:用于 JavaScript 的 monorepo 管理脚本。同时支持 Yarn、pnpm 和 Lerna。支持 parallel builds (并行构建)。
  • Monorepo builder:跨 PHP monorepos 安装和更新 packages。

项目仓库扩展

代码控制是 monorepos 的另一个痛点。这些工具可帮助扩展你的项目仓库:

  • Virtual Filesystem for Git (VFS):添加了对 Git 的 streaming 支持。 VFS 根据需要从 Git 仓库下载对象。 这个项目最初是为了管理 Windows 代码库(最大的 Git 仓库)而创建的。 仅适用于 Windows,但已宣布支持 MacOS。
  • Large File Storage:Git 的开源扩展,增加了对大文件更好的支持。安装后,你可以 track 各种类型的文件并将它们无缝上传到云存储,释放你的仓库并使 pushing 和 pulling 的速度更快。
  • Mercurial:作为 Git 的替代品,Mercurial 是一个专注于速度的分布式版本控制工具。Facebook 使用 Mercurial,多年来贡献了许多提高速度的 patches
  • Git CODEOWNERS:允许你指定哪个团队拥有项目仓库中的子目录。当有人 open 了一个 pull request 或 push 到受保护的分支时,会自动要求代码所有者进行审查。GitHub 和 GitLab 都支持此功能。

Monorepo 的最佳管理实践

基于 monorepo 故事的集合,我们可以定义一组最佳实践:

  • 定义统一的目录结构以便于查找。
  • 维持分支的干净。让分支尽量的小,考虑采用基于主干的开发 (trunk-based development)。
  • 为每个项目使用固定依赖。一次性升级所有依赖,强制每个项目跟上依赖。保留例外以用于真正特殊的事情。
  • 如果你使用 Git,请了解如何使用 shallow clone (浅克隆) 和 filter-branch (分支过滤) 来处理大容量代码仓库。
  • 货比三家,寻找像 Bazel 或 Buck 这样智能的构建系统,以加快构建和测试速度。
  • 当你需要限制对某些项目的访问时,请使用 CODEOWNERS。
  • 使用云 CI/CD 平台来大规模测试和部署你的应用程序。
  • Define a unified directory organization for easy discovery.

  • Maintain branch hygiene. Keep branches small, consider adopting trunk-based development.

  • Use pinned dependencies for every project. Upgrade dependencies all at once, force every project to keep up with the dependencies. Reserve exceptions for truly exceptional cases.

  • If you’re using Git, learn how to use shallow clone and filter-branch to handle large-volume repositories.

  • Shop around for a smart build system like Bazel or Buck to speed up building and testing.

  • Use CODEOWERS when you need to restrict access to certain projects.

  • Use a cloud CI/CD platform such as Semaphore to test and deploy your applications at scale.

需要使用 monorepos?

这取决于… 并没有适合每个用例的直接答案。一些公司可能会选择 monorepo 一段时间,然后决定他们需要切换到 multirepos,反之亦然,而其他公司可能会选择混合。当犹疑不决时,考虑从 monorepo 迁移到 multirepo 通常比反过来更容易。但永远不要忘记,归根结底,这与技术无关,而与 work culture (工作文化) 和沟通有关。因此,请根据你想要的工作方式做出决定。

原文作者

本文翻译自 What is monorepo? (and should you use it?)

原文作者 @Tomas Fernandez

扩展阅读

本站文章除注明外均采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 协议进行许可 ヾ(゚ー゚ヾ)
分享到