如何高效地协作开发:一些 Google 的实践

这是我一年前发在 TapTap 内部 Confluence 的一篇文章,经过一些修改后公开出来,希望对更多人有价值。同时也想顺便打下招聘广告(见文末)。

Google 的很多软件工程实践都在对外发布的各种 Tech Talk、CppCon 的演讲以及多本已出版的书里提到过(比如 Software Engineering at Google、Site Reliability Engineering 等),所以这篇文章的内容并不算新鲜事,只是贡献一些个人视角。另外我在 Google 工作已经是 10 多年前的事,现在可能已经变化很大,但我认为 2000~2010 年的时候是 Google 最有创造力、最高效、对人才的吸引力也最强的时候。

一点背景

有时言必谈前公司的人会有点让人讨厌,不过无论是之前创业,还是现在,讨论起工程方面的事情都很难避免提起 Google 是怎么做。Google 和所有公司一样,并不是所有方面都做得很好,比如产品能力以及饱受诟病的客户服务。但就是因为在 engineering 方面领先大部分公司很多,所以削弱了其他方面的弱点带来的影响。Google 取得成功的大部分产品基本都是在技术实现上大幅领先同时代的产品,从而实现用户体验上的领先,早期产品中最有代表性的是 Search、Maps 和 GMail。Google 的 engineering culture 也对包括 Facebook 等在内的大量硅谷公司以及国内包括字节这样的公司产生了深远影响。

我在 Google 的三年是在一个叫 Google Web Server(简称 GWS)的团队。这个项目可以说是 Google 历史最悠久的项目,从 Google 存在开始就有 GWS,到现在 20 多年,Google 的 HTTP header 里 server 还是 GWS,应该还是同一个项目、同一个 code base 和 binary。

cli gws
GWS in Google's HTTP header

一开始的时候 Google Search 就是 GWS。后来从它里面拆出了一部分放到前面承担类似 SLB 的角色,叫 GFE(Google FrontendEnd);又把实现单纯搜索的部分拆了出来叫做 Superroot。GWS 更多地成为了一个实现搜索相关的整体业务逻辑的服务,它后面有 15~20 个后端服务,除了 Superroot 外,还有广告、拼写检查、搜索词修正、query rewrite、用户偏好等等。经过多年的演变,GWS 的开发语言也从 Python 变成了 C,再变成 C++,后来又为了方便快速做试验内嵌了 Python 解释器。在整个过程中,GWS 从来没被真正意义上重写过,因为 Google 一直都有大量的业务需求等着实现,不可能有停下来重写或者重构的机会。所有大的改变都必须以渐进的方式来实现,包括换语言。尽管只有 Google 一家公司在用,但是 2010 年的时候 GWS 就支撑了全球 13% 的活跃网站,是排在 Apache、Nginx、IIS 之后的第四大 web server,因为 Google host 的网站很多都是 GWS 来 serve 的,包括自定义域名的 custom search 以及 enterprise search。

几乎所有在搜索结果页用户可见的改动都会需要改 GWS 的代码。这就涉及到了很多其他项目和团队,除了和搜索有紧密关系的广告外,还有地图、新闻、财经,甚至小到汇率转换、计算器。Google 的搜索框不仅是狭义的搜索,其实功能是很多的。这就导致项目非常复杂,改动也非常频繁。GWS 的二进制文件编译出来有 1G 多,在当年已经超过了 gcc 和 gdb 的上限,需要使用内部改过的版本。项目每周发布一个新版,平均有大几百个 changelist。Changelist 是 Perforce 的名词,相当于是 GitHub 的 pull request 或者 GitLab 的 merge request,是一次 code review 的单位,大小通常是多个 commit。

这样一个项目听起来似乎需要一个庞大的团队来维护,而且很容易成为瓶颈。我 2007 年加入的时候,团队有不到 20 人。2010 年我离开的时候有 40 人左右(负责的范围也变大了),分布在 Mountain View、芝加哥、匹兹堡、纽约。除了 GWS 外,这个团队还维护着一些服务端的基础组件,比如我们开发维护的一个项目是 Google 大部分 C++ 服务的基础,提供了标准的健康检查、feature flag、监控变量管理、实验框架等功能。团队成员还有时间发起一些 20% project,比如我和另外两位同事合作开发了一个 serve Google 所有静态文件的新服务,把这部分职责从 GWS 分离出来。听起来这么多的事情由那么小的团队来做是不可能的。秘诀在于 GWS 每周大几百个 changelist 中大部分并不是这个团队里的人写的代码。

