最近在读google的软件工程实践,翻译如下,共勉。
原文如链接 https://arxiv.org/pdf/1702.01715.pdf
大致分为了以下5个方面进行论述:
1.Google软件工程之道(一)综述
2.Google软件工程之道(二)软件开发
3.Google软件工程之道(三)项目管理
4.Google软件工程之道(四)员工管理
5.Google软件工程之道(五)总结
摘要
本文中,我们描述和整理Google的核心软件工程实践经验。
作者简介
Fergus Henderson已在Google从事十余年的软件工程开发工作。在1979年,还在孩童时期的他就开始学习程序设计,后来在学校研究计算机程序语言的设计和实现。他和他的博士生导师,在墨尔本大学共同创建了一个研究小组,该小组开发了程序设计语言Mercury。他是八个国际程序委员会的委员,目前已经开放了超过50万行的开源代码。他曾是Usenet newsgroup comp.std.c++的早期版主,也是ISO C和C++的标准委员会的公认专家。他拥有超过15年的商用软件行业经验。在Google,他是Blaze的早期开发者之一,目前Bazel作为构建工具目前已经在Google广泛使用。他曾从事语音识别、voice action(比Siri还早的语音助手)和语音合成等产品的服务端软件开发。目前,他负责管理Google的语音合成工程团队,但他依然奋斗在一线,开发和审阅大量代码。他写的代码已经安装在数十亿计的设备上,每天使用超过数十亿次。
Google已经成为一家非常成功的公司。除了Google搜索和AdWords之外,Google还发布了一系列卓越的产品,如Google地图、Google新闻、Google翻译、Google语音识别、Chrome浏览器和Android。Google还极大的增强并拓展了通过收购兼并的小公司的产品(例如Youtube),并且为一些列开源软件和项目做出了突出的贡献。Google还有一些尚在研发还未发布的非常酷炫的产品,如自动驾驶。
Google如此的成功有很多原因,包括开明的领导、杰出的人才、高标准的招聘门槛和其在非常快速增长市场中通过领先地位积累的财务实力。但其中还有一个原因是Google积累了卓越的软件工程实践经验,这同样助力了Google的巨大成功。这些实践由全世界最杰出最才华横溢的工程师积累、提炼,并随着时间不断演进完善。现在我们想向大家分享我们软件工程实践经验,也分享一路走来从错误中汲取的教训。
本篇文章的目的是整理和简要描述Google的核心软件工程实践经验。这样,其他的组织或者个人可以比较、对比他们自己的软件工程实践经验,并考虑是否应用其中一些工程实践经验到他们开发中。
许多作者( [9], [10], [11])都写书或者文章分析Google的成功和历史,但其中的大多数都在围绕着商业、管理和文化分析,仅有一小部分作者( [1, 2, 3, 4, 5, 6, 7, 13, 14, 16, 21])研究软件工程对Google成功的贡献,在这一小部分之中,很多又仅在单个层面上分析。目前还没有关于Google软件工程实践的既全面又概要的性的综述,这就是本文的目的。
2.1 代码仓库
在Google,绝大多数的代码都存储在一个代码仓库中,Google所有的工程师都可以访问该仓库。其中有个别例外,典型的如两个大型的开源工程Chrome和Android,这两个项目使用单独的开源的代码仓库,并且对于其中高价值和高安全级别的代码,设定了严格的读取权限。尽管如此,Google的大部分工程还是共享一个代码仓库。截至2015年一月,该代码仓库总共86TB,包含10亿个文件,其中900万个为源代码文件,包含20亿行源代码,共计3500万次代码提交,平均每个工作日产生4万个提交[18]。该代码仓库的写权限是受控的,仅该子树的所有者才可以批准在该子树的代码上修改。但一般来讲,在Google的任何工程师可以都可以访问、下载、编译该代码仓库中的任何一段代码,并可以做本地的修改、测试,并发送相关的修改到代码所有者,如果代码所有者同意修改,则该修改就可以准入合并。从公司文化角度讲,公司鼓励工程师解决那些他们知道如何解决的代码仓库中的任何有问题的代码,而不要在意项目之间的鸿沟,这鼓励了工程师在使用中修改相关代码以服务他们的需求,从而成就了更高质量的基础代码。
所有的开发代码修改都发生在代码仓库的头部,而不是分支。这有助于在早期发现代码的集成问题,并减少代码合并工作量。同时,它让安全代码的提交也更容易、更快。
自动化测试系统会频繁测试,一般测试发生在代码修改后,测试依赖修改代码的模块,但是该方法不一定对所有情况可行。该系统一般会在几分钟内完成测试,并自动通知代码修改者和代码审阅者哪些测试由于修改代码失败了。大多数团队会将当前的构建进度明显的标记出来,或使用更好的显示器,或使用不同的颜色标记(绿色表示构建成功并且所有的测试都通过了,红色表示有的测试失败了,黑色表示构建过程出了问题),这样可以帮助工程师集中注意力使所有的构建变绿(通过)。大多数大团队还有”构建警察“,它通过和作者协作修改或撤销有问题的修改,从而确保所有的测试继续通过(构建警察通常是由小组成员或者小组中的资深成员轮流担当)。关注构建过程的持续顺利通过,使得仅在代码仓库头部开发成为可能,即使在非常大的团队开发中也同样奏效。
代码所有权。代码仓库中的每个子树中均有一个文件代码记录所有者,其中列举了该子树所有者的用户ID。子文件夹从父目录中继承所有权限,但是这样的简单继承也可以被阻止。如上文所述,子树的所有者控制该子树的写权限,每个子树至少需要两个所有者,但是实际中一般子树会拥有更多的所有者,尤其是地理上比较分散的团队。一种常见的做法是在所有者记录文件中,包含团队的所有成员。除了代码所有者,Google所有的人都可以在子树上进行修改,但是需要得到所有者的同意,这确保了每个修改都会由负责该子树代码的并理解该代码的所有者的审阅。
如需了解更多的关于Google代码仓库的知识,请参考[17, 18, 21], 如需了解其他大公司如何解决该问题,请参考[19]。
2.2 构建系统
Google使用分布式的构建系统Blaze,Blaze负责编译、链接和测试。Blaze在整个代码仓库上提供了标准的构建和测试命令,标准化的命令和其非常高效的实现意味着对于Google的任意一个工程师,构建和测试代码仓库中的任何软件都是非常容易且快速的。这种构建标准的一致性是使跨项目的代码修改成为可能的关键性因素。
在Blaze中,工程师通过写"Build"文件告诉Blaze如何去构建他们的软件。构建的实体例如链接库、程序、测试都通过高层次的声明性的构建规范声明,指定每一个实体、其名字、源代码文件、链接库和其需要依赖的其他实体。构建的规范由一系列"build rule"组成,其中的每一个都详细指定高层次的概念,例如“这里有一个C++库,它依赖哪些源代码文件,依赖哪些其他的库”。构建系统会将每个"build rule"映射为一系列的构建动作,例如编译源代码的动作、链接的动作、决定使用哪个编译器及编译时的参数。
在一些情况下,典型的如Go语言,"Build"文件可以自动生成(更新),因为可以通过Go语言源代码的依赖信息抽象出其Build文件,虽然如此,自动生成的这些Build文件仍然会提交到代码仓库。这确保了构建系统可以迅速的由Build文件而非源代码确定依赖关系,并且避免了其支持的不同语言和构建系统、编译器和分析工具之间的过度耦合。
该构建系统通过Google分布式计算的基础设施得以实现。一般来说,每一个构建工作都分布在数百台甚至上千台机器上完成。在上千台机器上进行分布式编译和测试,使快速构建巨型程序成为可能。
单一的构建动作必须是“封闭”的,即它仅依赖Build文件中声明的输入。分布式构建的自然结果是要强制所有依赖必须被显示正确声明,仅有声明的输入才会被发送到相应的执行构建动作的机器上。这样,才可以依赖构建系统得出正确的依赖关系。在该构建系统中,编译器器也被当作输入。
单一的构建动作必须是确定的。这样,构建系统才可以缓存构建的结果。软件工程师可以同步他们的工作区到一个旧的修改编号,并可以重新构建得到相同的二进制文件。此外,不同的用户之间还可以安全的共享缓存(为了使该想法正确工作,我们必须消除由编译工具引起的不确定性,例如去掉输出文件中的时间戳信息)。
构建系统必须是可信的。构建系统自动跟踪和代码修改相关的“build rule”,并且知道生成的目标是否发生变化,即使所有的输入并没有变化(例如当编译选项发生变化时)。构建系统也需要正确处理构建中断时和在构建中修改源代码的情况,在这些情况下,你只需要重新运行build命令即可,而并不需要去执行类似"make clean"的命令。
构建结果在云端缓存,包括中间结果。如果一个构建需求需要相同的结果,构建系统将自动重用相同的结果而非重新构建,即使构建的需求是来自不同的工程师。
增量式的编译非常迅速。构建系统会驻在内存,所以它可以增量式地仅分析相比上次构建发生修改的文件。
提交前检查。当向代码仓库提交一个修改代码或者一个代码审阅时,Google有一系列的自动化工具做检查。代码仓库中的每一个子树都有一个配置文件,其中决定了何时作测试,在代码审阅还是代码提交时时作测试(或者都做)。这些测试可以是同步的,即在向代码仓库发送修改或者提交之前(优点是快速测试);也可以是异步的,测试的结果会通过邮件在代码审阅会话的讨论中通知)[代码审阅会话是和代码审阅相关的邮件会话,在该会话中的所有信息都也可以通过一个浏览器版的代码审阅工具展示]。
2.3 代码审阅
Google开发了基于web的强大的审阅工具,并与邮件集成。其允许代码作者发起审阅请求,允许审阅者通过边到边的对比视图查看修改(用不同的颜色标记),同时还可以评论。当修改代码的作者发起一个审阅请求时,审阅者会收到邮件通知,该邮件中有该修改在web审阅工具中的链接。当审阅者提交他们的评论时,也会有邮件通知。除此之外,自动化的工具也会发送邮件通知,如自动化的测试的结果、请求静态分析工具等。
所有代码仓库中的修改至少要有一个非修改者的工程师审阅。除此之外,如果修改者不是相关改动文件子树的所有者,那么至少要有一个修改文件子树的所有者审阅和批准修改。
在特殊情况下,子树的所有者可以在代码审阅完成之前,合并(提交)非常紧急的修改,但仍需提名一个审阅者,不然代码修改者就会持续收到自动通知,直至相应的修改被审阅和批准。在该情况下,为解决后来审阅者评论的必要修改都必须在另一个单独的修改中,因为原始的改动已经合并了。
对于修改,Google的工具会自动推荐代码审阅者。该功能基于查找修改代码的所有者和原始作者,历史上最近的审阅者,也会考虑潜在的代码审阅者目前还未完成的审阅的代码数量。至少要有一个代码子树的所有者审阅和批准修改,除此之外,代码修改者可以任意指定他们认为合适的代码审阅者。
代码审阅一个潜在的问题是审阅者响应太慢,或者审阅者太过冗余,这可能会减缓开发速度。事实上,代码修改者可以通过选择审阅者避免该问题,如尽量不要选择对自己代码控制欲过强的审阅者,对于简单的改动选择较少的审阅者,对于复杂改动选择更多更资深的审阅者。
每个项目的代码审阅的评论会自动复制到项目指定维护者的邮件列表。所有的人都可以自由的在任意修改上评论,而不用在意自己有没有被提名为审阅者,且在修改提交前后均可以评论。如果发现了bug,通常是追踪导致该bug的修改,并在该修改上进行评论,指出该修改的错误,这样原始代码修改者和审阅者都会了解到该bug。
我们也可以在提名了多个代码审阅者,并且其中一个审阅者(当然是作者自己或者第一个响应的代码所有者)已经批准,其他审阅者还未评论之前继续提交合并修改。随后,当其他审阅者有评论时再在后续的其他改动中处理。这样可以减少代码审阅的交互时间。
代码仓库中除了主体分支之外,还有实验分支,实验分支不强制进行正常的代码审阅。但是,产品线上的代码必须是代码仓库中主体分支的代码,并且我们强烈鼓励工程师在主体分支上进行开发,而非先在实验分支上开发随后移动到主体分支,因为在开发中进行代码审阅比在开发完成后进行要更为高效。实际中,即使在实验分支工程师依然频繁使用代码审阅。
鼓励工程师使用尽可能小的修改,鼓励将大的修改拆分为一系列小的修改,这样审阅者可以容易的完成一次审阅。小修改也使修改者在回应审阅者的建议时更加容易,巨大的改动通常更难更抗拒审阅者的修改建议。鼓励小修改的一种方式是,代码审阅工具会根据提交修改量分类标记该提交,30-99行的增/删/移除会被标记为"medium-size"(中型修改),超过300行以上会被打上不太友好的标记,例如"large"(大型修改,300-999),“freakin huge” (他妈的修改,1000-1999)等。(而且,在更为Google的方式中,为了保持趣味性,每年隔上一些天就会为这些标签换一个更有趣的描述,如talk-like-a private day 。
2.4 测试
Google强烈推荐并广泛使用单元测试。代码审阅工具会突出显示新增的却没有相应的测试的源代码文件。代码审阅者会要求对于修改中新增的功能添加相应的覆盖新功能的测试。Mocking framework(该框架允许构建轻量级的单元测试,即使针对有复杂依赖库的代码)非常流行。
Google也广泛使用集成测试和回退测试。
如上文所提到的预提交检查,强制执行自动化测试是进行代码提交和审阅的一部分。
Google也开发了自动化评估测试覆盖度的工具,该工具的分析结果可以作为源代码浏览时的可选的一层。
负载测试先于上线是Google的一条规范准则。团队应该制作图表展示关键性指标,特别是延迟和错误率随着请求数量的变化情况。
2.5 Bug跟踪
Google使用bug跟踪系统Buganizer跟踪问题,包括bugs,请求属性,用户问题和过程(如发布或清理工作量)。Bugs会被分类和层次组件化,每个组件都会有默认的委托人和默认邮件CC列表。当发送一个源代码请求审阅时,工程师可以立即关联到相应代码修改的问题编号。
通常Google的团队会定期查看在他们组建中处于"open"状态的问题,设定问题的优先级,并将问题分配给恰当的工程师。一些团队有特定测试人员负责bug分类,另一些在他们的日常会议中处理bug分类。在Google,许多团队都在bug上打标签,标明已分类的bug、未来哪次发布会修复哪些bug。
2.6 程序设计语言
Google强烈鼓励工程师使用正式批准的5种程序设计语言C++、Java、Python、 Go或JavaScript之一进行编程。减少编程语言的种类可以减少代码复用和代码合作上的障碍。
对于每种程序设计语言,Google都有其编码风格规范,鼓励公司所有的代码都有相近的风格、布局、命名规范等。此外,Google还有全公司范围的代码可读性培训,关心代码可读性资深的工程师会培训其他工程师使用特定的一种语言如何去写可读性高、符合惯用模式的代码。培训通过审阅修改者大量的修改或一系列的修改,直至审阅者满意、修改者知道如何去写高可读性的代码方可。对于在某个语言中新增的重大代码,都必须经过一个通过该代码可读性培训的工程师的批准。
除了以上5种语言之外,也会使用有特殊作用的领域专用语言(例如,用于指定构建目标和构建依赖的构建专用语言)。
不同语言之间的互操作主要使用Protocol Buffers。Protocol Buffers是一种可扩展的结构化编码数据的方式。它包括领域特定语言定义数据结构和一个编译器,输入定义好的数据结构描述,自动生成该数据结构对应C++, Java, Python对象代码,代码中提供构造、访问、序列化、反序列化这些对象的功能。Google的Protocol Buffers和Google的RPC库深度集成产生了一个简单的跨语言的RPCs(gRPC), 请求和响应的序列化和反序列化操作都由RPC框架自动处理。
过程的通用性是使开发容易的关键性因素,即使在由庞大的代码库和多种程序语言的情况也是如此。在Google仅有一个简单的命令集执行日常软件开发的所有任务(如更新、编辑、构建、测试、审阅、提交、bug报告等),该命令也适用任何项目和语言。开发者无需因为他们刚好要修改的代码是另一种语言或者代码而学习新的开发流程。
2.7 调试和分析工具
Google服务链接到提供一系列调试运行时服务的工具库。在服务崩溃的情况下,一个信号处理器会自动把栈调用写到日志文件,并且会存储core文件。如果崩溃是由于用尽了堆内存,服务会采样记录活动堆的分配点的栈调用信息。并且还有调试的web接口,其允许检查进入和外出的RPCs(包括时间、错误率、速度限制等)、修改命令行参数(如增加特定模块的日志级别)、资源消耗、分析等。这些工具极大的提高了整体调试的容易程度,甚至很少需要去启动一个传统调试工具如GDB。
2.8 发布工程
在一些团队需要全职投入的发布工程师,但在Google的大多数团队,发布工作由普通的软件工程师完成。
对大多软件来说,会频繁进行发布。一周一次或者两周一次的发布是很常见的目标,有的团队甚至每天都有发布。自动化绝大多数普通的发布任务使频繁的发布成为可能。频繁的发布有助于保持程序员的积极性(不然,在以月计甚至以年计的发布周期中很难保持兴奋),并提升整体迭代的速度,在给定的时间内也就增加反馈和响应反馈的机会。
通常发布在一个全新的工作区开始,通过同步最新通过构建(最新的通过所有自动化测试的修改)的修改ID,并且制作一个发布分支。发布工程师可以选择其他的改动进行"cherry-picked"操作,例如从主分支合并到发布分支。随后,软件会从头开始构建和测试。如果有失败的测试,需要相应的修改去修复这些失败的测试,这些修改也要"cherry-picked"到发布分支。之后,会重新进行构建和测试。当所有的测试都通过后,构建的可执行程序和数据会被打包。所有的这些过程全部都是自动化的,发布工程师只需要运行几个简单的命令,甚至仅需要在菜单驱动的界面上选择选项和必要的cherry pick的改动即可。
一旦打包好候选的构建,通常会先加载到"staging"(临时)服务器进行进一步小规模用户(有时仅是部署团队)集成测试。
一项有用的技术是除了向"staging"服务发送产品线流量的复制(或者一部分)请求,还会将其发送到当前产品线服务进行实际处理。来自staging服务的响应将会被丢弃掉,而来自目前产品线服务的响应会返回给用户。这有助于确保在投入真实产线之前发现严重问题(如服务崩溃)。
下一步通常是部署一个或者多个”canary“(金丝雀)服务,该服务会处理真实产品线的一部分流量。和"staging“服务不同,这里既会处理也会响应真实用户。
最后,该发布会在所有数据中心全部部署。对于高请求量、高可靠性的服务,会在几天时间内逐渐部署,这有助于减少由于之前步骤没有发现的bug而引起的故障的影响。
想要了解更多的关于Google发布工程的信息,请参考SRE[7]的第8章,也可以参考[15].
2.9 发行许可
对于任何用户可见的修改或者巨大的设计修改的发行,需要得到开发该修改的核心开发团队之外的许多人的批准。特别是需要获得批准(需要更为详细的审阅)去保证编译代码符合法律要求、隐私要求、安全要求、可靠性要求(如有合适的自动监测机制,检测服务故障,并自动通知合适的开发者)、商业要求等。
发行过程也会确保当有重大产品或者特性发行时,会通知公司内相关的人。
Google内部都有发行时的许可工具,它可以跟踪各个产品必要的审阅和许可,以确保该产品的发行和规定的发行过程一致。这个工具很容易定制,因此不同的产品或者产品线都可以根据需要定制不同的审阅和许可。
想要了解更多的关于发行过程的信息,请参考SRE第27章。
2.10 故障分析
无论何时当产线系统发生严重故障,或者与之相近的错误时,相关的员工需要写一个事故分析文档。该文档中描述事故的标题、总结、影响、时间线、事故的根本原因、哪些方法有效哪些无效、行动项目。该文档聚焦在问题本身和以后如何避免相同问题,而不是人或者问责。影响部分要尽量量化事故的影响,包括故障持续时间、丢失的查询数量(或者失败的RPC等)和经济损失。时间线部分要给出导致故障的各个事件和分析、修复故障步骤的时间线。哪些方法有效哪些无效部分要描述汲取的教训——什么实践有助于快速发现和解决该问题,什么是错的,可以采取哪些具体的行动(最好是类似bug分配给特定人)在以后降低类似问题的概率或者严重程度。
想要了解更多的关于事故分析的信息,请参考SRE第15章[7]。
2.11 频繁重写
Google的大多数软件每隔几年就会重写。
表面上看起来这样代价非常高,实际上,它也确实消耗了Google的一大部分资源。但是,它同样有重大的意义,是Google保持敏捷和长期成功的关键性因素。在几年内,通常产品的需求会发生重大的改变,软件开发环境及其周围相关的技术也会改变,并且技术或者市场上的改变会影响用户需求、渴望和期望。几年以上的老软件是基于旧的需求集合开发的,通常不是根据当前需求的最优设计。而且,通常也积累了很多复杂性。重写代码避免了为了解决不再重要的需求引入的累积复杂性。除此之外,重写代码还是一种迁移学习知识的方式,也能让新的团队成员找到归属感。归属感对生产力非常重要:工程师天然的会在他们感觉是“自己的”代码上投入更多的精力开发和修复问题。频繁重写也鼓励了在不同项目间流动,这有助于鼓励思想的交叉碰撞。频繁重写也有助于确保代码是使用现代的技术和方法论开发的。