谷歌为什么使用monorepo?

| 2023-05-02

本文要点

Google已经展示出:一个拥有10亿个文件,3500万次提交和数以万计开发人员的单仓代码库源代码管理和扩展能力。

这种模型的优点在于:

统一的版本控制广泛的代码共享简化的依赖管理原子化的更改大规模的重构跨团队的协作灵活的代码所有权代码可见性

缺点在于:必须为创建以及维护代码健康而创建一些相应的工具平台,以及潜在的代码库复杂性(例如不必要的依赖关系)。

概述

早期的谷歌员工决定使用一个由集中式源代码管理系统管理的共享代码库。这种方法已经服务于谷歌超过16年(本文写于2016年7月,所以大仓模式自2000年起)。

如今,绝大多数谷歌软件资产仍然存储在一个单一的共享存储库中。与此同时,谷歌软件开发人员的数量稳步增加,谷歌代码库的规模呈指数级增长(见图1)。因此,用于托管代码库的技术也在不断发展。

图1. Millions of changes committed to Google’s central repository over time.

本文概述了谷歌代码库的规模,并详细介绍了谷歌定制的单体式源代码库及选择这种模式的原因。谷歌使用自主研发的版本控制系统来托管一个大型代码库,由公司中的大多数软件开发人员使用和可见。这个中央化的系统是谷歌许多开发工作流的基础。在这里,我们提供了关于系统和工作流的背景信息,使得管理和有效地使用这样一个庞大的代码库成为可能。我们解释了谷歌的“基于主干的开发”策略以及支持工作流的系统,包括静态分析软件、代码清理和简化的代码审查。

Google的规模

Google的单体代码仓库被其 95% 的软件工程师雇员使用,符合超大规模系统的定义,这可以作为单一大仓模式可以成功规模化的证据。

Google 的代码库包括大约10亿个文件,历史记录跨越了Google 的整个18年历程(自98年创建公司开始),约有3500万次提交。该仓库包含 86TB 的数据(注:内容压缩前的总大小,但不包含发布分支上的内容),含有约 900 万个唯一源文件中的约 20 亿行代码。文件总数还包括复制到发布分支的源文件、最新修订版本中已删除的文件、配置文件、文档和支持数据文件。请参阅下面的表格,以获取有关 Google 代码库统计数据的摘要。

Table. Google repository statistics, January 2015

2014年,Google代码库每周约有25万个文件(约1,500万行代码)被变更(注:只包含已审核和提交的代码,不包含由自动化系统执行的提交,以及对发布分支、数据文件、生成的文件、导入到存储库的开源文件和其他非源代码文件的提交)。

大家可以与 Linux 内核的代码量做一个对比。Linux 内核是一个著名的大型开源代码库的例子,它总共包含约1,500万行代码和4万个文件,参见Wikipedia. Linux kernel. Accessed Jan. 20, 2015; 。所以,仅从代码量上来说,Google 相当于每天写一个 Linux 内核。

Google 的代码库由全球数十个办公室的超过 2.5 万名软件开发人员共享。在工作日,他们提交 1.6 万次代码更改,而自动化系统提交另外 2.4 万次更改。每天仓库提供数十亿个文件读取请求,在高峰期交通流量约为每秒大约80万个请求,每个工作日平均约为每秒50万个请求。其中大部分流量请求来自 Google 的分布式构建和测试系统 Blaze,它有一个开源版本,叫作 bazel

图2 报告了自 2010 年 1 月,至 2015 年 7 月主要代码库每周的独立提交者数量。图 3 报告了在同一时间段内提交到 Google大仓库的提交次数。总提交次数的曲线包括交互式使用情况(或叫人类用户)和自动使用情况的数据。

图2. Human committers per week.

图3. Commits per week.

在两个图表中,较大的下降处是那些影响大量员工的假期期间(例如圣诞节和新年、美国感恩节和美国独立日)。

2012 年 10月,Google 的大仓增加了对 Windows 和 Mac 用户的支持(此前只支持 Linux ),现有的 Windows 和 Mac 代码库已被合并到主代码库中。Google 的代码库合并工具将所有历史更改归属于它们的原始作者,因此在 图2 中出现了相应的上升趋势。这种合并的影响也在 图1 中显现出来。

