多仓库 VS 单仓库

近期公司内部需要开发前端可复用的 UI 业务组建库,需要一个库来涵盖公司里不同的项目。
同时满足各个项目中的组建能够互相引用, 并且各自独立, 兼容不同项目的 antd 版本等要求.
从而衍生出对多仓库单仓库的思考;

效果图

带着问题思考

  • 什么是单体仓库(mono-repo)?
  • 为什么诸如 Google/Facebook/Bilibili 的大厂会采用单体仓库?
  • 单体仓库(mono-repo)和多仓库(multi-repo)分别解决了哪些问题?
  • 单体仓库(mono-repo)和多仓库(multi-repo)在解决问题的同时又引入了哪些问题?

单体应用

在早期,实际上是不用区分单体仓库和多仓库的,因为在早期的时候包括目前大部分的情况都是一个应用就把所有功能打包了。
哪怕使用多个仓库也是根据公共模块方式来区分,仓库数量也是比较少的。
这时候的应用我们一般叫他单体应用, 而这样的单体应用就被存放在一个 git 仓库(虽然是一个但和我们要聊的单体仓库的概念是不同的)中进行管理。
主要的特点就是一个程序就打包所有功能,全家桶。
这样的开发模式有很多优点:

  1. 易于理解项目整体。
    开发人员可以把整个项目加载到本地的 IDE 当中,进行 code review,方便开发人员把握整体的技术架构和业务目标。
  2. 易于集成和部署。
    所有的代码在一个仓库里面,不需要特别的集中管理和协调,也可以直接在本地部署调试。
  3. 易于复用。
    所有的代码都在一个仓库中,开发人员开发的时候比较容易发现和重用已有的代码,而不是去重复造轮子。
  4. 易于重构
    可以抽取出一些公共的功能进一步提升代码的质量和复用度。
  5. 易于规范代码
    所有的代码在一个仓库当中就可以标准化依赖管理,集中开展 code review,规范化代码的风格。

但是随着单一应用功能的拓展,应用仓库无限制变大也带来了很多缺点:

  1. 开发效率大幅降低
    所有的开发人员在一个项目改代码,递交代码相互等待,代码冲突不断。
  2. 代码维护性变差
    随着功能以及代码量的大幅增加,所有代码功能耦合在一起,新人不知道何从下手。
  3. 构建时间过长
    任何小修改必须重新构建整个项目,这个过程往往很长。
  4. 稳定性太差
    任意一个功能出现问题,可以导致整个应用挂掉。
  5. 扩展性达不到要求
    所有功能都在一起,无法简单的横向扩展,容易受限于物理机器的配置。

微服务应用

既然单一应用的问题是由于后期功能过多,应用复杂度上升产生的。那是否可以通过将单体应用拆解成无数的小应用来避免这些问题呢?
实际上后来微服务的概念就类似于拆分的思路,通过将一个大型应用拆分成无数个服务,
每个服务有自己的代码文件,单独部署,然后共同组成一个应用程序。
微服务相比单体应用最大的好处是可以独立的开发测试部署和扩展
所以单体应用一般采用单个仓库,而微服务项目则是适用于单体仓库和多仓库.

多仓库

多仓库为我们带来了如下好处:

  1. 每一个服务都有一个独立的仓库,职责单一
  2. 代码量和复杂性受控,服务由不同的团队独立维护、边界清晰
  3. 单个服务也易于自治开发测试部署和扩展,不需要集中管理集中协调
  4. 利于进行权限控制
    可以针对单个仓库来分配权限,权限分配粒度比较细。

但同时,多仓库也存在着以下的问题:

  1. 项目代码不容易规范.
    每个团队容易各自为政,随意引入依赖,code review 无法集中开展,代码风格各不相同。
  2. 项目集成和部署会麻烦.
    虽然每个项目服务易于集成和部署,但是整个应用集成和部署的时候由于仓库分散就需要集中的管理和协调。
  3. 开发人员缺乏对整个项目的整体认知.
    开发人员一般只关心自己的服务代码,看不到项目整体,造成缺乏对项目整体架构和业务目标整体性的理解。
  4. 项目间冗余代码多不利于代码复用.
    每个服务一个服务一个仓库,势必造成团队在开发的时候走捷径,不断地重复造轮子而不是去优先重用其他团队开发的代码。

单体仓库

单体仓库的优势其实和单体应用的优势如出一辙, 因为单体应用就是单个仓库
而单体应用已经是包含于单体仓库这个概念中了
效果图

单体仓库的优势特殊的优势在于:

  1. 处理单体项目和单体项目之间的依赖关系
    假设子項目 A 依赖子項目 B,如果子項目 B 经常改動,非单体仓库的形式下那么每次 B 改动了,都要修改 A,显得异常麻烦
    而单体仓库加上合适的工具链正是可以处理此类问题

  2. 提取公共依赖
    所有子项目的公共依赖都可以提取到根目录下使得版本控制更容易,依赖管理更方便。

