研发效率破局之道06 | 代码入库到产品上线:Facebook如何使用CI/CD满足业务要求?

你好,我是葛俊。 
在上⼀篇⽂章中,我和你分享了代码⼊库前的流程优化,即持续开发。今天,我会继续与你介绍流程优化中,代码⼊库和⼊库后的 3 种持续⼯程⽅法,即持 续集成(Continuous Integration, CI)、持续交付(Continuous Delivery, CD)和持续部署(Continuous Deployment, CD)。 
在接下来的分享中,⾸先我会与你介绍这 3 种⽅法的定义和作⽤,帮助你深⼊理解这 3 个“持续”背后的原理和原则;然后我会以 Facebook 为参考,给你介 绍基于这些原则的具体⼯程实践。我希望,这些内容能够帮助你⽤好这三个“持续”⽅法,提⾼团队的研发效能。 
⾸先,我先来介绍⼀下,持续集成、持续交付和持续部署是⽤来解决什么问题的,也就是它们的定义和作⽤。 
3 个“持续”的定义和作⽤ 
不知道你是否还记得,在开篇词中,我提到过⼀个低效能的情况,即产品发布上线时出现⼤量提交、合并,导致最后时刻出现很多问题。这个情况很常⻅,引 起了很多⽤户的共鸣。产⽣这个问题最主要原因是,代码合并太晚。这⾥,我再与你详细解释⼀下原因。 
当多个⼈同时开发⼀款产品的时候,很可能会出现代码冲突。⽽解决冲突,需要花费较多的时间;同时很可能出现冲突解决失败,导致产品问题。 
如果没有⼀个机制督促我们尽早把代码推到主仓进⾏集成的话,我们通常会尽量先在⾃⼰的分⽀上进⾏开发。结果往往是,在冲刺快要结束,或者功能即将发 布时,才出现⼤量的代码合并。 
⽽这时因为很⻓时间没有进⾏过代码集成了,进⾏集成的代码量通常⽐较⼤,不同开发者的代码区别很⼤,冲突也很严重,难以解决。具体的负⾯影响是:发布推迟,产品质量不⾼,每⼀次发布时的熬夜和紧张影响团队⼠⽓。 
⽽持续集成的根本出发点,就是为了解决这个问题。也就是说,它能够帮助开发⼈员尽量早、尽量频繁地把⾃⼰的改动推送到共享的代码仓库分⽀上,进⾏代码集成,从⽽减少⼤量代码冲突造成的低效能问题。 
所以,持续集成的定义就是:在团队协作中,⼀天内多次将所有开发⼈员的代码合并⼊同⼀条主⼲。 
代码⼊库后,剩下⼯作是把代码编译打包成可以发布的形式,先发布到测试环境进⾏测试,再发布到类⽣产环境进⾏测试,最终部署到⽣产环境。 
在这个过程中,有两个问题需要特别注意。 
  • 首先,我们需要这个流程尽量频繁。如果我们能够把产品和功能尽快地发布到市场上,就能够更快地服务客户,以更快的试错速度寻找到用户,提供真正对客户有价值的功能。 即使你的产品由于自身特性不会太频繁地部署给用户,但这种能够频繁生产出可以马上部署的产品的能力,也能让你在需要部署时,快速完成任务。
  • 其次,在产品发布到不同环境的过程中,我们会发现一些在开发和持续集成中没有暴露的问题。如果在产品要正式发布时才发现这些问题,就会造成产品交付推迟,影响线上用户等情况,造成损失。针对这个情况,我们需要提前发现问题。