每周提交次数的图表显示,提交速率一直由人类用户主导。直到2012年,Google 切换到自定义源代码控制实现来托管中央代码库(后面会进一步讨论)。在此转换后,向代码库的自动提交开始增加。提交速率的增长主要是由自动化引起的。

管理这种规模的代码库和在其上的工程活动一直是 Google 面临的挑战。尽管经过数年的实验,Google 仍然无法找到商业或开源版本控制系统来支持这种规模的单个代码库。Google 为存储、版本控制和供应此代码库而构建的专有系统名为 Piper。

背景

在讨论使用单一代码库的优缺点之前,需要了解一些关于谷歌工具和工作流程的背景信息。

Piper 大仓

Piper 存储单个大型代码库,是基于标准谷歌基础架构(最初是 Bigtable ,现在是 Spanner )实现的。 Piper 分布在全球 10 个谷歌数据中心,依靠 Paxos 算法确保副本之间的一致性。这种架构提供了高度的冗余性,并帮助优化谷歌软件开发人员的使用延迟。此外,缓存和异步操作可以将大部分网络延迟隐藏在开发人员之后。这很重要,因为要充分利用谷歌的基于云的工具链,需要开发人员在线。

在启动 Piper 之前,谷歌依赖于一个主要的 Perforce 实例,托管在单个机器上,并配合自定义缓存基础架构使用了超过10年的时间。谷歌代码库的持续增长是开发 Piper 的主要动机。

由于谷歌的源代码是公司最重要的资产之一,因此安全功能也是 Piper 设计的关键考虑因素。Piper 支持文件级别的访问控制列表。仓库中的大多数代码对所有 Piper 用户都可见;然而,重要的配置文件或包含业务关键算法的文件可以进行更严格的控制。此外,对 Piper 中的文件的读取和写入访问会被记录。如果敏感数据意外地提交到 Piper 中,则可以清除问题文件。通过读取日志,允许管理员确定在删除文件之前是否有人访问了有问题的文件。

图4. Piper workflow.

在 Piper 工作流程中(参见图 4 ),开发人员在更改文件之前创建存储在工作区中的文件的本地副本。Piper 工作区类似于 Apache Subversion 中的工作副本,Git 中的本地克隆库或 Perforce 中的客户端。可以将 Piper 库中的更新拉入工作区,并根据需要与正在进行的工作合并(参见图 5 )。工作区的快照可以与其他开发人员共享以供审查。在通过谷歌代码审查过程后,工作区中的文件才会提交到中央代码库。

图 5. Piper team logo “Piper is Piper expanded recursively;”

云端客户端 CitC

大多数开发人员通过称为“云端客户端”( Clients in the Cloud,简称CitC )的系统访问 Piper 。CitC由基于云的存储后端和仅适用于 Linux 的 FUSE13 文件系统组成。开发人员将他们的工作区视为文件系统中的目录,包括他们的变更叠加在完整的Piper代码库上。CitC 支持代码浏览和正常的 Unix 工具,无需在本地克隆或同步状态。开发人员可以在 Piper 代码库中的任何地方浏览和编辑文件,只有修改的文件会存储在他们的工作区。这种结构意味着 CitC 工作区通常只占用很少的存储空间(平均工作区只有不到 10 个文件),同时向开发人员呈现整个 Piper 代码库的无缝视图。

所有对文件的写入都作为快照存储在 CitC 中,可以根据需要恢复以前的工作阶段。可以对快照进行显式地命名、恢复或标记操作,以供代码评审( Code Review,简称 CR)。

CitC 工作区可在任何能连接到基于云的存储系统的计算机上使用,使得无缝切换机器并继续工作成为可能。它还使得开发人员能够在 CitC 工作区中查看彼此的工作。将所有进行中的工作存储在云中是 Google 工作流程的重要组成部分。因此,工作状态可供其他工具使用,包括基于云的构建系统、自动化测试基础设施和代码浏览、编辑和审核工具。

很多工作流程都利用 CitC 中这种未提交代码的可用性,让软件开发人员更加高效。例如,当将更改发送到代码审核时,开发人员可以启用自动提交选项,这在代码作者和审核人员处于不同时区时特别有用。当 CR 被标记为「完成」时,将运行测试;如果测试通过,则将代码提交到代码库,无需进一步人为干预。Google 代码浏览工具 CodeSearch 支持使用 CitC 工作区进行简单的编辑。在浏览代码库时,开发人员可以单击按钮进入编辑模式并进行简单的更改(例如修复拼写错误或改进注释)。然后,他们可以启用自动提交,并在不离开代码浏览器的情况下将变更发送到适当的审核人员。

