Google的软件工程之道
作者:朱少民 2017-03-21 {{allComments.length}} 2226 深V专栏 前段时间百度MTC的专栏专家朱少民老师联合其他两个业内专家徐文锦、杨晓慧编译了一系列非常有行业价值的《Google的软件工程之道》分享出来给大家。
Ps.文末有惊喜!我们不撒狗粮,只充鸡血!
谷歌的成功有很多原因,包括开明的领导力、优秀的人才、招人高门槛,以及有良好的财务实力可以成功利用快速成长的市场中的早期领导者。但其中一个不可忽视的原因是谷歌不断积累了优秀的软件工程实践,助成功一臂之力。借助许多全球最有才华的软件工程师的积累和智慧的结晶,随着时间的推移而不断提升软件工程实践。本文将比较彻底、全面地分享谷歌公司的软件工程之道,分为三个部分:
软件开发之道:源代码、构建系统、代码评审、测试、缺陷跟踪等
项目管理之道
人员管理之道
第1部分 Google软件开发之道
1. 如何管理源代码?(The Source Repository)
大部分的代码都用统一的源代码库(repository)来管理,允许所有Google的软件工程师访问。同时也有以下几种值得注意的例外情况,如:
a.像大型开源项目Chrome和Android使用了独立的开源代码库
b.一些高价值的或高安全性的代码,则设定了严格的读取权限
但是大多数的程序依然是共享同一个代码库。截止到2015年1月,这个86TB的代码库已经存有10亿多个文件,其中有900多万源代码文件,其代码行数高达20亿,并包含了3500万代码提交(commits/check in)记录,每天还以4万次提交在增长(译者注: 谷歌有超过3万开发人员)。对代码库的写权限是被限定的:只有列出的每个库subtree(译者注:Git 的术语)的所有者(Owner,译者注:本来也可以不翻译)有权批准对subtree的修改。但一般来说,任意一位工程师都可以访问任何代码,可以检出(check out)所需代码并进行构建,也可以在本地对其修改、测试,并且经代码的所有者复审(review)通过后提交(check in)新修改的代码。谷歌的企业文化鼓励工程师跨越项目边界去修复他们认为受到破坏并知道如何修复的任何缺陷。这给了工程师更多授权,并促使达到高质量的基础设施,以更好地满足其使用需求。
几乎所有的开发均发生在代码库的“head”上(译者注:git中的head,即要merge到master之前的分支),而不是在subtree上。这有助于尽早识别出集成问题并最小化所需的合并工作量。同时也使得安全补丁能够更容易和更快的上线。
自动化系统频繁地运行测试,通常是在任何代码有改动而进行传递性依赖项的测试(译者注:就是通常所说的回归测试),虽然这并不总是可行。如果修改没通过测试,系统通常只在几分钟内自动通知作者及其复审者。大多数团队会通过安装引人瞩目的显示或带有彩色光线的雕塑(绿色代表构建成功和所有测试通过,红色表示某些测试失败,黑色表示构建失败) 以非常直观地呈现构建的状态。这种行为有助于将工程师们的注意力集中在“构建处在绿色状态”。大多数规模较大的团队也有一个“构建警察”(这一角色通常由团队成员或是经验更丰富的成员轮流承担),通过与作者合作来快速解决问题或回滚其违规的修改以确保测试在head上的持续通过。即使对于规模非常大的团队,关注持续成功构建,对“head”的开发具有实践价值。
代码所有权。库的每个subtree都可以有一个列出subtree “所有者”用户id的文件。子目录还会从父目录继承所有者,尽管这是选择性地控制。每个subtree的所有者控制其写访问权限(见后面“代码评审”部分中说明)。虽然通常有更多的所有者,但是每个subtree至少需要两个以上,尤其是分布在不同地方的团队。在所有者文件中列出整个团队成员,这是十分常见的。不局限于所有者,Google的任何人均可以对subtree进行修改,但他需要某个所有者的许可。这可以确保每一个改动都被能够理解所做的修改的工程师审核过。
更多的Google源代码存储库详见[17、18、21],关于另一个大公司又是如何处理同样挑战的,可参见[19]。
2. 构建系统(The Build System)
谷歌使用一个被称为Blaze的分布式构建系统,此系统负责软件编译、链接及其测试。它提供了用来构建和测试软件的标准命令,并适用于整个代码库。这些标准命令和高度优化的工具意味着任何Google工程师构建版本和测试软件通常是非常简单和快速的。这种一致性在帮助工程师能够跨越项目边界进行修改起着关键作用。
程序员需要为Blaze编写如何完成版本构建的“BUILD”文件。诸如库、程序和测试这些构建实体,均由高级的声明式构建规范(declarative build specifications,DBS)进行声明,描述具体的每一个实体,如实体的名称、源文件、相关的库文件或所依赖的其它实体。这些DBS是由被称为“构建规则”的声明来组成,每个声明均用来指定如“一个拥有依赖于其他库的一系列源文件构成的C++库” 这样的高层次概念,并取决于构建系统将每项构建规则和一系列的构建步骤映射起来,例如为每个源文件进行编译的步骤、链接的步骤,以及确定使用哪个编译器和编译标志。
在某些情况下,特别是Go程序,因为BUILD文件中的依赖性信息 (通常)是源文件依赖信息的抽象,所以构建文件是可以自动生成 (和更新) 的。尽管如此,这些文件也要被检入到代码库中。这就确保了构建系统通过分析构建文件而不是源文件来快速地确定依赖关系,并以此避免了构建系统和编译器或者用于支撑其他编程语言的分析工具之间的过度耦合。
构建系统使用谷歌的分布式计算设施进行实现。通常每个构建的工作是分布在成百上千台机器中穿插进行的。这使得快速构建超大型程序或并行运行成千上万的测试成为可能。
个人构建步骤必须是“密封”的:只取决于其已声明过的输入。
所有依赖项都必须被正确的声明,这是分布式构建的必然诉求:只有已声明的输入信息会被发送到正在运行构建步骤的机器。因此, 可以依据构建系统来获取真实的依赖关系,即使构建系统的某个编译器被视为一种输入。
个人构建步骤是确定的。因此构建系统可以缓存构建结果。软件工程师可以同步其workspace回到原来的变更号码再进行重新构建,以得到完全相同的二进制版本。此外,这个缓存可以在不同的用户之间安全地共享。(为了使它正常工作,我们必须消除构建所调用的工具中的非确定性,例如通过洗掉所生成的输出文件中的时间戳)。
构建系统是可靠的。构建系统跟踪构建规则本身变更上的依赖性,这样如果某操作导致规则变化时依旧知道如何重新构建目标,即使输入对操作是不可知的,例如只有编译器选项发生改变时。同时,也能够妥善处理构建某部分被中断或在构建中修改源文件等情形:在这种情况下,你只需要重新运行构建命令,但从来不需要运行“make clean”这类命令。
构建结果缓存在“在云中”,还包括中间结果。如果另一个构建请求需要相同的结果,构建系统将自动复用而不是进行重建,即使此请求来自于不同的用户。
快速的增量构建。构建系统一直驻留在内存中,因此可以仅仅针对上次构建之后修改过的文件来进行增量分析来完成新的构建。
Presubmit检查。针对启动代码审查和/或准备提交变更到存储库时,谷歌有工具自动运行一组测试。每个库subtree都可以包含一个配置文件,以确定要运行哪些测试,以及是否在代码评审时进行测试,或在提交之前即刻运行测试,或者都要。测试可以是同步的,例如在发送变更给审查之前运行,和/或在提交变更到代码库中之前运行(有利于快速测试)。测试也可以是异步的,即通过邮件将结果发送到评审讨论线程(thread)。[评审线程正是代码审查活动发生的电子邮件线程,所有线程的信息也会在基于web的代码复查工具中呈现出来。]
3.代码评审(Code review)
谷歌自己开发了优秀的基于Web的代码评审工具,并和邮件系统集成,允许作者发起评审请求、允许评审人员两边对比方式地去浏览代码差异 (带有颜色标注)并能直接在工具上写意见。当代码变更的作者发起代码评审时,评审人员会收到电子邮件通知,并附有链接指向评审工具对应的web页面。当评审人员提交他们的审查意见时,也会发送邮件通知作者。此外,自动化工具可以发送包含自动化测试或静态分析工具的测试结果等相关通知。
所有源代码主库的变更都必须经过除作者外至少一个以上工程师的审查。此外,如果作者不是该修改文件的其中的所有者,那么此项变更还需要至少一个所有者的审查和批准。
在特殊情况下,一个库subtree的所有者可以在通过审查前就能发布(提交) 该库subtree的一项紧急的变更,但评审人员必须留名,并且在变更被审核和批准之前,系统会不断自动催促此变更的发布者和评审人员。在这种情况下,由于变更在通过审核前就已经被执行,任何单项的变更都需要明确的评审意见。
对于一个给定的变更,谷歌通过工具来自动推荐出合适的评审人员。工具通过查看发生变更的代码的所有权和作者、近期评审人员的历史、每个潜在的评论人员的待审查的代码数量等信息来实现此功能的。每个库subtree都至少有一个所有者必须审查该项变更的影响并批准这一变更。但除此之外,作者可以自由选择他们认为合适的评审人员。
代码评审中一个潜在的问题是——如果评审人员相应过慢或过于勉强而不批准变更,这可能会降低开发速度。代码的作者选择评审人员时应该有助于避免此类问题,因此允许工程师绕开那些可能会过度拥有他们的代码的评审人员,或把较为简单的变更发送给经验不丰富的评审人员,而相对更复杂的变更则发送给更有经验的评审人员或同时发给多个评审人员。
每个项目的代码评审讨论会被自动拷贝到指定的项目维护者邮件列表中。任何人都可以对任何变更进行自由评论,无论他们是否被提名为此变更的评审人员,同时也不管这条变更是否已经通过了审核。如果发现一个错误,通常会追踪到引入此项错误的变更和原代码的评审意见,并指出问题在哪里,以便让原始作者和评审人员都了解问题所在。
也可以同时发送代码审查给多个评审人员,在其他评审人员给出意见之前,但只要其中有一个评审人员已批准,就尽快提交这项变更(当然,作者和第一个响应的评审人员中至少有一位是所有者),随后的评审意见将在其后依次加入变更序列中。以此来减少评审的周转时间。
除了主库部分, “实验性”的代码库部分就没有强制要求执行常规的代码评审。然而,在产品线上运行的代码必须存储在主库中,而且鼓励工程师一开始就在主库的管理下开发代码,而不是先在实验库中研发之后再将其移动到主库中。因为在刚写完代码后就进行代码审查是最有效的,而不是彻底完成代码开发工作之后再进行代码评审。实践中,有些工程师甚至会在实验阶段就请求进行代码评审了。
鼓励工程师每次尽量保持小规模的变更,大的变更也最好是分解成一系列的小变更,以便评审人员很容易地一次性地完成评审。这也使作者在每次评审过程中更容易应对重大变化。非常大的变化通常太僵硬,并阻碍评审人员建议的变化。所提倡的保持小型变更的一种方法是——借助代码审核工具给每次变更的规模打上一个文字描述的标签:
对于30~99行的添加/删除/删除等类型的变更,标注为 “medium-size”;
对于300行以上的变更,根据其程度进行标注,如(300~999)为 “large”, (1000 ~1999)为“freakin huge”等等。
(然而,在常见的Google方式中,为了让工作有趣,每年会有几天,如“像海盗一样说话”的那天,会使用风趣少见的描述来代替这些熟悉的描述方式)
4.测试(Testing)
单元测试是Google公司非常提倡和广泛采用的工程实践。产品线上所有的代码都要求进行单元测试,如果新添加的源文件没有进行相应的测试,代码审核工具将会将它们突显出来。代码评审者通常要求:任何增加了新功能的变更都应该添加新的测试来覆盖这项新功能。Mocking框架很受欢迎,在此框架下仍允许对于重量级库函数有依赖的代码进行轻量级单元测试的构建。
集成测试和回归测试也得到了广泛的应用。
正如上文“预提交检验”中提到的,测试自动被视作代码审核和提交流程中的一部分。
Google也有自动化工具来度量测试覆盖率,其结果作为可选层(an optional layer),与源代码浏览器(窗口)集成在一起。
在Google,部署之前的压力测试也是必备礼仪(de rigueur),要求团队通过图表显示在各种输入请求(负载压力)下系统的关键指标,如延迟和错误率。
[ 译者注:欲全面了解Google的软件测试,请参考James Whittaker 等《Google软件测试之道》]
5.缺陷跟踪(Bug tracking)
谷歌使用一个命名为Buganizer的缺陷(Bug)跟踪系统来跟踪问题: Bug、特性请求、客户问题和流程(如产品发布或缺陷清理流程)。Bug按层次化的组件/模块进行分类,每一组件会有一个默认的受让人(assignee,即将bug分配给这人)和邮件抄送列表。当发送变更的源代码供审核,系统会自动提示工程师,将变更与特定问题联系起来。
在Google的团队中定期扫描各个组件中未关闭的Bug是十分常见的(虽然不很普遍),优先关注这些Bug并合理地将它们分配给相应的工程师。有些团队会有一个特定的人员来负责Bug分配(triage,译者注:有些bug处理有争议,需要特定的处理,就像微软早期的“”三国会议),而有些团队则在定期团队会议上进行做Bug分配。Google的很多团队均依据Bug上的标识(译者注:Bug状态标记)来判断此Bug是否已经被分配、每个Bug需要在哪个版本发布前被修复。
6 编程语言(Programming languages)
Google极力鼓励软件工程师去使用官方认可的四种编程语言C++、Java、Python或Go中的一种进行编程。不同的编程语言使用数量达到最低,从而减少代码复用和程序员合作上的障碍。
Google为每种语言制定了编程规范,以确保整个公司的代码编写使用相似的风格、布局、命名约定等。除此之外,公司还有一套代码可读性培训流程。通常这项培训是让在意代码可读性且有经验的工程师来培训其他的工程师,针对特定的编程语言,通过审查实质性的变更或一系列变更,直到评审人员满意并认可作者知道如何编写易读的代码为止,从而培养他们如何写出可读性强、符合惯例的代码。在特定语言增加了任何有意义的、新代码的变更,均需要由一个通过该项语言可读性培训合格的人的批准,方可执行。
除了上述四种编程语言,也有许多专业的领域特定语言(Domain-Specific Languages,DSL)是用于特殊用途的(如BUILD语言是用于指定构建目标及其依赖关系的)。
这些不同的编程语言之间的互操作主要是通过使用协议缓冲(ProtocolBuffers)来实现的。协议缓冲是对结构化数据编码的一种有效的、可扩展的方法,它包括一种用于说明结构化数据的DSL,连同一个可以解析这样的描述并生成C++、Java、Python代码的编译器, 完成这些对象的构造、访问、序列化和反序列化。Google版本的协议缓冲是与Google的RPC库相集成的,允许简单的跨语言RPC实现,并对RPC框架自动处理的请求与响应进行序列化和反序列化。
过程的共性是在巨大的代码库和多样语言下依然能轻松进行软件开发的关键因素:无论什么项目或语言都使用一组相同的命令,来执行所有日常的软件工程任务(如代码检出、编辑、构建、测试、评审、提交代码、缺陷报告等等)。这样一来,不论开发人员正在编辑的代码来自不同的项目、还是用不同的语言编写的,他们都不需要重新学习一种新的开发流程。
7.调试和分析工具(Debugging and Profiling tools)
Google服务器与为正在运行的服务器进行调试的工具库是联系在一起的。一旦服务器崩溃,一个信号处理程序将堆栈跟踪信息自动存储到日志文件中,并保存其core文件。如果由于堆内存耗尽导致服务器崩溃的,服务器将保存一个堆对象的样本子集所在的那些站点的堆栈跟踪信息。也提供了Web的调试接口,允许检查传入和传出的RPC(包括时间、错误率、速率限制等)、改变命令行标志值(例如为特定模块增加日志冗长度)、资源消耗、分析等等。
这些工具大大增加整体调试的容易度,因此也很少启动像gdb那样的传统调试器。
8.发布工程(Release engineering)
在Google,只有很少的几个团队有专职的的发布工程师,但大多数团队,其发布工程的工作是由非专职的软件工程师来完成的。
大多数软件发布频繁;每周或每两周发布一个版本是普遍的,甚至有些团队做到每日发布。可以这样做,是因为大部分常规的版本发布实现了自动化。频繁发布新版本有助于保持工程师的士气(如果几个月或几年才发布一个新版本,那么他们是很难保持兴奋的),并通过更快的迭代来提高了整体的开发效率,而且在给定的时间内有更多的机会获得用户的反馈和对反馈做出响应。
版本的发布通常开始于一个崭新的workspace,通过同步最新的“绿色”版本(即最后一个通过所有自动测试的版本)的变更号并创建一个版本分支。发布工程师可以选择额外的变化作为“择优挑选(cherry-picked)”,即将主分支合并到新版本分支。然后软件会从头重新构建并执行响应的测试。如果测试失败,则会添加新的变更来修复失败,这“择优选取”的变更会再次合并到新版本分支,再重新构建和重新测试。直到测试全部通过后,将构建好的可执行文件和数据文件一起打包。所有这些步骤都是自动执行的,因此发布工程师只需要运行一些简单的命令,甚至只需要在菜单驱动的UI上选择相关项,选择哪些变更(如果有)作为“择优挑选”。
一旦候选版本已经完成打包,则通常部署到staging(译者注Beta 服务器或准产品线环境)服务器上,由少量用户(有时只是开发团队)进一步完成集成测试。
一个有用的技术,涉及到从产品线上发送一份请求的copy(或子集)到staging服务器,同时也发送同样的请求到当前真正的产品服务器以进行实际的处理。从staging服务器获得的响应已被丢弃,而从真实的产品服务器所获得的响应会送回给用户。这有助于确保在新版本真正上线之任何有可能会导致严重后果的问题(如服务器崩溃)能被检测出来。
下一步通常是推出一个或多个canary服务器,用于处理产品线流量的一个子集(subset of the live production traffic)。与staging服务器不同,它是处理和应对真实用户的。
最后发布就可以推广到所有数据中心的服务器上了。一些非常高流量、高可靠性的服务是需要几天时间、逐步推出,以减少由于未被之前的检测步骤所发现的、新引入的缺陷而造成任何停机/服务中断(outages)的影响。
更多的Google发布工程信息,请参阅SRE一书的第8章 [7],或参考 [15]。
9 产品启动审批(Launch approval)
发布任何用户可见的变更或发布重大设计变更时,需要通过实施这项变更的核心工程团队以外的成员的审批。特别是审批(通常是受到详细审核)必须确保代码的编写符合法律要求、隐私需求、安全需求、可靠性需求(例如有合适的自动监测器来检测服务器停机,并自动通知相应的工程师)、业务需求等。
验收过程的设定也是为了确保在发布重大新产品或者新特性时能通知到公司内部相关的人。
Google有一个内部使用的验收审核工具,用于跟踪所需的评审及批准,确保符合每个产品定义的启动流程。这个工具很容易操作、并且可定制,因此不同的产品或产品领域可以按不同的要求进行审核和批准。
关于验收审批的更多信息,请参阅SRE一书的第27章[7]。
10 事后分析报告(Post-mortems)
每当生产系统有重大的outage或类似的事故发生时,所涉及到的人员都被要求写一份事后分析报告。该文档描述了这一事故,包括标题、摘要、影响、时间表、事故根源、做对/错了什么(what worked/what didn’t)以及行动计划。分析重点在于问题本身以及未来该如何避免再次发生,而不是谁出的问题或该问题由谁负责。
影响部分试图量化意外事件带来的影响,如停机多少时间、丢失了多少查询(或失败的RPC等)和损失了多少营业额等。
时间表部分可以完整给出从停机开始到诊断、纠正问题这样一个过程。
做对/错了什么:总结教训——哪些做法有助于快速检测并解决问题、哪些做错了、采取了哪些具体行动(最好是报成Bug再分配给特定的人)以此在将来减少发生此类问题的可能性和/或类似问题的严重性。
关于Google事后调查文化的更多信息,请参阅第SRE书[7]的15章。
11 频繁重写(Frequent rewrites)
Google大多数的软件每隔几年就会被重写一次。
这么做的成本之高,往往会是令人惊讶。事实上它也的确消耗了Google很大一部分的资源。但它仍然带来一些非常良好的收益,正是这些优点对Google敏捷性和长期成功起着关键的作用。近几年来,随着软件环境和其他技术的变化,产品的快速变化成为了一项典型的需求,随着技术或市场的变化也进一步影响用户的需求、渴望和期望。多年前发布的软件是围绕一组旧的设计要求来设计的,通常不适合当前的需求。此外,它通常会积累了大量的复杂性。重写代码削减掉所有不必要的复杂性,例如过去强调的需求在今天不再那么重要了。另外,重写代码也是将知识和归属感传递给新的团队成员的一种方式。这种归属感是生产力的关键——工程师们自然而然地会把更多的精力投入到开发并修复那些他们觉得是属于自己代码中的缺陷。频繁的重写也鼓励不同项目之间的工程师流动,以推进思想的交融。频繁的重写也有助于确保代码总是使用最新的技术和方法来写的。
作者介绍
Fergus Henderson作为软件工程师已在Google工作了10年以上。在1979年,他还是一个孩子的时候就开始编程,并继续从事编程语言设计和实现上的学术研究。在墨尔本大学,他和他的博士导师共同建立了一个研究小组,开发了编程语言Mercury。他是8个国际会议的程序委员会委员,并发布了超过50万行的开源代码。他是Usenet新闻组comp.std.c++的前主席,并且是ISO C和C ++委员会官方认可的“技术专家”。他拥有超过15年的商业软件行业经验。在Google,他是Blaze(在整个Google上还在使用的构建工具)的原创开发人员之一,并致力于具有语音识别、语音操作(在Siri!之前)和语音合成功能的服务器端软件开发。他目前管理Google的文字转语音工程团队,但仍然编写和审查大量的代码。他写的软件安装在超过十亿个设备上,每天使用也超过十亿次。
之所以把全文分成三部分介绍,让大家阅读轻松些,一次不要阅读太多,而且能细读,收获更大一些。而且这三位老师认真对待翻译,把每一个疑点搞清楚再翻译,提供高质量的精品,有心的同学要关注老师的微信公众号哦,定期会分享很多值得学习的内容:
如果你看的意犹未尽,如果你想随时随地充实自己,请扫描以下二维码,关注我们的公众账号,可以获取更多技术类干货,还有精彩活动与你分享~