Google 是如何构建 Web 框架的

众所周知,Google 使用单一代码库来共享所有 20 亿行代码,并且使用主干(trunk-based)开发模式。

它无疑是世界上最大的单一代码库之一。(具体内容见此

对于公司之外的众多开发者来说,尽管这点令人惊讶,并且感觉违反常理,但它实际上运作得很好。(上面的文章已经给出了很好的例子,这里我就不重复了。)

Google 的代码库为世界各地数十个办事处,共超过 2.5 万名 Google 软件开发者提供共享服务。在一个普通的工作日内,他们能对代码库进行 1.6 万次修改。(具体内容见此

本文是关于如何构建一个开源 Web 框架(AngularDart)的一些细节。

“人类用户”指的是在 Google 上提交代码的软件工程师,它与资源生成工具相反。(具体内容见此

仅有单一版本

当你在单一代码库中使用主干开发模式时,你拥有的一切都是单一版本的。从字面上看,这很明显,但仍需要特别指出,它的意思是,Google 的 FooBar 不会有 AngularDart 2.2.1 和 2.3.0 两个版本,一定只会存在单一版本,而且是最新的版本。

(说明图片来自 trunkbaseddevelopment.com

这就是为什么 Google 的员工有时会说,他们所有的软件都处于行业前端,使用了最新的技术。

如果这时你突然想大叫“危险!”,这是可以理解的。诚然,仅仅依靠生产代码库中的主干(也就是 git 中的“master”),这听起来确实很危险。但别急,前方还有一个剧情转折点。

每个提交前的 7.4 万次测试

AngularDart 定义了 1601 个测试(看这里)。但是,当你在 Google 代码库中修改了 AngularDart 代码时,它也会为那些使用此框架的 Google 员工进行测试。目前,一次提交大约会进行 7.4 万次测试(取决于更改的程度,而系统会启发性地跳过不受影响的测试)。

当然,测试越多越好。

例如,我做了一个小更动(在这条 if 语句中添加了 && random.nextDouble() > .05),让它只显示了 5% 的时间,以模拟更改检测及验证算法中的竞态条件。运行后,它并没有开始 1601 个测试,却开始了一堆客户端测试。

它真正的价值在于,测试的是真实应用程序。它们不仅数量众多,还反映了开发者如何使用框架(而不仅局限于框架作者)。重要的一点是:框架作者并不是总能够正确地估计框架的使用情况。

这也能帮助生产中的应用程序获得每月数十亿美元的流水。框架作者在业余时间完成的演示应用程序,与数十或数百人投入数年产生的实际应用程序之间,仍然存在着很大差异。如果未来的网络是息息相关的,我们需要更好地支持后者的发展。

那么,如果框架破坏了其中一些应用程序,会发生什么事呢?

谁破坏,谁修复

如果 AngularDart 的框架作者引入了一种破坏性的更改,他们必须为用户去修复它。由于 Google 使用了单一的代码库,更容易发现破坏者,而且可以马上对其修复。

对 AngularDart 的任何破坏性的更改,也包括对所有依赖于 Google 的应用程序的修复。因此,破坏和修复,在由相关方面审查代码后,会同时进入代码库。

让我们举一个具体的例子:当 AngularDart 团队的某个人做出一些更改,影响到 AdWords 应用程序的代码时,他们会审查该应用程序的源代码并修复它。修复过程中,他们可以运行 AdWords 现有的测试,还可以添加新的测试。然后,他们会把所有这一切放到变更列表中,并要求进行代码审查。由于变更列表涉及到 AngularDart 和 AdWords 两者的代码,系统会自动要求这两个团队进行代码审查。只有两边都通过,才能提交更改。

这很明显能够阻止框架开发过程中,毫无督促自由发挥这一情况的发生。AngularDart 框架的开发者们可以访问自己平台构建的数百万行代码,而且由于经常接触这些代码,他们不需要设想别人如何使用他们的框架。(值得一提的是,他们只能看到了 Google 内部使用此框架者的代码,而不是世界上所有使用此框架者的代码。)

升级用户的代码也会降低开发速度,影响不会有你想的那么大,(你可以看看 10 月以来 AngularDart 的进展),但总归会让开发进度降低一点。最终结果的好坏取决于你想从框架中得到什么。等会儿我们再回到这个问题上来。

那么,下一次,你就能理解,为什么 Google 的某人会,某个库的 Alpha 版本是稳定的,并可以应用到生产环境了。

大规模改动

如果 AngularDart 需要做一个重大改动(比方说,版本从 2.x 升级到 3.0),会影响到 7.4 万个测试吗?团队要修复所有的问题吗?他们是否要对数千个源文件进行修改,甚至其中的大部分都不是他们编写的?

答案是 Yes。

类安全系统(sound type system)能够让一切变得更加高效、简单。例如,在类安全系统 Dart 中,工具可以是某个变量的特定类型。对其中一个变量的修改,能够自动化应用到所有相同类型,无需开发者手动更改。

当类 Foo 上的一个方法从 bar() 变更为 baz() 时,你可以创建一个工具,来遍历整个单一 Google 代码库,查找 Foo 类及其子类的所有实例,并将所有的 bar() 变更为 baz()。借助类安全系统 Dart,你可以确保不影响其他部分。没有类安全系统,即便是一个简单的改动也是麻烦多多。

 
一次按键,你的代码就能根据 Dart 的风格指南进行格式化。事实上,指南中有提到:“Dart 官方的空格处理规则是由 dart_style 进行的。”

dart_style(Dart 的默认格式化程序)是另一个有助于大规模改动的利器,Google 所有的 Dart 代码都是通过此工具进行格式化的。当你的代码推送到审查者面前时,它已经自动应用了 dart_style 进行格式化,所以不再有“新行是不是要放这里”这样的问题。同时,它也适用于大规模的代码重构。

性能指标

如上所述,AngularDart 能从子产品的测试中受益,而又不仅仅局限于测试。Google 非常严格地衡量其应用程序的性能,因此大多数(也可能是所有)产品应用程序都有基准套件。

当 AngularDart 团队引入了一个改动,使 AdWords 的加载速度降低了 1%,在改动上线前 Google 内部就会知道。团队在今年 10 月表示,自 8 月份以来,AngularDart 应用程序的规模缩小了 40%,但速度却提高了 10%。他们谈论的不是像 TodoMVC 这样的小型应用程序,而是真实世界中那些处理关键任务、有数百万用户基础和上兆字节的业务逻辑代码。

旁注:封闭的构建工具

读者可能会好奇:你怎么知道 AngularDart 引入了这个脆弱的 Bug 后,要在巨大的内部存储库中进行哪些测试哪?当然不能手动挑选 74 万个测试,也肯定不会在 Google 内运行所有的测试。答案是一个叫做 Bazel 的东西。

在这种代码规模下,你不可能构写一系列的 shell 脚本来进行编译。首先这样很脆弱,其次它非常非常慢。你需要的是一个封闭的构建工具。

“封闭”这个词与函数中的“pure”非常相似。你的构建步骤不能有副作用(例如临时文件、更改 PATH 路径等等),而且它们必须是确定的(即相同的输入一定得是相同的输出)。在这种情况下,你可以在任何时间,在任何机器上运行构建和测试,以保证输出的一致性。你不需要 make clean。因此,你可以发送编译/测试来构建服务器,并将它们并行化。

Google 花了数年时间开发这个构建工具,而且去年将它作为 Bazel 开源了。

有了这一基础架构,内部测试工具可以自行决定构建/测试的影响范围,并在适当的时候开始运作。

这一切意味着什么哪?

AngularDart 的目标明确,就是在开发大型 Web 应用程序时,达到最佳的生产力、性能和可靠性。本文说的就是最后一部分——可靠性,以及为什么 Google 的重量级应用程序,如 AdWords 和 AdSense,都在使用这个框架。不是团队吹嘘他们的用户,如上所述,拥有庞大的内部用户使得 AngularDart 不太可能引入一些表面上的变化,而这使得框架更加可靠。

如果这一切听起来过于商业化,你可以看看我的非商业化 AngularDart 项目,比如 Automatic Donald Trump (Markov 链) 或 Prime Finder

如果你正在寻找一个隔几月会有一次大改或者重大功能升级的框架,AngularDart 绝对不适合你。即使团队希望以这样的方式构建框架,本文也明确得告诉你,这一点做不到。但我们真诚地相信,一个不那么符合潮流,但格外可靠的框架,还是有其生存空间的。

在我看来,一个开源技术栈是否会有长期维护和支持,最佳的预测方法是看它是否是主要维护人员公司业务的重要组成部分。以 Android、dagger、MySQL 或 git 为例。这就是为什么我很高兴 Dart 终于有了一个首选的 Web 框架(AngularDart)、一个首选的组件库(AngularDart Components)和一个首选的移动框架(Flutter),所有这些都用来构建 Google 关键业务的应用程序。

1 1 收藏 评论

关于作者:飞哥的咖啡

I'm OUT, never IN. 个人主页 · 我的文章 · 50 ·  

可能感兴趣的话题



直接登录
跳到底部
返回顶部