Piper 也可以在没有 CitC 的情况下使用。开发人员可以将 Piper 工作区存储在他们的本地机器上。Piper 也具有有限的与 Git 的互操作性。今天超过 80% 的 Piper 用户使用CitC,采用率继续增长,因为 CitC 提供了许多优点。

Piper 和 CitC 使在 Google 代码库规模下能够高效地使用单一的、统一的源代码库进行工作成为可能。这些系统的设计和架构都受到了 Google 采用的基于主干的开发模式的重大影响,如下所述。

主干开发模式

Google 使用 Piper 源代码库,采用基于主干的开发方法。绝大多数 Piper 用户都在最新版本的代码库中工作,即所谓的“主干”或“主线”。所有变更都以单个且串行的顺序应用于代码库。基于主干的开发与中央代码库的组合定义了单一代码库模式。在任何提交后,新代码立即对所有其他开发人员可见和可用。Piper 用户在 Google 代码库的一个一致的视图上工作,这是提供本文后面所述优势的关键。

图 6. Release branching model.

主干开发方法的优点在于避免了长期分支合并时经常发生的痛苦合并。在 Google,分支开发不常见,而且不受良好地支持,而分支通常仅用于发布。发布分支是从特定版本的代码库中分支出来的。那些必须添加到某个发布版的漏洞修复和功能增强通常要先在主线上开发,然后再拣选到发布分支中(参见图6)。由于需要维护稳定性并限制发布分支上的变更,因此发布通常是主干的快照,根据需要从主干中拉出一小部分挑选的提交。在分支和主干上进行并行开发的长期分支使用极为罕见。

Piper 和 CitC 让谷歌高效地使用单一大仓模式成为可能。

在开发新功能时,通常同时存在新旧两种功能代码路径,并通过条件开关进行控制。这种技术避免了需要开发分支的情况,通过配置更新的方式发布,很容易打开和关闭功能,而不是发布一个完整的二进制。虽然这会给开发人员带来一些额外的复杂性,但是可以避免开发分支的合并问题。开关状态的修改可以更容易且更快速地将用户从有问题的新功能实现中切换出来。这种方法通常用于项目特定的代码,而不是通用的库代码。并且,最终标志被废除后,旧代码也可以被删除。Google 在通过不同的代码路径路由实时流量进行实验时也使用了类似的方法,通过配置更改可以实时调整这些 A/B 实验,这些实验可以测量代码的性能特征以及与微小产品变化相关的用户参与度等方面。

谷歌的工作流程

在基于主干的开发模式中,数千名工程师每天提交数千个变更,需要遵循一些最佳实践和支持系统,以避免主干代码不断出现质量问题。例如,谷歌有一个自动化测试基础设施,几乎每次提交更改时都会启动对所有受影响依赖的重新构建。如果该变更导致大范围的构建错误,就会自动撤销该变更。为了减少提交错误代码的发生率,高度可定制的 Google “预提交”基础设施提供了在将更改添加到代码库之前进行自动化测试和分析的功能。所有变更类似于开发分支的机制,在将新版本暴露给客户端代码之前强制进行额外的测试。

鼓励代码质量是谷歌文化的一个重要方面,所以,在将代码提交到代码库之前会进行代码评审( CR )。除了一小部分高度机密的代码受到更严格的控制之外,大多数开发人员可以查看和提出对跨整个代码库任何文件的更改。通过代码审查过程和代码所有权的概念,减轻了开发人员更改他们不熟悉的代码的风险。谷歌代码库采用树形结构布局,每个目录都有一组所有者( Owner ),他们可以决定是否接受对应目录中文件的变更。Owner 通常是在相关目录中工作的项目的开发人员。变更通常会接受一位开发人员对代码质量的详细审查,包括评估变更的质量,以及接受来自所有者的提交批准,评估变更对他们的代码库区域的适当性。