GWS team 不负责实现其他产品在搜索里的需求,但是会花大量时间 review 其他产品的工程师提交到 GWS 的改动。由于每周的改动量巨大,很多人会花一半以上的工作时间在 code review 上。GWS team 自己也做开发,但是责任是不同的,日常开发大概有几类

  • 为了提高 GWS 的性能和稳定性,或者为了团队本身的开发效率做的重构和改进
  • 为了让其他团队能更容易地修改 GWS 而开发的模块化功能,比如 OneBox、ManyBox、Universal Search
  • 为搜索和广告的整体业务需求增加的功能,比如试验框架
  • 上一段所说的输出给其他项目的基础组件以及从 GWS 衍生出的新项目

因为这是我的第一份全职工作,其实在 Google 期间并没有觉得有多好,以为大公司都是这样的。但是离开之后才觉得,能有这段各团队之间能顺滑地高效协作的工作经历是很幸运的。这样的工作模式在大部分其他公司很难完全复制,因为它需要一些很强的基础设施做支持。以我们目前的工作模式,很难想象能达到同样的吞吐量,可能也很难复制同样的模式。但是分析一下别人做得好的地方还是能给我们提供一些方向上的参考,做一些努力能提高的空间还是很大的。下面我就介绍一下支撑 Google 团队间高效协作最重要的几个方面。

代码管理和安全

Google 的每个工程师都可以访问全公司 99.9% 的代码,这是决定 Google 的工作方式最根本的条件。剩下只有少数人能访问的那部分叫 HIP (High-value Intellectual Property),主要是防 SPAM 的逻辑。这部分代码如果泄露了,很快就会被恶意网站利用使得搜索质量显著下降,并且没有可以迅速补救的办法。其他所有项目的所有代码都是对所有工程师开放的。

Google 全公司共用一个代码库,叫做 google3,用 Perforce 做版本管理,但是自己在 Perforce 的命令行客户端 p4 之外包了一个工具叫 g4(为了描述方便,以下 Git 和 Perforce 里 submit、merge 等名词区别就不纠结了,从上下文意思应该清楚)。每次一个工程师在自己本地创建一个 workspace 的时候,g4 会用 OverlayFS 从 NFS 上把最新版本的整个 google3 映射到本地。从工程师的视角,整个 Google 的代码树就是自己电脑上一个只读的大目录。当他 checkout 某个路径进行编辑的时候,g4 会把对应的子目录实际复制到本地,这样在他的视角那个目录就变成可读写的了。所以任何一个工程师都可以编译、测试、修改公司的任何一个项目。

很多人都会问,这样代码不会泄露吗?在那么大的公司可以肯定一定是会泄露的。2010 年前后有很多 Google 中国的人去百度,人民搜索(云壤)、盘古搜索也都是 Google 的人出来做的,要说没有知识产权外泄谁都不信。但是对公司而言,让自己跑得更快远比让竞争对手跑得慢一点更重要。所以大部分情况下保密措施应该是以不伤害效率为前提的。对用户数据的保密除外,但是保护用户数据的措施通常不会影响到大部分人的工作效率。

当时内部邮件列表每个人都可以建,项目管理工具的项目每个人都可以建,工程师可以看任何一个数据中心的任何机器上任何服务的日志,甚至可以动态修改它的 feature flag。理论上这些都可以被滥用或者误用,但是滥用往往缺乏动机,误用可以从设计上避免。一个大原则是风险可控或可逆的事情默认是没有流程的,只有实际发生了问题,证明必要时才会靠引入流程来解决。有了流程就需要有人审批有人执行,如果它解决的问题不常发生、有其他方案或者产生的危害不如流程带来的成本,那么设立流程就是不理性的。

共用一个开放的 repo 目的是让每个工程师都能访问、修改、运行公司的任何项目。要做到这一点,还需要每个项目都使用同样的基本工具。

构建工具

Google 使用的构建工具叫 Blaze,支持 Google 当时允许使用的四种语言:C++、Java、Python、JavaScript(Closure)。后来 Blaze 开源了,改名叫 Bazel,由社区增加了对更多语言的支持。

无论哪个项目,编译的方式都是在 google3 目录运行 blaze build [path],而 blaze test [path] 是运行项目里的所有测试。一个工程师如果需要对某个他不熟悉的项目做一些小的修改,需要的知识是很少的,只要做完改动后确保新的代码有测试覆盖,所有老的测试能通过,就可以提交到 code review 的系统了。