单体仓库的劣势:

  1. 单体仓库基本放弃了对读权限的限制
    开发人员可以接触到项目所有代码,Bilibili 的源代码泄露也印证了这个问题。
    对于写权限,单体仓库也是有着自己的解决方案,比如 OWNERS,CODEOWNERS 等,但相比多仓库还是差了一些。

  2. 自治程度低
    单个服务的开发测试部署和扩展,需要集中管理集中协调,降低了微服务单个服务的自治程度。

  3. 代码量和复杂性不受控
    随着公司业务团队规模的变大,单一的代码库会变得越来越庞大复杂性也呈极度的上升,容易受团队能力及开发流程等影响导致结果不可控。

  4. 需要团队进行整合
    使用单体仓库,一般需要独立的代码管理和集成团队加上配套的自动化构建工具来支持。某些方面已经出现了开源的方案,
    比如 Google 自研的面向单体仓库的构建工具 Bazel 和 Facebook 的 Buck
    但还是少不了团队进行整合。

  5. 安装依赖
    各个包之间都存在各自的依赖,有些依赖可能是多个包都需要的,我们肯定是希望相同的依赖能提升到 root 目录下安装,其它的依赖装哪都行。
    此时我们可以通过 yarn 来解决问题(npm 7 之前不行),需要在 package.json 中加上 workspaces 字段表明多包目录,通常为 packages。
    之后当我们安装依赖的时候,yarn 会尽量把依赖拍平装在根目录下,存在版本不同情况的时候会把使用最多的版本安装在根目录下,其它的就装在各自目录里。

    效果图

    这种看似正确的做法,可能又会带来更恶心的问题。
    比如说多个 package 都依赖了 React,但是它们版本并不都相同。
    此时 node_modules 里可能就会存在这种情况:
    根目录下存在这个 React 的一个版本,包的目录中又存在另一个依赖的版本。

    效果图

    因为 node 寻找包的时候都是从最近目录开始寻找的,此时在开发的过程中可能就会出现多个 React 实例的问题,
    由于 React 实例多个所以就报错了。
    遇到这种情况的时候,我们就得用 resolutions 去解决问题,当然也可以通过阻止 yarn 提升共同依赖来解决(更麻烦了)。
    相信使用 mono-repo 模式这样的问题一定是无法避免,q 提取的根依赖和依赖造成的多版本问题。
    这种依赖的依赖术语称之为 「幽灵依赖」

    世界上采用单体仓库管理源码的公司实际并不少,比如: Google,Facebook,Twitter 这些互联网巨头。可以看到,虽然这些公司系统庞大、服务众多,内部研发团队人数众多,但是采用单体仓库也很好的解决了业务需求。

  6. 构建

    效果图
    • 构建太慢
      因为所有包都存在一个仓库中了,如果每次执行 CI 的时候把所有包都构建一遍,那么一旦代码量变多,每次构建可能都要花上不少的时间。
      这时候肯定有读者会想到增量构建,每次只构建修改了代码的 package,这个确实能够解决问题,核心代码也很简单:

      1
      git diff --name-only {git tag / commit sha} --{package path}

      上述命令的功能是寻找从上次的 git tag 或者初次的 commit 信息中查找某个包是否存在文件变更,然后我们拿到这些信息只针对变更的包做构建就行。但是注意这个命令的前提是在部署的时候打上 tag,否则就找不到上次部署的节点了。
      但是单纯这样的做法是不够的,因为在 mono repo 中我们还会遇到多个 package 之间有依赖的场景:

      效果图

      在这种情况下假如此时在 CI 中发现只有 A 包需要构建并且只去构建了 A 包,那么就会出现问题:在 TS 环境下肯定会报错找不到 D 包的类型。
      在这种存在包于包之间有依赖的场景时,我们需要去构建一个有向无环图(DAG)来进行拓扑排序。
      总之在这种场景下,我们需要寻找出各个包之间的依赖关系,然后根据这个关系去构建。比如说 A 包依赖了 D 包,当我们在构建 A 包之前得先去构建 D 包才成。
      以上是没有工具链时可能会出现的问题。如果我们用上 lerna 的话,内置的一些命令就可以基本帮助我们解决问题了:
      lerna changed 寻找代码有变动的包,接下来我们就可以自己去进行增量构建了。
      通过 lerna 执行命令,本身就会去进行拓扑排序,所以包之间存在依赖时的构建问题也就被解决了。

参考文献:

  1. https://blog.csdn.net/kunyus/article/details/105059469
  2. https://zhuanlan.zhihu.com/p/364109385
  3. https://fed.taobao.org/blog/taofed/do71ct/uihagy