代码审阅者对代码质量的各个方面发表评论,包括设计、功能、复杂性、测试、命名、注释质量和代码风格,这些都记录在各种特定于语言的 Google 样式指南中。谷歌编写了一款名为 Critique 的代码审查工具,允许审阅者查看代码的演变,并在任何一行代码变更上发表评论。它鼓励进一步修订和交流,最终得到审阅者的最终批准(通常是 LGTM,即 Looks Good To Me" ),表示审阅已完成。

Google的静态分析系统(Tricorder)和 预提交基础设施还可以在 Google 代码审查工具中自动提供有关代码质量、测试覆盖率和测试结果的数据。这些计算密集型检查定期触发,以及当代码更改发送进行审查时触发。Tricorder 还提供一键式代码编辑的建议修复方法来解决许多错误。这些系统提供重要数据,以增加代码审查的效果,并保持 Google代码库的健康。

Google的开发团队偶尔会进行一组大范围的代码清理变更,以进一步维护代码库的健康状况。执行这些更改的开发人员通常将它们分为两个阶段。采用这种方法,首先进行大规模的向后兼容性更改。一旦完成,可以进行第二个较小的更改,以删除不再引用的原始模式。Google 工具 Rosief 支持此类大规模清理和代码更改的第一阶段。使用 Rosie,开发人员通过在整个存储库上执行查找和替换操作或通过更复杂的重构工具创建一个大补丁。然后,Rosie 负责将大型补丁拆分成较小的补丁,独立测试它们,将它们发送到代码审查,并在它们通过测试和代码审查后自动提交它们。Rosie 沿着项目目录线拆分补丁,依赖于前面描述的代码所有权层次结构,将补丁发送到适当的审阅人员那里。

图7. Rosie commits per month.

图 7 展示了每月通过 Rosie 提交的更改数量,展示了 Rosie 作为 Google 进行大规模代码更改的工具的重要性。在使用 Rosie 的同时,需要平衡对团队的成本,因为这些团队需要审核 Rosie 生成的简单更改的持续流。随着 Rosie 的流行和使用增长,明确必须建立某种控制,以限制 Rosie 的使用,使其仅用于高价值的更改,这些更改将分配给许多审核人员,而不是单个原子更改或被拒绝。2013 年,Google 采用了一种正式的大规模更改审查流程,从而导致了 2013 年至 2014 年之间通过 Rosie 提交的提交数量的减少。在评估 Rosie 更改时,审查委员会平衡更改的收益与审核人员时间和代码库翻转的成本。我们稍后将更仔细地讨论这些以及类似的权衡。

总之,Google 已经开发了许多实践和工具来支持其巨大的单体代码库,包括基于主干的开发、分布式源代码库 Piper、工作空间客户端 CitC 和工作流支持工具 Critique、CodeSearch、Tricorder 和 Rosie。我们在此讨论这种模型的优缺点。

分析

本节概述并讨论了单一代码库的优点,以及与大规模维护这种模型相关的成本。

优点

Google面临的挑战是如何支持其巨大的单体式代码库,同时为数以万计的用户维持良好的性能,但由于其具有很高的吸引力和优势,Google已经采用了单体式模型。

最重要的是,它支持:

  • 统一版本控制,一个真理源;
  • 广泛的代码共享和重用;
  • 简化的依赖管理;
  • 原子性的更改;
  • 大规模重构;
  • 跨团队协作;
  • 灵活的团队边界和代码所有权;以及
  • 代码可见性和清晰的树形结构,提供了隐式的团队命名空间。

单个存储库提供了统一的版本控制和一个真正的源头。没有哪个存储库包含文件的权威版本会造成困惑。如果一个团队想要依赖于另一个团队的代码,它可以直接依赖。Google 的代码库包括大量有用的库,而单体式存储库促进了广泛的代码共享和重用。

Google 的构建系统使得跨目录包含代码变得容易,简化了依赖管理。对一个项目依赖的更改会触发依赖代码的重构。由于所有代码都在同一个存储库中进行版本控制,所以只有一个版本的真相,不用担心依赖的独立版本控制。

最值得注意的是,这种模型使得Google避免了“钻石依赖”问题(见图8)。当A依赖于 B 和 C 时,B 和 C 都依赖于 D,但 B 需要版本 D.1,而C需要版本 D.2,大多数情况下无法构建 A 。对于基础库 D,发布新版本很难不会引起破坏,因为所有的调用者必须同时更新。当库调用者在不同的存储库中托管时,更新就变得困难了。

Figure 8. Diamond dependency problem.

在开源世界中,依赖关系通常会因库更新而中断,找到所有能够一起工作的库版本可能会很有挑战性。更新依赖关系的版本对于开发人员来说可能会很痛苦,并且更新的延迟会产生技术债务,这可能会变得非常昂贵。相比之下,在单片源树中,对于更新库的人来说,同时更新所有受影响的依赖关系是合理的,也更容易。依赖系统产生的技术债务会随着更改的进行立即还清。对于基础库的更改会立即传播到依赖链中的最终产品中,而无需进行单独的同步或迁移步骤。

需要注意的是,钻石依赖问题存在于二进制之间,也可以存在于源代码/ API 之间。在 Google 中,二进制问题通过使用静态链接来避免。

单一代码库的一个非常强大的功能是能够进行原子更改。开发人员可以在单个一致的操作中对存储库中数百或数千个文件进行重大更改。例如,开发人员可以在单个提交中重命名类或函数,而不会破坏任何构建或测试。

在单一代码库或至少在一个集中式服务器上可用所有源代码,使核心库的维护人员更容易在提交更改之前进行测试和性能基准测试。这种方法对于探索和衡量高度破坏性更改的价值非常有用。一个具体的例子是一个实验,评估将 Google 数据中心转换为支持非 x86 机器架构的可行性。

在 Google 存储库的单一结构中,开发人员永远不必决定存储库边界在哪里。工程师永远不需要 “fork” 共享库的开发,也不需要在不同的存储库之间合并以更新复制的代码版本。团队边界是流动的。当项目所有权更改或计划合并系统时,所有代码已经在同一个存储库中。这种环境使得对代码库进行逐步重构和重新组织变得容易。将项目移动并更新所有依赖项的更改可以原子地应用于存储库,并且受影响代码的开发历史保持完整和可用。

单一代码库的另一个属性是代码库的布局易于理解,因为它是在一个单一的树形结构中组织的。每个团队在主树中有一个目录结构,有效地作为项目自己的命名空间。每个源文件可以通过单个字符串唯一标识,即文件路径,可以选择包括修订号。在浏览代码库时,很容易理解任何源文件如何适合存储库的大局。

Google 的代码库是不断增大的。更复杂的代码库现代化工作(例如将其更新到C++11或推出性能优化)通常由专门的代码库维护人员集中管理。这样的努力可以触及到分布在数十万个源代码文件中的数十万个变量声明或函数调用站点。由于所有项目都是集中存储的,专家团队可以为整个公司进行此项工作,而不是需要许多人开发自己的工具、技术或专业知识。

以Google的编译器团队为例,该团队确保Google的开发人员使用最新的工具链,并受益于生成的代码和“可调试性”的最新改进。单一的代码库为团队提供了完整的可见性,以了解Google如何使用各种语言,并允许他们进行代码库范围的清理,以防止更改破坏构建或为开发人员创建问题。这极大地简化了编译器验证,从而减少了编译器发布周期,使得 Google 可以安全地定期发布编译器(C++编译器通常每年发布 20 多个更新)。

使用在整个 Google 代码库的夜间构建上运行的性能和回归测试生成的数据,编译器团队调整默认编译器设置以实现最佳化。例如,由于这项集中的工作,Google 的J ava 开发人员在 2014 年至 2015 年间看到了其垃圾回收(GC)CPU 消耗减少了 50% 以上,GC 暂停时间减少了10%至40%。此外,当发现软件错误时,通常可以让团队添加新的警告以防止再次出现。在这种变化的同时,他们扫描整个代码库以查找和修复其他软件问题的实例,然后再处理新的编译器错误。拥有编译器拒绝过去出现问题的模式对于 Google 的整体代码健康状况是一个重要的提升。

将所有源代码存储在公共版本控制仓库中使得代码库维护人员可以高效地分析和更改 Google 的源代码。像 Refaster 和 ClangMR 这样的工具(常与 Rosie 一起使用)利用 Google 源代码的单体视图执行源代码的高级转换。单体式的代码库捕获了所有的依赖信息。旧的 API 可以自信地移除,因为可以证明所有的调用者都已迁移到新的 API。一个单一的共同仓库通过确保更改的原子性和任何给定时间整个仓库的单个全局视图,大大简化了这些工具的工作。

谷歌文化鼓励代码质量的一个重要方面是,期望所有代码在提交到存储库之前都经过审查。

成本与权衡

需要注意的是,单体代码库并不意味着单体软件设计,使用这种模型需要考虑一些弊端和权衡。

这些成本和权衡可分为三类:

  • 开发和执行工具的投资;
  • 代码库的复杂性,包括不必要的依赖和代码发现困难;
  • 在代码健康方面的投入。

从很多方面来看,单体代码库可以简化工具,因为对于处理源代码的工具,只有一个参考系统。但同时,工具必须要扩展到代码库的规模。例如,谷歌编写了一个 Eclipse 集成开发环境(IDE)的自定义插件,以便从IDE中处理大规模的代码库。谷歌的代码索引系统支持静态分析、代码浏览工具中的交叉引用以及Emacs、Vim和其他开发环境的丰富IDE功能。这些工具需要不断的投资来管理谷歌代码库不断增长的规模。

除了投资于构建和维护可扩展工具之外,谷歌还必须承担运行这些系统的成本,其中一些非常计算密集。谷歌的内部开发者工具套件,包括自动化测试基础设施和高度可扩展的构建基础设施,对于支持单体代码库的规模至关重要。因此,需要权衡运行这些工具的频率,以平衡执行成本和提供给开发人员的数据利益。

单体代码库模型使得理解代码库的结构更容易,因为依赖之间不存在跨存储库的情况。然而,随着规模的增加,代码发现可能变得更加困难,因为像grep这样的标准工具会变得更加缓慢。开发人员必须能够探索代码库,找到相关的库,了解如何使用它们以及谁编写了它们。库的作者通常需要查看其API的使用情况。这需要对代码搜索和浏览工具进行重大投资。然而,Google发现这种投资非常有回报,可以提高所有开发人员的生产力,详见Sadowski等人的更多细节。

访问整个代码库鼓励广泛的代码共享和重用。有人会认为,这种模型依赖于 Google 构建系统的极端可扩展性,使添加依赖项变得过于容易,并降低了软件开发人员生产稳定和经过深思熟虑的API的动力。

由于创建依赖项的便利性,团队通常不考虑其依赖图,使得代码清理更容易出现错误。不必要的依赖会增加项目面临下游构建故障的风险,导致二进制大小膨胀,并在构建和测试方面创建额外的工作量。此外,当留在存储库中的废弃项目继续得到更新和维护时,会导致生产力下降。

Google进行了多项努力,旨在控制不必要的依赖关系。存在一些工具可以帮助识别和删除未使用的依赖关系,或者对历史或意外原因与产品二进制文件链接的不需要的依赖关系。也存在一些工具可以识别未充分利用的依赖项,或者对大型库的依赖项大部分未被使用,作为重构的候选项。其中一个工具Clipper,依赖于自定义Java编译器来生成准确的交叉引用索引。然后使用此索引来构建可达性图并确定哪些类从未使用。Clipper有助于通过找到相对容易移除或拆分的目标来指导依赖项重构工作。

开发人员可以在一次一致的操作中对整个存储库中的数百或数千个文件进行重大更改。

依赖重构和清理工具很有帮助,但理想情况下,代码所有者应该能够防止不必要的依赖关系在首次创建时出现。2011 年,Google开始强调 API 依赖的可见性概念,将新 API 的默认可见性设置为“private”。这迫使开发人员明确标记适合其他团队使用的API。从Google使用大型单体存储库的经验中得出的一个教训是,应尽快实施此类机制,以鼓励更卫生的依赖结构。

大多数 Google 代码对所有 Google 开发人员都是可用的,这导致了一种文化,即一些团队希望其他开发人员阅读他们的代码,而不是为他们提供单独的用户文档。这种方法有利有弊。不需要编写或更新文档,但开发人员有时会阅读 API 代码以外的内容,依赖底层实现细节。这种行为可能会为团队创建维护负担,因为他们难以废弃从未打算向用户公开的功能。

这种模型还要求团队在使用开源代码时互相合作。存储开源代码的一个区域专门用于存储开源代码(在 Google 内部或外部开发的)。为了防止依赖冲突,正如前面所述,重要的是任何给定时间只有一个版本的开源项目可用。使用开源软件的团队有时需要花时间升级其代码库以与更新的开源库版本配合使用,以防止依赖关系冲突。

Google 投入了大量精力来维护代码健康,以解决与代码库复杂性和依赖管理有关的一些问题。例如,特殊的工具可以自动检测和删除死代码,拆分大型重构,并自动分配代码审查(例如 Rosie ),以及标记 API 已过时。人力需要运行这些工具并管理相应的大规模代码更改。团队还需要审查由代码库范围的清理和中心化现代化努力引起的一系列简单重构的持续流。

Piper 的替代品

随着分布式版本控制系统(DVCS)如Git的流行和使用增长,Google已经考虑过是否将其主要版本控制系统从Piper迁移到Git。Google的一个团队专注于支持Git,它被Google的Android和Chrome团队在主要的Google代码库之外使用。对于这些团队来说,使用Git非常重要,因为他们需要与外部合作伙伴和开源社区合作。

Git社区强烈建议开发人员拥有更多、更小的代码仓库。Git-clone操作需要将所有内容复制到本地机器上,这个过程与大型代码库不兼容。要将源代码托管迁移到基于Git的系统,需要将Google的代码库拆分成数千个单独的仓库,才能实现合理的性能。这种重组将需要Google开发人员进行文化和工作流程上的变化。以Google Git托管的Android代码库为比较,分成了800多个单独的仓库。

考虑到Google已经建立的现有工具所带来的价值,以及单体式代码库结构的许多优势,可以明确的是,对于Google的主要代码库来说,转向更多、更小的仓库是不合理的。转向Git或任何需要仓库拆分的其他DVCS的选择对于Google来说也不是很有吸引力。

Google源代码团队当前的投资主要集中在内部源代码系统的持续可靠性、可扩展性和安全性上。该团队还与Mercurial社区合作,探索一个实验性的项目,它是一个类似Git的开源DVCS。目标是向Mercurial客户端添加可扩展性功能,以便它能够有效地支持像Google这样的代码库。这将为Google的开发人员提供使用流行的DVCS工作流程与中央代码库相结合的替代方案。这个项目与开源Mercurial社区以及其他看重单体式源代码模型的公司的贡献者合作进行。

结论

1999 年,当现有的 Google 代码库从 CVS 迁移到 Perforce 时,Google 选择了单体源管理策略。早期的 Google 工程师认为,单一的代码库比分割代码库严格地更好,尽管当时他们没有预料到未来代码库的规模以及为使扩展可行而构建的所有支持工具。

随着持续扩展集中式代码库所需的投资不断增加,多年来,Google 领导层偶尔考虑是否有意义从单体模型转移出来。尽管需要付出努力,但因为它的优势,Google 一再选择坚持使用中央代码库。

源代码管理的单体模型并非适用于每个组织。它最适合像 Google 这样具有开放和协作文化的组织。它对于大部分代码库是私有或在组之间隐藏的组织不会起到良好的作用。

在 Google,我们发现,在一些投资后,单体源管理模型可以成功地扩展到拥有超过 10 亿个文件,3500 万个提交和遍布全球的数千个用户的代码库。随着项目内外的规模和复杂性的不断增长,我们希望本文所描述的分析和工作流程可以使正在权衡其代码库长期结构决策的其他人受益。

FAQ 1. :如何处理仓库与服务部署之间的影射

我很好奇源代码模型(单体库 vs 多个库)与部署模型之间的相互作用,特别是考虑持续部署 vs 明确发布的情况。

我的理解是,Google 的服务是从主干编译和部署的;这对于数据库迁移(例如,模式升级)意味着什么,特别是当同一服务的不同实例由不同的团队维护时:在二进制文件更或多或少连续地升级的情况下,如何协调这些分布式数据迁移?由于不存在软件包的发布和稳定版本的概念,您是否需要实现无限的向后兼容性? 同样,当一个服务从今天的主干部署,但依赖服务仍在上周的主干上运行时,如何保证这些服务之间的 API 兼容性? 看起来,必须建立严格的跨服务 API 和模式兼容性协议,以防止由于实时升级而导致的故障?

回复:

团队可以打包他们自己的二进制文件,在数据中心生产环境中运行。

实际上,在发布二进制文件的团队和使用它们的客户之间存在一个服务级别协议(SLA)。如果你不喜欢SLA(包括向后兼容性),你可以自由地编译自己的二进制包来运行在生产环境中。

迁移通常通过三个步骤来完成:先进行公告,然后是新代码的转移,最后通过删除废弃的旧代码来完成。


原文作者:Rachel Potvin, Josh Levenberg 原文链接:Why Google Stores Billions of Lines of Code in a Single Repository 发表时间: July 2016