⽽解决这两个问题,正是持续交付和持续部署的出发点。 
持续交付的⽬标是,对每⼀个进⼊主⼲分⽀的代码提交,构建打包成为可以发布的产品。它的定义是:⼀种软件⼯程⽅法,在短周期内完成软件产品,以保证 软件保持在随时可以发布的状态。也就是说,对每⼀个提交,把集成后的代码部署到“类⽣产环境”中进⾏验证。如果代码没有问题,后续可以⼿动部署到⽣产 环境中。 
⽽持续部署,则更进⼀步。它把持续交付产⽣的产品⽴即⾃动部署给⽤户,定义就是:将每⼀个代码提交,都构建出产品直接部署给⽤户使⽤。 
以上就是持续集成、持续交付与持续部署的作⽤和定义。在实现上,它们共同的本质是,让每⼀个变更都经过⼀条⾃动化的检验流⽔线,来检查每⼀个变更的 质量,通过就进⼊下⼀个阶段。 
这⾥的“下⼀个阶段”具体包括:代码并⼊主仓、产品进⼊测试环境、产品进⼊类⽣产环境、产品最终进⼊⽣产环境,如下图所示。 
图 1 CI/CD 流⽔线示意图 
你应该已经注意到了,整条流⽔线中,持续部署只是持续交付的最后⼀步,也就是⾃动化上线的那⼀步,前⾯的各种检查,都属于持续交付流⽔线。所以,我 在后⾯的内容中再提到流⽔线时,CI/CD 指的就是“持续集成 / 持续交付”。 
CI/CD 流⽔线,能够⼤⼤提⾼代码⼊库的速度和质量,是这⼏年硅⾕互联⽹公司做到⾼效研发的必要条件。接下来,我就与你介绍 CI/CD 流⽔线的具体原则 以及最佳实践,然后以 Facebook 的具体实践为例帮助你加深理解。 
需要注意的是,在这篇⽂章中,我会重点与你分享 CI/CD 流⽔线的搭建原则。⽽关于具体的搭建⽅式,通常是持续集成⼯具 + 代码仓管理系统 + 检查⼯具 + 测试⼯具,⽐如 Jenkins+GitLab+SonarQube+Linter+UnitTest 的组合。你可以参考这个链接提供的⽅式去搭建。 
CI/CD 流⽔线的具体原则以及最佳实践 
根据上⾯提到的 3 个“持续”的本质,要做到⾼效,有 3 条基本原则: 
  • 流⽔线的测试要尽量完整;
  • 流⽔线的运⾏速度⼀定要快
  • 流⽔线使⽤的环境,尽量和⽣产环境⼀致。
基本原则 1:流⽔线的测试要尽量完整 
CI/CD 流⽔线的测试只有尽量完整,代码和产品的质量才能有保证。所以,最主要的⼯程实践,就是在流⽔线中运⾏⼤量⾼质量的测试和检查。 
Facebook 就有⼤量的单元测试和集成测试⽤例、安全扫描,以及性能专项测试⽤例。如果某个验证在流⽔线中失败,开发⼈员会考虑是否要添加测试⽤例来 防⽌再出现类似的问题。 
另外,Facebook 持续在测试⽤例的开发上投⼊。在内部⼯具团队,有⼀个专⻔的测试⼯具团队来建设测试框架和测试流程,⽅便开发⼈员⾃⼰开发测试⽤ 例。⽐如,我在 Facebook 那⼏年,他们就⼀直在改进 JavaScript 的 Mock 框架,对开发⼈员写测试⽤例来说⾮常⽅便。 
基本原则 2:流水线的运行速度一定要快 
因为每⼀个变更都要通过 CI/CD 流⽔线的检验,所以流⽔线的速度关乎研发速度。⽽要提⾼这条流⽔线的速度,我们可以从以下两个⽅⾯考虑。 
⾸先,从技术⻆度考虑。⽐如: 
  • 使⽤并⾏⽅式运⾏各种测试来提速; 
  • 投⼊硬件资源,使⽤⽔平扩展的⽅式来提速; 
  • 使⽤增量测试的⽅式进⾏精准验证。也就是说,只运⾏跟当前改动最相关的测试,以减少测试⽤例的运⾏数量。
其次,权衡流⽔线的运⾏速度、流⽔线资源和测试完整性的关系。不难理解,运⾏速度快、占⽤资源少、测试完整难以兼顾,因此我们必须做出权衡。这⾥我 推荐⼏个⽅法: 
  • 如果通过增加硬件资源来提升运⾏速度需要的成本太⾼的话,可以对测试⽤例按优先级进⾏分类,每天运⾏流⽔线的时候,不⽤每次都运⾏所有测试⽤例, 只选择其中⼏次进⾏全量测试。 
  • 提供⽀持,让开发⼈员在本地也能运⾏这些测试,从⽽使⽤本地资源尽早发现问题,这就避免了⼀些有问题的提交占⽤流⽔线的资源,进⽽提⾼整条流⽔线 的运⾏速度。 
  • 运⾏测试的时候,按照⼀定的顺序运⾏测试⽤例。⽐如可以先运⾏速度快的⽤例,以及历史上容易发现问题的⽤例。这样可以尽早发现问题,避免耗费不必 要的资源。
