相对于一般公司,Google 使用了单一代码仓库,很多人不理解为什么这么做。本文作者是谷歌基础设施小组的工程师,他对这个问题进行了详细解读。
早期 Google 员工决定使用集中式源代码管理系统来管理代码库。这种方法已经在 Google 运行了 16 年以上,而今天绝大多数的 Google 软件仍然存储在一个共享的代码库中。
随着 Google 开发软件数量稳步增加,Google 代码库的规模也呈指数增长。 因此,用于管理代码库的技术也发生了显著变化。
本文概述了该代码库的规模,并详细介绍了 Google 定制的集中式代码库以及该模型的选择原因。Google 使用自主开发的版本控制系统,管理公司的代码库,这个集中式系统是许多 Google 开发人员工作流程的基础。在这里,我们提供了系统和工作流的背景,这些系统和工作流程可以有效地管理和高效地使用这样一个大型代码库。我们将解释 Google 的“基于 trunk 的开发”策略和支持系统,以及构建工作流程,还有保持 Google 的代码库健康的工具,包括用于静态分析,代码清理和简化 code review 的软件。
Google 规模
Google 95%的软件开发人员使用的代码库满足超大规模系统的定义[4],该仓库是可以成功扩展集中式代码库的证据。
Google 代码库包含大约十亿个文件,并且具有约 3500 万次提交的历史(包含 Google 18 年所有的代码提交)。该代码库包含 86TB 的数据,包括 900 万个源文件以及大约 20 亿行代码。
文件总数还包括复制到发布分支的源文件、最新版本删除的文件、配置文件、文档和数据文件。请参阅此处的表格,以了解 2015 年 1 月以来 Google 存储库统计信息的摘要。
2014 年,每周在 Google 代码库中有 1500 万行代码被修改。相比之下,Linux 内核是一个大型开源软件代码库示例,该代码库包含的 40,000 个文件中共有大约 1500 万行代码。[14]
Google 的代码库由来自世界各国数十个办事处的 25,000 多名 Google 软件开发人员共享。在典型的工作日,他们通常会对代码库进行 16,000 次更改,另有 24,000 次更改由自动化系统提交。
每天,代码库提供数十亿次文件读取请求,峰值每秒大约有 80 万个查询,工作日平均每秒大约有 50 万个查询,大部分流量来自 Google 的分布式构建和测试系统。
总提交的代码包括交互式用例或用户数据以及自动化提交的代码,假期(如圣诞节和元旦,美国感恩节和美国独立日)会有大幅度提交行数下跌。
2012 年 10 月,Google 代码库增加了对 Windows 和 Mac 用户的支持(之前仅支持Linux),现有的 Windows 和 Mac 代码库与主代码库合并,Google 的代码库合并工具将所有历史变更归因于其原始作者。
根据每周提交的图表显示,到2012年之前,提交率由用户主导,此时 Google 将代码库改为私有实现。如下所述,在此之后,自动提交到存储库的代码开始增加,代码提交的增长主要是由于自动化。
管理这种规模的代码库和开发对于 Google 来说是一个持续的挑战,尽管经过几年的试验,Google 还没有找到一个商业上可用的或开放源代码版本控制系统,以便在单一代码库中支持这种规模,Google 解决此问题的专有系统是 Piper。
背景
你在审视使用单一代码库的优缺点之前,需要了解一些 Google 工具和工作流的背景。
Piper and CitC
Piper 是一个大型代码库,在标准的 Google 基础设施上实现,最初是基于 BigTable,现在是基于Spanner。[3] Piper 分布在全球 10 个 Google 数据中心,依靠 Paxos [6]算法来保证副本一致性。
该架构提供了高冗余,并有助于优化 Google 软件开发人员的延迟。此外,缓存和异步操作可以隐藏大量网络延迟。这很重要,因为获得 Google 云工具链的全部优势需要开发人员在线。
在推出 Piper 之前,Google 主要依靠一台 Perforce 实例(加上自定义缓存基础架构[1],提供服务超过 10 年)。继续扩展 Google 代码库是开发 Piper 的主要动力。
由于 Google 的源代码是公司最重要的资产之一,因此安全功能是 Piper 设计的关键考虑因素。Piper 支持文件级访问控制列表,所有Piper用户都可以看到大部分代码库,也可以更严格地控制重要的配置文件或关键算法的文件。
可以对 Piper 中的文件进行读写访问。如果敏感数据文件被意外地提交给 Piper,则可以清除该文件,读取日志允许管理员确定是否有人在删除问题文件之前访问过该文件。
在 Piper 工作流程中,开发人员在更改代码库之前创建文件的本地副本,这些文件存储在开发人员拥有的工作区中。
Piper 工作区与 Apache Subversion(Git 中的本地克隆)或 Perforce 中的客户端的工作副本相当,Piper 代码库中的更新可以根据需要被拉入工作空间并与正在进行的工作进行合并。
可以与其他开发人员共享工作空间快照以供审核,工作空间中的文件仅在经过 Google code review 过程后才会提交到中央代码库。
大多数开发人员通过名为 Clients in Cloud 的系统或 CitC 访问 Piper,该系统由基于云的存储后端和 Linux FUSE [13] 文件系统组成,开发人员将他们的工作空间看作是文件系统中的目录,将更改覆盖在完整的Piper库之上。
CitC 支持代码浏览和 Unix 工具,无需本地克隆或同步状态。开发人员可以在 Piper 存储库中的任何地方浏览和编辑文件,只有修改的文件才存储在其工作空间中。
这种结构意味着 CitC 工作区通常仅消耗少量存储(平均工作空间少于 10 个文件),同时向开发人员呈现整个 Piper 代码库。
对文件的所有写入都作为快照存储在 CitC 中,使得可以根据需要恢复以前的工作阶段,可以明确命名,恢复或标记快照以供审核。
CitC 工作区可以在任何连接到云的机器上使用,从而轻松切换机器并且不间断地工作,这也使得开发人员可以在 CitC 工作区中查看彼此的工作,将所有正在进行中的工作存储在云中是 Google 工作流程的重要组成部分。
工作状态可用于其他工具,包括基于云的构建系统,自动测试基础架构以及代码浏览,编辑和查看工具。
有几个工作流程利用了 CitC 中未提交代码的特性,使软件开发人员能够更有效率的使用大型代码库。
例如,当发送更改 code review 时,开发人员可以启用自动提交选项,这在代码作者和审阅者处于不同的时区时特别有用。review 被标记为完成时,测试将会运行。
如果可以通过测试,代码将被合并到代码库,不需要进一步的人工干预,Google 代码浏览工具 CodeSearch 支持使用 CitC 工作区进行简单的编辑。
浏览资料库时,开发人员可以点击按钮进入编辑模式,并进行简单的更改(例如修改打字或改进评论)。然后,在不离开代码浏览器的情况下,他们可以将自己的更改发送到适当的审阅者,并启用自动提交。
Piper 也可以在没有 CitC 的情况下使用,开发人员可以将 Piper 工作区存储在本地计算机上,Piper 还可以和 Git 互操作。目前,超过 80% 的 Piper 用户使用 CitC,由于 CitC 有许多优势,使用率持续增长。
Piper 和 CitC 可以保证在 Google 代码库的规模下,使用单一代码库进行有效的工作。这些系统的设计和架构都受到 Google 采用的基于 trunk 的开发模式的影响,如下所述。
基于 trunk 的开发
Google 在 Piper 源代码库之上实施基于 trunk 的开发。Piper 用户绝大多数在“head”或最新版本的“trunk”或“mainline”代码副本中工作,对代码库的更改是串行的。
基于 trunk 的开发与中央代码库的组合定义了单一代码库模型,在任何提交之后,其他所有开发人员都能看到更改。Piper 用户对 Google 代码库的一致视图是提供本文后面描述的优势的关键。
基于 trunk 的开发是有益的,因为它避免了合并长支链分支时的痛苦。尽管代码分支通常用于发布上线,但是在 Google 代码分支支持的不好。
通常在 trunk 上开发 bug fix 和必须添加到版本中的增强功能,然后将其引入到 release 分支中.
由于需要保持稳定性并限制发布分支上的流失,所以 release 通常是“head”的快照,根据需要从”head”拉出可选的少量带代码,在 branch 和 trunk 上并行开发的长寿命 branch 是非常罕见的。
Piper 和 CitC 可以在 Google 代码库的规模下,使用单一源代码库进行有效的工作。
当开发新功能时,新旧代码路径通常同时存在,通过使用条件标志来控制。这种技术避免了开发分支的需要,并且通过配置更新来打开或者关闭功能。
虽然开发人员还需要一些额外的复杂性,但是避免了开发分支合并问题,标志翻转使得用户切换具有问题的新实现变得更加容易和快捷。
该方法通常用于项目特定的代码,而不是通用的库代码,最终会删除标志和旧代码。Google 使用类似的方法来对不同代码做测试,这样的 A / B test 可以从代码性能得到与产品变化相关的参数。
Google 工作流程
需要几种最佳实践和支持系统,以避免在基于 trunk 的开发模式中碰到的问题。例如,Google 有一个自动测试基础设施,可以在几乎每个提交上启动所有受影响的依赖项测试。
如果一次代码更改造成构建破坏,系统就会自动撤消更改。为了减少发生的错误代码的发生率,高度可定制的 Google “预提交”基础架构可以在更改代码添加到代码库之前自动进行测试和分析。
针对所有更改运行一组全局预先提交分析,代码所有者可以创建仅在其指定的代码库中的目录上运行的自定义分析,仅有一小部分非常低级别的核心库使用 branch 的机制,以保证在新版本暴露给客户端代码之前执行其他测试。
鼓励代码质量的一个重要方面是期望在提交到代码库之前对所有代码进行 review。大多数开发人员可以在代码库的任何地方查看和建议更改(除了一组更加精心控制的高度机密代码之外)。
不熟悉的开发人员更改相关代码的风险通过代码 review 过程和代码所有权的概念得到缓解,Google 代码库以树结构布局,每个目录都有一组所有者控制是否接受目录中文件的更改。
所有者通常是在相关目录中处理项目的开发人员,变更通常会从一位开发人员收到详细的代码审查开始,从而评估变更的质量,以及所有者的认可批准,评估变更对的适用性。
代码 review 者会对代码质量方面进行评论,包括设计,功能,复杂性,测试,命名,评论质量和代码风格。
Google 已经编写了一个名为 Critique 的代码审查工具,允许审阅者查看代码的演变,并对任何一行的更改进行评论。它鼓励进一步的修改和 review,以达到所有者的要求。
Google 的静态分析系统(Tricorder [10])和预提交基础设施还可以在 Google 代码审查工具中自动提供有关代码质量,测试覆盖率和测试结果的数据。这些计算密集型检查被定期触发,发送代码修改以供 review。
Tricorder 还为许多错误提供了修改的建议,这些系统提供重要数据,以提高代码审查的有效性,并保持 Google 代码库的健康。
Google 开发人员小组不时进行代码清理,以进一步维护代码库的健康。执行这些更改的开发人员通常将过程分为两个阶段。
首先进行大的向后兼容的更改,一旦完成,可以进行第二个较小的更改以删除不再引用的代码,Rosie 工具支持这种大规模清理和代码更改的第一阶段。
使用 Rosie,开发人员可以创建一个大补丁。Rosie负责将大补丁分成较小的补丁,独立测试,发送出去进行代码 review,并在通过测试和代码审查后自动提交。
Rosie 根据项目目录行拆分补丁,依靠前面描述的代码所有权层次结构将补丁发送给适当的审阅者。
随着 Rosie 的流行度和使用率的增长,显而易见,必须建立一些控制措施,以将 Rosie 的用途限制高价值变化中。
2013 年,Google 通过了正式的大规模变化 review 流程,导致了从 2013 年到 2014 年的 Rosie 数量的减少。在评估 Rosie 变更时,评审委员会将变更的收益与审阅者时间和存储库流失的成本相平衡,我们稍后更仔细地研究类似的权衡。
总而言之,Google 开发了许多工具来支持其庞大的代码库,包括基于 trunk 的开发,分布式源代码存储库 Piper,工作区客户端 CitC 以及工作流支持工具 Critique,CodeSearch,Tricorder,和 Rosie,我们在这里讨论这个模型的利弊。
分析
本节概述并扩展了单一代码库的优势以及与维护此类模型规模相关的成本。
优点
支持超大规模的 Google 代码库,同时为成千上万的用户服务,保持良好的性能是一个挑战,但由于其引人注目的优势,Google 已经拥抱了单一代码库。
最重要的是它支持:
- 统一版本。
- 广泛的代码共享和重用。
- 简化依赖关系管理。
- 原子变化。
- 大规模重构。
- 团队合作。
- 灵活的团队边界和代码所有权。
- 代码可见性和清晰的树结构,提供隐含的团队命名空间。
单一代码库提供统一的版本控制和单一代码来源。对于哪个存储库托管文件的权威版本,并不存在任何混淆。
如果一个团队想要依赖另一个团队的代码,可以直接依赖,Google代码库包含大量有用的库,而单一代码库可以引导广泛的代码共享和重用。
Google 构建系统[5]可以轻松地在目录之间包含代码,从而简化依赖关系管理。对项目的依赖性的更改会触发依赖代码的重建,由于所有代码都在相同的存储库中进行版本控制,所以只有一个版本,也不关心依赖关系的独立版本。
最值得注意的是,该模型允许 Google 避免当 A 依赖于 B 和 C 时发生的“钻石依赖”问题,B 和 C 都依赖于 D,但 B 需要版本 D.1 和 C 需要版本 D 0.2。
在大多数情况下,可能很难在不导致破坏的情况下发布新版本,因为所有调用方必须同时更新,当库调用者托管在不同的存储库中时,这种更新很困难。
在开源世界中,依赖关系通常被库更新所破坏,查找所有共同工作的依赖库版本都是一个挑战。更新依赖关系的版本对于开发人员来说可能是痛苦的,延迟更新可能会变成非常昂贵的技术债务。
使用单一代码库,对于更新库的人来说,在同一时间更新所有受影响的依赖关系更容易。依赖引起的技术性债务在作出变更时立即予以偿还,基础库的更改将立即通过依赖关系链传播到依赖于库的最终产品中,而不需要单独的同步或迁移步骤。
请注意,如下所述,在源/ API 级别以及二进制文件之间可能存在钻石依赖问题。[12]在谷歌,通过使用静态链接避免了二进制问题。
进行原子变化的能力也是整体模型的一个非常强大的特征,开发人员可以在一致的操作中,对代码库中的数百或数千个文件进行重大变更。例如,开发人员可以在单个提交中重命名类或函数,但不会破坏任何构建或测试。
在单一代码库中,或至少在集中式服务器上,所有源代码的可用性使得核心库的维护者在提交高影响力更改之前可以更轻松地执行测试和性能基准测试。
这种方法对于探索和测量高度破坏性变化的价值是有用的, 一个具体的例子是评估转换 Google 数据中心以支持非 x86 机器架构的可行性的实验。
由于 Google 代码库的结构,开发人员无需决定代码库边界,工程师不需要“branch”共享库的开发,或者跨仓库合并来更新代码。
团队边界是流动的,当项目所有权更改或计划合并系统时,所有代码都已在同一个库中。这种环境使代码库的循环重构和重组变得容易,移动项目和更新依赖关系可以原子地应用于代码库,并且受影响代码的开发历史保持不变且可用。
单一代码库的另一个属性是容易理解的代码库的布局,因为它被组织在单个树中,每个团队在主树中都有一个目录结构,有效地充当项目自己的命名空间。
每个源文件都可以通过单个字符串唯一标识,该文件路径可选地包含修订版本号,浏览代码库,很容易了解任何源文件如何适用于代码库。
Google 代码库不断发展,更复杂的代码库现代化工作(例如将其更新为 C++ 11 或推出性能优化[9])通常由专用的代码库维护者集中管理。
这样的努力可以触及五十万个变量声明或函数调用点(分布在数十万个源代码文件中),由于所有项目都集中存储,所以专家团队可以为整个公司做这项工作,而不是要求很多人开发自己的工具。
举个例子
请考虑 Google 的编译器团队,他们会确保 Google 的开发人员使用最新的工具链,并从生成的代码和“可调试性”的最新改进中获益。
单一代码库使编译团队能够全面了解 Google 如何使用各种语言,并允许他们进行代码库范围的清理,以防止更改破坏构建。
这大大简化了编译器验证,从而减少了编译器发布周期,并使 Google 有可能安全地执行编译器版本(通常每年对 C ++ 编译器来说超过 20 个)升级。
通过对夜间运行性能测试和回归测试产生的数据进行分析,编译器团队可以将默认编译器设置调整为最佳。
例如,谷歌的 Java 开发人员都看到他们的垃圾回收(GC) CPU 消耗量下降了50%以上,而且 GC 停留时间从 2014 年到 2015 年下降了 10%-40%。另外,当软件发现错误,编译器团队有可能添加新的警告以防止错误重复发生。
结合此更改,他们会扫描整个存储库以查找并修复正在存在该问题的其他实例,然后再转到新的编译器错误,过去的实践证明编译器拒绝有问题的代码大大提升了 Google 的代码运行状况。
将所有源代码存储在通用版本控制存储库中可以使代码库维护者有效地分析和更改 Google 的源代码。像 Refaster [11] 和 ClangMR [15] (通常与 Rosie 一起使用)这样的工具利用 Google 源代码的单一视图来执行源代码的高级转换。
单一代码库捕获所有依赖关系信息,可以放心地删除旧的 API。因为可以使所有调用者使用新API,在任何给定时间,通过确保更改的原子性和整个存储库的单一全局视图,单一代码库极大地简化了这些工具的开发过程。
鼓励代码质量的 Google 文化其中一个重要方面是期望在提交到代码库之前对所有代码进行审核。
成本和权衡
注意单一代码库绝不意味着整体化的软件设计,使用这个模型涉及必须考虑一些缺点和权衡。
这些成本和权衡分为三类:
- 开发和执行的工具投资。
- 代码库复杂性,包括不必要的依赖性和代码发现的困难。
- 达到代码健壮性的努力。
在许多方面,单一代码库导致更简单的工具。然而,还需要将工具规模扩展到代码库的规模。
例如,Google 已经为 Eclipse 集成开发环境(IDE)编写了一个自定义插件,以使 IDE 能够使用大型代码库。
Google 的代码索引系统支持静态分析,代码浏览工具中的交叉引用,以及 Emacs,Vim 和其他开发环境的丰富的 IDE 功能,这些工具需要持续的投资来管理日益增长的 Google 代码库规模。
除了建立和维护可扩展工具的投资外,Google 还必须承担运行这些系统的成本,其中一些是非常计算密集型的。
许多 Google 的内部开发人员工具套件,包括自动化测试基础架构和高度可扩展的构建基础设施,对于支持单一代码库的规模至关重要。因此,必须权衡如何运行这些工具以平衡执行成本与提供给开发人员的数据的好处。
单一代码库更容易理解代码库的结构,因为在依赖关系之间没有跨仓库边界。然而,随着规模的增加,代码查找变得更加困难,因为像grep这样的标准工具基本不可用。
开发人员必须能够探索代码库,找到相关的库,并了解如何使用它们以及谁编写它们,库作者经常需要了解他们的 API 如何被使用。
这需要对代码搜索和浏览工具的重大投资,Google 已经发现这种投资非常有益,提高了所有开发人员的生产力。[9]
访问整个代码库鼓励广泛的代码共享和重用,有些人会认为,这种模式依赖于 Google 构建系统的可扩展性,使得添加依赖关系变得太容易,并且减少了软件开发人员设计稳定且精心设计的 API 的动机。
由于创建依赖关系的轻松,通常团队不要考虑其依赖关系图,使代码清理更容易出错。不必要的依赖可能会增加项目对下游构建破坏的风险,导致二进制文件膨胀,并在构建和测试中创造额外的工作,此外,维护遗留项目会导致生产力下降。
Google 的试图控制不必要的依赖,已经有工具帮助识别和删除不需要依赖关系。还存在用于识别未充分利用的依赖关系或识别不需要的库的工具。
工具 Clipper 依赖于一个自定义的 Java 编译器来生成一个精确的交叉引用索引,然后,它使用索引构建可达性图,并确定从不使用什么类。
Clipper 可以通过帮助开发人员找到相对容易删除或分解的目标来指导依赖重构的工作。、
开发人员可以在一个一致的操作中,通过存储库中的数百或数千个文件进行重大变更。
依赖重构和清理工具是有帮助的,但理想情况下,代码所有者应该能够防止创建不必要的依赖关系。
2011 年,Google 开始推广 API 可见性的概念,将新 API 的默认可见性设置为“私有”,这迫使开发人员明确地标记 API,以供其他团队使用。从 Google 的大型代码库的经验中学到的教训应该是尽快实施,以鼓励更好的依赖结构。
大多数 Google 代码可供所有 Google 开发人员使用,这导致了一种文化,一些团队希望其他开发人员阅读他们的代码,而不是为他们提供单独的 API 文档。
这种做法有利与弊,开发人员有时会阅读 API 代码,最终依赖于底层的实现细节,这种行为可能会为那些不愿意向用户暴露的细节的团队提供一些维护负担。
该模型还要求团队在使用开源代码时相互协作,存代码的一个区域保留用于开源代码(在 Google 开发或外部开发)。
为了防止依赖冲突,需要确保在任何给定的时间只有一个开源版本可用,使用开源代码的团队在进行依赖升级时,会花时间处理新版本的开源库。
Google 投入巨大的努力来维护代码健康,以解决与代码库复杂性和依赖关系管理相关的一些问题。
例如,专用工具会自动检测和删除死码,分割大量重构,并自动分配代码评估(如通过 Rosie),并将 API 标记为不推荐使用。
需要人力运行这些工具并管理相应的大规模代码更改,审查代码库范围内的清理和其他工作引起的持续简单重构也会产生成本。
备择方案
随着像 Git 这样的分布式版本控制系统(DVCS)的普及和使用越来越多,Google 考虑是否将 Piper 转移到 Git 作为其主要的版本控制系统。
Google 的一个团队专注于支持 Git,Google 在 Google 主代码库之外由 Google 的 Android 和 Chrome 团队使用,由于外部合作伙伴和开源协作,使用 Git 对于这些团队很重要。
Git 社区强烈建议开发人员拥有越来越多的代码库,Git-clone 操作需要将所有内容复制到本地计算机,这是与大型存储库不兼容的过程。要转移到基于 Git 的源代码托管,有必要将 Google 的存储库拆分成数千个独立的存储库,以实现合理的性能。
这样的重组将需要 Google 开发人员的文化和工作流程更改。作为比较,Google 的 Git 托管的 Android 代码分为超过 800 个独立的代码库。
鉴于 Google 已经建立的现有工具所获得的价值以及整体代码库结构的许多优势,转换到越来越多的代码库对于 Google 的主代码库来说是没有意义的,移动到 Git 或需要代码库拆分对 Google 来说并不引人注目。
Google 源代码团队目前的投资主要集中在内部源代码系统的持续可靠性,可扩展性和安全性上,该团队还在与Mercurial进行实验性工作,这是一款类似 Git 的开源DVCS。
目标是向 Mercurial 客户端添加可伸缩性功能,以便高效地支持 Google 的规模。这将为 Google 开发人员提供一种与单一代码库库一起使用流行的 DVCS 风格工作流的替代方案。
这一努力与开源的 Mercurial 社区合作,其中包括来自其他公司的贡献者。
结论
Google 在 1999 年将现有的 Google 代码库从 CVS 迁移到 Perforce 时,选择了单一源代码管理策略,早期的 Google 工程师认为,单独的代码库比多个代码库要严格得多,尽管当时他们没有预料到代码库的未来规模以及所有支持的工具。
多年来,随着继续扩大集中式存储库所需的投资增长,Google 领导层偶尔会考虑从单模模式转变是否有意义。尽管需要努力,但由于其优势,Google 选择坚持使用集中式单一代码库。
源代码管理的单一模型不适合所有人,它最适合像 Google 这样的组织,具有开放和协作的文化。对于代码库的大部分是私有的或组之间隐藏的组织来说,这不太适用。
在 Google 方面,我们发现通过一些投资,源代码管理的整体模式可以成功扩展到具有超过十亿个文件,3500 万个提交和全球数千个开发者的代码库。
随着 Google 和 Google 内部项目的规模和复杂性不断增长,我们希望本文中描述的分析和工作流程可以使他们对其代码库的长期结构进行权衡决策。