连载:谷歌为什么使用MONOREPO?(下)
原文作者:Rachel Potvin, Josh Levenberg
原文链接:Why Google Stores Billions of Lines of Code in a Single Repository
发表时间:July 2016
3
本节概述并讨论了单一代码库的优点,以及与大规模维护这种模型相关的成本。
3.1 优点
Google面临的挑战是如何支持其巨大的单体式代码库,同时为数以万计的用户维持良好的性能,但由于其具有很高的吸引力和优势,Google已经采用了单体式模型。
最重要的是,它支持:
统一版本控制,一个真理源;
广泛的代码共享和重用;
简化的依赖管理;
原子性的更改;
大规模重构;
跨团队协作;
灵活的团队边界和代码所有权;
代码可见性和清晰的树形结构,提供了隐式的团队命名空间。
单个存储库提供了统一的版本控制和一个真正的源头。没有哪个存储库包含文件的权威版本会造成困惑。如果一个团队想要依赖于另一个团队的代码,它可以直接依赖。Google 的代码库包括大量有用的库,而单体式存储库促进了广泛的代码共享和重用。
Google 的构建系统使得跨目录包含代码变得容易,简化了依赖管理。对一个项目依赖的更改会触发依赖代码的重构。由于所有代码都在同一个存储库中进行版本控制,所以只有一个版本的真相,不用担心依赖的独立版本控制。
最值得注意的是,这种模型使得Google避免了“钻石依赖”问题(见图8)。当A依赖于 B 和 C 时,B 和 C 都依赖于 D,但 B 需要版本 D.1,而C需要版本 D.2,大多数情况下无法构建 A 。对于基础库 D,发布新版本很难不会引起破坏,因为所有的调用者必须同时更新。当库调用者在不同的存储库中托管时,更新就变得困难了。
图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。一个单一的共同仓库通过确保更改的原子性和任何给定时间整个仓库的单个全局视图,大大简化了这些工具的工作。
谷歌文化鼓励代码质量的一个重要方面是,期望所有代码在提交到存储库之前都经过审查。
3.2 成本与权衡
需要注意的是,单体代码库并不意味着单体软件设计,使用这种模型需要考虑一些弊端和权衡。
这些成本和权衡可分为三类:
开发和执行工具的投资;
代码库的复杂性,包括不必要的依赖和代码发现困难;
在代码健康方面的投入。
从很多方面来看,单体代码库可以简化工具,因为对于处理源代码的工具,只有一个参考系统。但同时,工具必须要扩展到代码库的规模。例如,谷歌编写了一个 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 已过时。人力需要运行这些工具并管理相应的大规模代码更改。团队还需要审查由代码库范围的清理和中心化现代化努力引起的一系列简单重构的持续流。
2
随着分布式版本控制系统(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社区以及其他看重单体式源代码模型的公司的贡献者合作进行。
5
1999 年,当现有的 Google 代码库从 CVS 迁移到 Perforce 时,Google 选择了单体源管理策略。早期的 Google 工程师认为,单一的代码库比分割代码库严格地更好,尽管当时他们没有预料到未来代码库的规模以及为使扩展可行而构建的所有支持工具。
随着持续扩展集中式代码库所需的投资不断增加,多年来,Google 领导层偶尔考虑是否有意义从单体模型转移出来。尽管需要付出努力,但因为它的优势,Google 一再选择坚持使用中央代码库。
源代码管理的单体模型并非适用于每个组织。它最适合像 Google 这样具有开放和协作文化的组织。它对于大部分代码库是私有或在组之间隐藏的组织不会起到良好的作用。
在 Google,我们发现,在一些投资后,单体源管理模型可以成功地扩展到拥有超过 10 亿个文件,3500 万个提交和遍布全球的数千个用户的代码库。随着项目内外的规模和复杂性的不断增长,我们希望本文所描述的分析和工作流程可以使正在权衡其代码库长期结构决策的其他人受益。
FAQ 1. :如何处理仓库与服务部署之间的影射
我很好奇源代码模型(单体库 vs 多个库)与部署模型之间的相互作用,特别是考虑持续部署 vs 明确发布的情况。
我的理解是,Google 的服务是从主干编译和部署的;这对于数据库迁移(例如,模式升级)意味着什么,特别是当同一服务的不同实例由不同的团队维护时:在二进制文件更或多或少连续地升级的情况下,如何协调这些分布式数据迁移?由于不存在软件包的发布和稳定版本的概念,您是否需要实现无限的向后兼容性?同样,当一个服务从今天的主干部署,但依赖服务仍在上周的主干上运行时,如何保证这些服务之间的 API 兼容性?看起来,必须建立严格的跨服务 API 和模式兼容性协议,以防止由于实时升级而导致的故障?
回复:
团队可以打包他们自己的二进制文件,在数据中心生产环境中运行。
实际上,在发布二进制文件的团队和使用它们的客户之间存在一个服务级别协议(SLA)。如果你不喜欢SLA(包括向后兼容性),你可以自由地编译自己的二进制包来运行在生产环境中。
迁移通常通过三个步骤来完成:先进行公告,然后是新代码的转移,最后通过删除废弃的旧代码来完成。
乔梁老师开课啦~视频课程《持续部署训练营(Python版)》,原价1699元, 限时特价699元
你的软件开发效率够高吗?质量够好吗?你的团队多长时间才能向用户实时推送一个生产变更?你的软件在发布时,你是否因担心软件交付质量而感到压力倍增?你是否遇到过部署失败,甚至导致停机的情况?持续部署可以帮助你消除软件交付的痛苦,让你能专注于为客户高价值的需求,而不会因为这些交付执行类问题而花太多精力。本课程通过学练结合,理论结合实战,让你体验如何使用构建-测试-部署管道,进行持续部署。并练习如何在有效监控部署的同时,逐步发布功能特性,并作数据库结构变化。
(扫码订阅)