基本原则 3:流⽔线使⽤的环境,尽量和⽣产环境⼀致 
这⾥的环境,包括机器环境、数据、软件包、⽹络环境等。环境不⼀致可能导致问题暴露在⽤户⾯前,损失严重;另外,在⾮⽣产环境上难以复现⽣产环境的 问题,调试困难。 
保证流⽔线环境与⽣产环境⼀致,具体⽅法包括: 
  • 软件包最好只构建⼀次,保证各种不同环境都⽤同⼀个包。如果不同的运⾏环境需要不同的参数,可以采⽤环境变量的⽅式把这些参数传递给软件包。 
  • 使⽤ Docker 镜像的⽅式,把发布的产品以及环境都打包进去,实现环境的⼀致性。在我看来,这正是 Docker 的最⼤好处。 
  • 尽量使⽤⼲净的环境。⽐如,测试时,使⽤刚从镜像产⽣的系统;⼜⽐如,使⽤蓝绿部署,每次产⽣新的部署时,直接丢弃旧的环境。
以上就是 CI/CD 流⽔线的 3 个基本原则和最佳实践。通过提⾼验证的完整性、速度,以及保证环境的⼀致性,我们可以降低成本,提⾼产品质量和验证产品 价值假设的速度。 
接下来,为了帮助你理解并正确运⽤这些原则和最佳实践,我们⼀起来看看 Facebook 是怎么做的。 
具体案例:Facebook 是如何实施 CI/CD 来提⾼效能的? 
Facebook ⼀直就⾮常注重 CI/CD,早在 2009 年就建设了顺畅的 CI/CD 流⽔线,⽽且⼀直在持续改进。  
在 CI ⽅⾯,加强建设持续开发,让开发⼈员能在开发环境上进⾏⼤量的验证。本地的所有验证,与 CI 流⽔线上的验证⽅式保持⼀致,这就⼤⼤提⾼了开发 ⼈员在本地发现问题的能⼒,从⽽⼤量避免了有问题的代码提交到 CI 流⽔线,浪费资源。 
代码⼊库的步骤,采⽤ Phabricator 作为 CI 的驱动,并作为质量检查中枢,尽量提⾼⼊库前代码审查的流畅性。在这个过程中,Facebook 做到了以下⼏ 点: 
  • 测试的完整性。代码提交到 Phabricator 进⾏代码审查的同时,进⾏各种静态检查、单元测试、集成测试、安全测试,以及性能测试等。 
  • ⼯具的集成。Phabricator 提供的插件机制,可以跟其他系统和⼯具集成,以⽀持运⾏各种检查。
  • 沙盒环境。代码在提交到 Phabricator 进⾏审查时,Phabricator 会⾃动产⽣⼀个沙盒环境。沙盒环境有两个好处:⼀是,可以让开发者之间进⾏联调;⼆ 是,可以让开发者并⾏地进⾏其他开发⼯作,因为在进⾏代码审查时,开发者的开发机器并没有被占⽤。 
  • ⾼效的代码审查。⽐如,代码审查不通过时,代码作者可以⽅便地在原来的提交之上进⾏修改,并在下⼀轮审查时只进⾏增量代码的审查。这就⼤⼤降低了 每次代码审查的交易成本,从⽽保证了 CI 的顺畅性。