每个项目会有一个 Wiki 页面介绍怎么在本地运行和调试,通常来说是描述需要传递什么命令行参数来连接本服务依赖的线上或测试环境的其他服务。对于需要实际运行、进行人工验证的复杂改动,看了 Wiki 以后就能自己在本地跑一个实例,这类改动在 code review 时也通常会要求提供一个本地 demo 的地址。所以任何一个工程师都可以把 Google 的任何一个服务从源代码编译并运行起来。

自动化测试

GWS 作为一个 C++ 的项目,测试覆盖率保持在 90% 以上,这是非常不容易的。用静态语言的项目测试难度比动态语言大很多,因为对象的属性和方法无法动态替换,想要能在测试中 mock 掉 side effects 需要在设计上做更多的努力。自动化测试的好处相关的书上有很多,就不赘述了。我只说两点:第一,对自动化测试的要求确实可以产生更好的设计,比如鼓励面向接口的设计;第二,对于 Google 那样的协作模式来说,自动化测试不是一件锦上添花的事,而是必须。因为无论是去修改其他项目的信心,还是让其他人来修改自己的项目的信心,都来自于很高的测试覆盖率。

确保自己项目的稳定性和质量的方式不是不让别人改,而是把自己关心的东西加到测试里去。比如当时我们有一个用来表示用户请求的类叫 GWSRequest,一个 GWSRequest 对象的生命周期就是整个请求处理的过程,所以这个类自然就成了存这个过程中产生和消费的各种信息的地方。时间一久它的属性就越来越多,还出现了多个属性重复存了同样的信息的情况。后来我们加了一个测试 STATIC_ASSERT_LE(sizeof(GWSRequest), MAX_GWSREQUEST),试图往 GWSRequest 加新属性的改动在编译时就会失败。如果有人想改 MAX_GWSREQUEST 的值,就需要说明为什么是必须的。有的比较卷的团队为了控制代码的复杂度,还把自己 code base 的行数上限放到了测试里。如果有人增加了 10 行代码,就需要重构其他地方的代码来省出 10 行,或者提供一个好的理由来提高上限。

Code Review

Code review 是每个 Google 的工程师日常工作中很核心的部分。每一个改动都需要经过除作者外至少另一个人的 review 才会 merge。无论是对发明 Python 的 Guido van Rossum 还是发明 C 的 Ken Thompson, 都是如此。google3 代码树中每个项目的目录有一个 OWNERS 文件,里面是一些工程师的 ID。当有人提交一个 changelist 后,系统会自动把这个 changelist 分配给涉及到的每个目录的一个 owner 来 review,每个 owner review 通过,并且涉及到的各目录的测试都成功之后才能 merge。Guido van Rossum 加入 Google 后前几年的成果就是开发了内部的 code review 工具 Mondrian,可见这件事对 Google 的重要程度。

做 Review 的人主要关心几件事:

  • 改动的业务逻辑是否正确,具体实现在性能和可读性方面是否合理;
  • 是否符合 Google 的代码规范以及本项目附加的一些条件;
  • 新的代码是否有足够的自动化测试覆盖;
  • 用户可见的改动是否被产品经理批准(会要求提供 Buganizer 的链接,Buganizer 是 Google 自己开发的类似 Jira 的系统)。

除了日常的 code review 外,每个新员工会需要学习公司的代码规范,并通过工作中会用到的每个语言的可读性 review。方式是准备一个百行以上的 changelist,提交给一个有资格做 readability review 的工程师,通过之后才有权限提交用在生产环境的代码。有权做这类 review 的是在一个叫 readability committee 里的资深工程师,他们也负责制定公司的代码规范。比如当时给我做 C++ readability review 的是Generic Programming and the STL 的作者 Matt Austern。我刚入职时同组的同事告诉我 Guido van Rossum 到 Google 后第一次 Python readability review 没有通过,不知真假。

Code review 除了让项目的 owner 保证代码质量,让其他项目可以快速推进需要的改动外,还起到传播知识的作用。很多时候一些在公司多年的工程师的 reviewer 是入职不久的新人,做 review 的人往往也能从别人的代码里学到很多东西。

版本管理和发布流程