代码⼊库之后,进⼊持续交付步骤。Facebook 使⽤⼤仓,同⼀个仓中每天有⼏千个代码提交,所以持续交付的挑战很⼤。他们有⼀个专⻔的发布⼯具团队, ⾃研了⼀套发布⼯具来实现⾃动化流⽔线,通过以下两点⽐较好地实现了流⽔线资源和测试完整性的平衡。 
  • 不针对每⼀个提交进⾏ CD 验证,⽽是按照⼀定时间间隔进⾏验证。因为提交太多,如果每个提交都进⾏构建打包,资源消耗实在太⼤,所以 Facebook 采⽤了按照⼀定时间间隔,⽐如,每 10 分钟进⾏⼀次构建打包。这就⼤⼤降低了资源的消耗,不过这⾥有个问题,在验证步骤发现 Bug 时,因为验证的 是最近 10 分钟的所有提交,所以不能精准定位造成问题的提交。 针对这个问题,Facebook 使⽤单主⼲开发分⽀⽅式,并强制在代码合并时,只能使⽤ git rebase 不能产⽣合并提交,所以提交历史是线性的,从⽽可以 使⽤ git bisect 命令来⾃动化定位问题。这部分内容我会在下⼀篇⽂章中详细介绍。 
  • 对验证进⾏分级。也就是说,有⼏条不同的 CD 流⽔线,按照不同的时间间隔运⾏构建和检验。根据运⾏时间间隔的不同,它们运⾏的检验数量以及检查 出来的 Bug 优先级也不同。间隔时间越⻓,运⾏的检验越全⾯,检查出来的 Bug 优先级越⾼。
这⾥需要说明的是,2017 年以前,Facebook 并没有把每⼀个在主⼲分⽀上成功通过流⽔线验证的软件包作为发布候选,⽽是在每周五的固定时间,从主⼲ 分⽀上拉出⼀个发布分⽀,稳定 3 天后上线。也就是说,这并不是严格意义上的持续交付。这是因为当时的⾃动化检验还不能确保产品达到上线要求。其 实,这对很多公司来说都很常⻅,都需要⼀些额外的测试和检验来确保上线产品的质量。 
最后,是持续部署的操作。在 2017 年以前,Facebook 并没有持续部署,⽽是采⽤的每周全量代码部署的⽅式。但到 2017 年,因为代码提交实在太多,每 次周部署代码,处理的提交量会超过 10000,需要很⻓时间才能稳定发布分⽀,所以 Facebook 转向了持续部署。   
具体的⽅法是,极致地进⾏⾃动化测试验证。关于实施细节,你可以参考 Facebook 的第⼀个发布⼯程师 Chuck Rossi 对持续部署流程的描述。 
值得⼀提的是,跟持续交付⼀样,Facebook 的持续部署也不是纯粹的持续部署。因为代码提交太多,他们并没有每个提交都单独部署,⽽是采⽤类似持续交 付的⽅法,把⼀段时间之内的提交⼀起部署。这种不教条的⽅式,是我从 Facebook 学到的⼀个重要的做事⽅法。 
⼩结 
Facebook 在 CI/CD 上做到了极致,对每⼀个代码提交都⾼效地运⾏⼤量的测试、验证,并采⽤测试分层、定时运⾏等⽅式尽量降低资源消耗。正因为如 此,他们能够让⼏千名开发⼈员共同使⽤⼀个⼤代码仓,并使⽤主⼲开发,产⽣⾼质量的产品,从⽽实现了超⼤研发团队协同下的⾼效能。 
在前⾯⼏篇⽂章中,我们多次提到“持续”。这个词,近些年在软件研发中⽐较流⾏,⽐如我今天与你分享的持续集成、持续交付、持续部署,加上持续开发, ⼀共有 4 个了。 
实际上,在 CI/CD 流⽔线中,做为流⽔线的⼀部分,测试⼀直在运⾏并最快地给开发者提供反馈。这正是另⼀个“持续”,也就是“持续测试”的定义。 
“持续”如此重要的原因是,软件开发是⼀个流程,只有让这个流程持续运转才能⾼效。这⾥我把这 5 个持续都列举出来,⽅便你复习、参考。 
图 2 5 个“持续”⽅法定义与关键点对⽐ 
思考题 
1. 在⼏千名开发⼈员共同使⽤⼀个⼤代码仓的⼯作⽅式下,做好 CI 有很⼤的挑战性。你觉得挑战在哪⾥,容易出现什么样的问题,⼜应该怎么解决呢? 
2. 今天我提到了持续开发在 CI 中的作⽤,请你结合上⼀篇⽂章,思考⼀下持续开发和 CI/CD 是怎样互相促进的。 
感谢你的收听,欢迎你在评论区给我留⾔分享你的观点,也欢迎你把这篇⽂章分享给更多的朋友⼀起阅读。我们下期再⻅!

发表评论

您的电子邮箱地址不会被公开。