Google 在内部做版本管理的方式就是不做版本管理,所有项目都 live at head。当然,每个项目都有对公司外的版本发布流程,live at head 是对内依赖的策略。一般不会出现一个项目依赖一个组件的 1.1,而另一个项目依赖这个组件的 1.2 这样的情况。每个项目在每次编译的时候,都会把所有依赖从最新的代码编译,使用到的依赖的版本是由编译的时间点决定的。这自然就意味着每一次提交后的代码状态都应该作为稳定版本对待。

一般外部的开源项目,会分不同的版本号,由用户决定什么时候升级,并且由用户做升级所需要的改动。按照 2015 年的数据,Google 有 20 亿行代码,其中的依赖关系错综复杂。而 C++ 对于一个项目用到的两个组件依赖同一个组件的不同版本的场景(菱形依赖)支持是很脆弱的。所以不太可能使用 SemVer 之类的版本方案,除了纯技术上的问题外,每个组件要为内部用户维护多个版本也是成本巨大的。所以 Google 把版本管理完全倒了过来,每个项目/组件都只要维护一个最新版,所有的改动最重要的原则是不能破坏任何测试。所以如果有人在一个共享组件里做了向前不兼容的改动,就会需要在同一个 changelist 里把整个代码库里所有调用到这个接口的地方改过来。有的改动会动到十多个项目,需要十几个 owner 来 review,这并不少见。当然 Google 有强大的代码搜索工具来辅助这样的事。据几年后 Google 工程师在 CppCon 上的分享,他们还用 MapReduce 做了大规模重构工具,不过是在我离开之后了。

没有内部版本管理就意味着要保证每个版本的稳定。Code review 的工具会保证每次 merge 前都运行了所有涉及的测试,但是有时还是会发生代码 merge 之后导致测试失败的情况。比如可能有 race condition,也就是两个人几乎同时 merge 了会相互影响的代码;也可能失败的测试和改动的文件不在一个目录,导致 merge 前没有运行。GWS team 有个工程师轮值的角色叫警长(sheriff),一旦 GWS 的任何测试变红,本周的警长就会找出来是谁干的,并把对应的改动撤销。后来有人做了工具把这个流程半自动化了,一旦有测试失败,就会自动生成一个 changelist 把对应改动撤销并发 code review 给警长,他只要确认、跑测试、点 merge 就行。再后来有的团队把这个过程完全自动化了,不用人工干预。

GWS 每周会做一次 binary push,也就是二进制文件的发布。流程是每周一早上负责发布的工程师从当前的代码做一个发布分支编译出一个二进制文件,交给 QA 开始测试,发现 bug 就把修复 cherry pick 到发布分支。由于大部分逻辑错误都会在开发过程中的自动化测试发现,QA 阶段发现的大部分是通过截图比对工具找到的某条线移了一个像素之类的问题。QA 流程通过后,就从单数据中心单台机器开始灰度,逐步发布到全球。如果到周四下班还没完成发布,本周的发布就会被放弃,下周再重复同样的流程。发生发布失败的情况很少,如果发生会作为事故来开会复盘。

除了 binary push 外,还有 data push,也就是数据发布。数据包括 GWS 的配置文件、各种黑名单白名单、模版文件等。Data push 比较轻量,每天有多次,都是采用灰度发布到全球的方式。

以上这样 live at head 的方式意味着不太可能有长期存在的功能分支。如果一个工程师在独立功能分支上开发了几周,那基本上是不太可能合并回主线的。无论是多大的新功能,都会需要拆成很多小的 changelist,高频地提交到主线。只是未完成的功能会用一个 flag 屏蔽掉,在生产环境不会运行。所有用户可感知的改动都是用与试验一致的方式发布,从单个数据中心千分之一的流量开始灰度到全量。如果发现问题,只要做 data push 把 flag 的值改回来就行,因为老的 code path 已经在线上运行了很久,所以改回来一定没问题。这让发布很安全,大家也有信心做大胆的尝试。

关于项目管理和排期

排期这个词在 Google 其实很少出现,离开 Google 之后都在小创业公司就更不会出现。如果大部分事情都需要项管排期,一件很小的事情也可能被排到一两个月后,而这件小事可能 block 了很多其他工作。把一个工程师的时间按开发任务线性地排列,完全做完一件事再开始做另一件事,也并不是高效的方式。工程师确实是需要专注的、避免多任务切换的时间,但这样的时间应该是以小时计,而不是以天或周计的。一个工程师应该把任务分割成尽可能小的单元,写完代码和测试后及时提交给别人 review,并且也需要 review 其他人的 merge request。从天和周的维度看,本来就是需要在多件事情之间切换的。

在 Google 的三年多里,我们团队的 project manager 对于工程师来说存在感一直比较低,项管不会过多干预个人的工作计划。每个工程师都相当于自己的项管,工程师之间会互相就优先级进行沟通达成共识和妥协。这样的好处是工作量小但 block 了其他人的事情会被快速完成;实际做事情的人用专业的语言沟通,不需第三者传话,也不容易造成误解;一些重要但不紧急的事情,比如重构、还技术债,也可以由工程师在日常工作中穿插地推进。我无法想象在 Google 当时的团队能按集中排期来安排工作。过去十多年里,硅谷比较成功的互联网公司都是用与 Google 相似的方式来工作的,这不是偶然现象。网状的沟通协作与传统的树状比,表面可能感觉混乱,但是因为不容易形成瓶颈、沟通中信息损耗少,效率是很高的。

我们能先从哪些事做起

Google 的人喜欢说 Google 的工程实践是为连续运行十年以上的软件设计的。10 年不是很长时间,已经基本站稳脚跟的公司需要探索在开发协作上更 scalable 的方式。我们不太可能去复制 Google 的所有东西,Google 的方式也未必放在所有公司都是适用的,但是有一些方向性的结论在整个业界是得到了共识的,我们也应该朝那个方向去努力。以下是我们可以在公司推动改进的一些方向。

在工程师团队里培养测试文化

在这个年代写完代码就交给 QA 去测试,靠人工来保证质量,是很落后的方式,因为它不能 scale。人工的测试做十次就是十倍的成本,哪怕每次内容都一样。自动化测试是所有人都知道好,很少人实际做,更少人能做好的事。Google 也不是从一开始就把测试做得很好,而是由一小群人努力地在全公司推动起来。最可见的一件事是 Testing on the Toilet,内部简称 TotT。他们在全球办公室的几百个马桶前都装上了这样的海报,这样大家在上厕所的同时还能学习如何写测试。久而久之,重视的人越来越多,开始在 code review 中执行测试覆盖率的要求,大部分的项目都逐步建立起高质量的测试。

让自己的项目更容易被别人改动

要改代码首先要看得到。我一直认为代码应该默认是对内公开的,虽然可能会增加泄露风险,但和效率的提高比是微不足道的。如果市值几千亿美元的公司都能对每个工程师开放代码,对大部分公司而言部门之间还互相捂得很严就太小气了。Git 本身不是为 Monorepo 设计的(虽然最近有一些支持,以及微软有全球最大的 Git repo),可能短期我们也难以大范围转为 monorepo,但是 GitLab 把 repo 组织成树状的设计一定程度上是为了在权限管理上模拟 monorepo 按目录管理的方式,是很容易做到默认 owner 可读写,其他人可读,让任何人都可以通过 merge request 改任何代码的。

让别人更容易改动还包括提供内部文档,让别人知道如何修改、运行、调试(别人包括团队新人、其他团队、未来的自己);提高可读性和模块化,让别人容易理解;提高测试覆盖率,让别人不容易改坏。

尽量提交代码而不是需求

现在经常会遇到有一件事需要涉及多个项目的改动,把需求提到其他项目,发现要排到很久以后了。解决优先级上的冲突,让对自己重要的任务早点完成的最好办法就是把一件事的开发收缩到同一个团队,不管要改的代码在哪里,都尽可能由同一个团队完成开发,由 owner review。从经济上来说,由需求方投入资源也是更合理的方式。这样的工作方式偏爱能力全面的团队和工程师,在 Google 一个人经常性地提交 2~3 种语言的代码是很常见的。

当然,也不是所有的事情都能收敛到同一个团队进行。有的任务是需要对多个项目进行根本性的改动的,就会需要来自多个团队的工程师形成一个临时的小团队了。

建立代码规范

很多公司通常是从很小的时候就会建立公司范围的代码规范,因为有了一致的规范,才好使用同样的工具,阅读和修改其他团队的代码时才能比较容易。如果在规模较大时再从零开始做这件事会非常困难。可行的路径可能是自下而上的,各个团队先建立自己的规范,协作得多的团队可以取长补短,从能建立共识的部分形成共同的规范,逐步扩大适用的范围。

打一个招聘广告

TapTap 开发者服务(TDS)在招聘后端工程师、Android 工程师和技术支持工程师,详情请见我们的技术负责人在 V2EX 发的帖子


最新文章

评论

Loading comments ...