作者:Radha Narayan, Bobbi Jones等
来源:《谷歌软件工程》第24章
我最近刚刚做完《Software Engineering at Google》一书的审校,顺便捡选了一点儿其中有关持续交付的内容,在此分享给大家,仅作为学习交流之用。让大家先睹为快!
开发速度是一项团队运动,最佳的工作流程需要模块化的架构和持续集成。
通过功能开关的方式才能尽早做特性隔离。
使用灰度部署来解决设备多样性和用户群的广度问题。
尽可能使用类生产环境做发布验证,否则,可能会导致后期的意外故障。
只发布有用的功能:监控任何功能的成本和价值,以了解它是否仍然相关,是否提供了足够的用户价值。
左移:通过 CI 和持续部署,在所有变更之前实现更快、更数据驱动的决策。
更快就是更安全:尽早且频繁的小批量发布,以降低每次发布的风险,并最大限度地缩短上市时间。
以下内容无图,
全是文字,不易阅读,
慎入!!!
考虑到技术领域变化得如此之快以及不可预测性,任何产品的竞争优势都在于其快速投放市场的能力。一个组织的速度是与其他参与者竞争、维护产品和服务质量或适应新规则的能力的一个关键因素。这个速度受到部署时间的限制。部署并不是一次性的。
在软件产品的生命周期中涉及到对新想法的快速探索、对环境变化或用户问题的快速响应,以及实现大规模快速开发。任何一个组织长期成功的关键,始终在于它能够将想法尽快付诸实施,并交到用户手中,对用户的反馈做出快速响应。Martin Fowler 在 2013 年 5 月发表的博客文章《持续交付》中指出,「任何软件工作的最大风险是你最终构建的东西是无用的。你越早、越频繁地将可工作的软件呈现在实际用户面前,你就会越快地得到反馈,从而发现它的真正价值。」在交付用户价值之前,长时间处于进行中的工作是高风险和高成本的,甚至会消耗士气。在谷歌,我们努力尽早并经常发布,或者「产品上市并迭代」,以使团队能够快速看到他们工作的影响,并更快地适应不断变化的市场。代码的价值不是在提交时实现的,而是特性呈现在用户面前时实现的。缩短「代码完成( Code Complete )」和用户反馈之间的时间,可以最大限度地降低正在进行的工作的成本。
持续交付( CD )和敏捷方法论的一个核心原则是,随着时间的推移,小批量变更会带来更高的质量;换句话说,更快更安全。乍一看,这对于团队来说似乎是非常有争议的,尤其是在实施 CD 的先决条件尚未到位的情况下,例如,持续集成( CI )和自动化测试。对于所有团队来说,要达到 CD 的理想状态可能需要很长一段时间。所以,我们要专注于开发的各个方面,这些方面能够在实现最终目标的过程中独立交付价值。- 力争采用模块化架构,以隔离变更,并使故障排除更加容易
似乎频繁地发布软件新版本有风险。随着你的用户群的增长,你可能会担心出现在测试中没有被发现的缺陷,愤怒的用户会强烈反对,并且在产品中可能有太多的新代码,以至于无法进行详尽的测试。但这正是 CD 能起作用的地方。理想情况下,上一个版本和下一个版本之间的变更很少,解决问题也就越简单。在极限情况下,使用 CD ,每个变更都会经过部署流水线并自动部署到生产环境中。没错,对于许多团队来说,这看上去通常是不太现实的。因此,必须引入「文化变革」的过程。在这一过程中,团队逐步完善「在任何时间点可部署」的准备度,而不必真的执行发布,从而建立他们在将来能更频繁发布的信心。
当一个团队很小的时候,变更会以一定的速度进入代码库。我们已经看到,随着时间的推移,一个团队不断成长或分裂成子团队,反模式也随之出现:一个子团队从其代码库拉分支,以避免「踩到任何人的脚」,但随后就会在集成和寻找「罪魁祸首」方面遇到困难。在谷歌,我们更希望团队持续在共享代码库中进行开发,并设置 CI 测试、自动回滚和寻找罪魁祸首来快速识别问题。我们的代码库之一 YouTube ,
它是一个大型的、单体 Python 应用程序。
发布过程非常辛苦,
需要构建警察、发布经理和其他志愿者等多人参与。
几乎每个版本都有多个 cherry-pick 的变更和衍生版本。
每个版本都需要远程质量团队做 50 个小时的手动回归测试。
当发布一个新版本的操作成本如此之高时,
就会出现一个循环,即:
在发布新版本前,你总是想再多执行一些测试。
此时,当有人想再向这个版本中添加一个几乎已经准备好了的特性时,很快你就会迎来一个费力、容易出错且缓慢的发布过程。
最糟糕的是,上次执行版本发布的专家已经因精疲力竭而离开团队了,现在甚至没有人知道,在试图发布更新时,如何排除发生的那些诡异崩溃,这让你一想到要按下那个发布按钮,就感到恐慌。
如果你的发布成本高且时常有风险,那么本能的反应就是放慢发布节奏,增加稳定期。然而,这只能提供短期的稳定性,随着时间的推移,它会降低开发速度,并让团队和用户感到沮丧。正确的答案是:降低成本、增加纪律性,并让风险变成递进式风险,而至关重要的是,要抵制明显的手工修复,并投资于长期的架构变更。对问题的手工修复会将工作流程导向那种非常传统的做法:回到原来那种缺乏学习或迭代的传统规划模型,向开发过程中添加更多的治理和监督,实施风险审查,或者奖励低风险(通常是低价值)的特性。然而,回报最好的投资是迁移到微服务架构,这样可以使大型产品团队保持活力和创新能力,同时降低风险。某些情况下,在谷歌,答案是从头重写一个应用程序,而不是简单地迁移它,将所需的模块化建立在新的架构之上。尽管这两种选择都可能需要数月时间,而且在短期内可能会很痛苦,但在运营成本和认知简化方面所获得的价值,将在应用程序的数年生命周期内得到回报。
可靠的持续发布的一个关键是确保工程师们对所有的变更都有「开关」。随着产品的增长,在同一个二进制包中,会有多种正处于不同开发阶段的特性共存。特性开关可用来在产品中控制各个特性代码的包含或表现,并且可以针对是发布阶段或是开发阶段,构建出相应的不同表现。如果语言允许的话,当关闭某个特性的开关后,构建工具就可以在构建中删除该特性。例如,一个已经提供给客户的稳定特性可以在开发版本和发布版本中同时启用;而正在开发的特性可能只为开发阶段启用,保护用户不受未完成功能的影响。此时,新特性代码在二进制包中,与旧代码路径并存,并且都可以运行,但新代码由一个开关保护。如果新代码生效了,你就可以删除旧的代码路径,并在后续版本中完全启用该特性。一旦有问题,可以通过动态配置更新开关的值,而无需更新二进制版本。在二进制发布的旧世界中,我们必须将媒体新闻发布稿与软件二进制版本的发布紧密绑定在一起。在发布关于新功能或新特性的新闻稿之前,我们就必须成功发布二进制版本。这意味着,在正式宣布这这一功能之前,就可能被别人发现这一新特性,失去“重大新闻”的意义。如果新代码有开关,则可以在新闻发布的同时更新该开关,以便立即打开特性,从而最大限度地降低泄漏特性的风险。这就是特性开关的魅力所在。注意,特性开关代码不是真正敏感特性的完美安全防护网。如果代码没有很好的混淆,仍然可以对代码进行爬取和分析,并且,不是所有特性都可以隐藏在开关后面而不增加复杂性的。此外,即使是使用开关,配置变更也要必须谨慎地进行。「立即为 100 % 的用户打开开关」并不是一个好主意,因此,管理安全配置发布的配置服务是一项很好的投资。然而,将具体某个特性的命运与整个产品发布分离的能力和控制水平,是实现应用程序长期可持续性的强大杠杆。
谷歌搜索产品是公司最古老的产品。它庞大而且复杂,代码库可以追溯到谷歌最早的源代码——搜索我们自己的代码库,仍然可以找到至少可以追溯到 2003 年(通常更早)编写的代码。当智能手机开始兴起时,一个又一个的移动功能被塞进了主要为服务器部署而编写的代码中。即使搜索体验变得更加有活力和互动,但是部署一个可行的构建变得越来越困难。有一段时间,我们每周在生产环境上只能部署一次搜索产品的二进制包。在当时,即使达到这个小目标也是很少见的,而且通常都是靠运气。当 Sheri Shipe 承担提高搜索产品发布速度的专项时,每个发布周期都要一组工程师用几天时间才能完成。他们构建二进制包,集成数据,然后开始测试。每个缺陷都必须经过人工分析,以确保它不会影响搜索质量、用户体验( UX )和(或)收入。这个过程既费时又费力,而且无法扩大变更的吞吐量和提升速度。结果,开发人员永远不知道他们的特性何时会发布到生产环境中。发布并不是凭空发生的,可靠的发布能使依赖因素更容易同步。在过去的几年里,一个工程师团队专门负责解决并实现了持续发布过程,简化了向生产环境发送搜索产品二进制包的所有工作。我们尽可能地将其自动化,设定提交特性的截止日期,简化插件和数据到二进制包的集成。现在,我们可以每隔一天部署发布一个新的搜索产品的二进制包。
1、没有一个二进制包是完美的
第一种思想是:没有一个二进制包是完美的,尤其是对于那些集成了几十个或数百个开发人员独立开发的几十个特性的构建来说。尽管不可能修复所有的缺陷,但我们仍然需要权衡以下问题:- 如果一条线向左移动了两个像素,会不会影响广告的显示和潜在的收入?
我们必须承认,软件本身就是复杂的。没有完美的二进制包——每次新的变更发布到生产环境中时,都必须做出权衡。根据明确阈值的关键性能指标来决策特性发布,即使它们并不完美,也能让原本有争议的发布决策更加清晰。曾经有一个缺陷,它涉及到一种罕见的方言,这种方言只存在于菲律宾的一个岛屿上。如果用户用这种方言问一个搜索问题,他们将得到一个空白的web页面,而不是问题的答案。我们必须决定,为了修复这个缺陷而付出的代价,是否值得推迟一个主要新特性的发布。
我们从一个办公室跑到另一个办公室,试图确定有多少人实际上说这种方言,是否每次用户搜索这种方言时都会发生这种情况,以及这些人是否经常使用谷歌。
与我们交谈过的每一位质量工程师都认为我们应该向一位更资深的人求助。
最后,在有数据在手的情况下,我们向搜索的高级副总裁提出了这个问题。
我们应该推迟一个关键的发布来修复一个只影响一个很小的菲律宾岛屿的缺陷吗?
事实证明,无论你的岛屿有多小,你都应该得到可靠和准确的搜索结果:我们推迟了发布,并修正了缺陷。
ps:这个故事在不同场景下可能有不同的答案。
2、赶上你的发布期限
第二种思想是:如果你错过了发布火车,它便不会等你。有句格言是这样说的:「最后期限是确定的,但生活不是。」在发布时间线的某个时刻,你必须坚定地拒绝开发者和他们的新功能。一般来说,截止日期一过,让某个新特性赶上今天发布的再多恳求和乞求,都无济于事。在某个周五的深夜,六名软件工程师在恐慌中冲进了发布经理的办公隔间。他们和NBA签了合同,并且刚刚才开发完成这个功能,而它必须在明天的比赛前上线。他们说:
「这次发布必须停下来,
我们必须把这个特性捡选( cherry-pick )入二进制包中,
否则我们将违反合同!」
一位睡眼惺忪的发布工程师摇摇头,说拉发布分支,并测试一个新的二进制包要花费四个小时的时间。今天是他们孩子的生日,他们还要去捡气球呢。
意味着他们还能在几小时后赶上下一班火车,而不是几天。这减轻了开发人员的恐慌,极大地改善了发布工程师的工作和生活的平衡。(ps:确定是改善?)
臃肿是大多数软件开发生命周期中的一个不幸的副作用,产品越成功,其代码通常就越臃肿。快速且高效的发布火车的一个缺点是:这种臃肿通常会被放大,并且会表现为对产品团队甚至用户的挑战。尤其是当软件交付的是客户端(比如移动应用程序)时,这可能意味,即使用户从未使用过的功能,他们的设备在空间、下载和数据成本方面也都要付出代价,而开发人员则要付出构建速度慢、部署复杂和罕见缺陷的成本。在谷歌,这通常意味着要配备专门的团队来不断提高产品的效率。虽然有些产品是基于网络的,运行在云上,但许多产品是在用户设备(电话或平板电脑)上使用共享资源的客户端应用程序。选择本地运行的应用程序本身就是一种权衡,本地应用程序的性能更好,对不稳定的连接更有韧性,但也更难更新,更容易受到平台级问题的影响。反对频繁、持续部署本地应用程序的一个常见论点是:用户不喜欢频繁更新,仩还必须为数据成本和中断买单。可能还有其他限制因素,比如访问网络的限制,或是为了更新而必须重启的限制。尽管在更新产品的频率方面需要做出取舍,但我们的目标是有意地做出这些选择。通过一个平稳、运行良好的 CD 过程,可以将一个可见版本的创建频率与用户接收它的频率分开。你能达成每周、每天或每小时部署一次的目标,但实际上并不必真的这么做。而且,你应该有意识地依据用户的具体需求以及更大的组织目标来选择发布流程,并决定最能支持产品长期可持续性的人员配置和工具模型。保持代码模块化,可以实现动态、可配置的部署(或加载),从而更好地利用受限资源,例如用户设备上的空间。在没有这种实践的情况下,每个用户都必须接收他们永远不会使用的代码,或用于其他类型设备的架构。动态部署允许应用程序保持较小的规模,同时只将那些能为用户带来价值的代码发送到其设备上,而 A/B 实验允许在功能的成本与它对用户和业务的价值之间进行有意地权衡。建立这些流程需要付出前期成本,而识别和消除令发布频率低于我们预期的那些摩擦是一个艰苦的过程。但是,风险管理、开发速度和支持快速创新方面的长期收益是如此之高,以至于花费这些初始成本是值得的。
如果你是为所有用户设计的,你可能会在智能屏幕、音箱或者 Android 和 iOS 手机和平板电脑上安装客户端。然而,超过 20 亿个 Android 设备的多样性会让你对发布一个合格的版本感到无所适从。我们的一位发布经理分享了一个充满智慧的观点,他说:我们客户市场的多样性不再是个问题,而是个事实。当我们接受这一点后,我们可以通过以下方式切换发布质量评估模型:- 既然全面性测试实际上已不可行,就以代表性测试为目标。
- 灰度发布,缓慢增加用户群的百分比,从而实现快速修复。
- A/B 发布,通过统计上显著的结果来自动化评估版本质量,而不需要疲劳的人工查看仪表盘并做出决定。
当涉及到为 Android 客户端开发时,谷歌应用程序使用专门的测试渠道,并通过灰度方式增加用户的比例,仔细监控这些渠道中的问题。因为 Play Store 提供了无限的测试渠道,我们还能在我们计划发布的每个国家设立一个 QA 团队,以便在测试关键功能方面实现全球一夜覆盖。我们在部署到 Android 时注意到的一个问题是,仅仅通过推送更新,我们就可以预期到用户指标在统计上会有显著的变化。这意味着,即使我们没有对产品进行任何更改,推送更新也会以难以预测的方式影响设备和用户行为。因此,尽管对一小部分用户进行更新,可以给我们提供有关崩溃或稳定性问题的良好信息,但它很少告诉我们,我们的应用程序的新版本是否比旧版本更好。在谷歌,我们的一些大型应用程序也会用 A/B 测试来测试它们的部署。这意味着发送两个版本的产品:一个是期望的更新版本,另一个对照的基线版本,它是一个「安慰剂」(旧版本会再次发布)。由于这两个版本同时向足够多的相似用户推出,你可以比较两个版本,以了解最新版本是否比前一个版本有所改进。有了足够大的用户基数,你应该能够在几天甚至几小时内获得统计上显著性的结果。一旦有足够的数据能够保证护栏指标( guardrail metrics )不会受到影响,自动化的度量流水线就能将它推向更多的用户,从而实现尽可能快的发布。注意:这种方法并不适用于每一个应用程序,而且在用户数量不够大的情况下,可能会造成大量的开销。在这种情况下,推荐的最佳实践是使用变更中立的版本,即:所有的新特性都有特性开关保护,因此,在部署期间唯一需要测试的变化就是部署本身的稳定性。
尽管「总是保持可部署状态」有助于解决影响开发人员速度的几个问题,但也有一些实践可以解决规模化问题。最初你的产品团队可能少于 10 人,每个人轮流负责部署和生产监控。随着时间的推移,你的团队可能会发展到数百人,其中不同的子团队负责各自的特定功能。一旦组织规模大了,每次部署中的变更数量和发布的风险也会呈超线性增长。「让发布成功」就变成了一个高度人工参与且劳动密集的工作。开发人员经常会试图决定下面哪种情况更糟糕:是放弃一个包含四分之一的新特性和缺陷修复的版本,还是发布一个对其质量没有信心的版本?随着规模扩大,复杂性的增加通常表现为发布延期的增多。即使你每天发布一个版本,也可能需要一周或更长的时间才能安全地完全发布,因此,排查和调试任何问题都会落后一周的时间。这就是利用「总是保持可部署状态」可以让项目开发回归正轨的地方。频繁发布火车使得每次变更的内容少,从而更容易定位和解决问题。但是一个团队如何确保一个庞大且快速扩展的代码库所固有的复杂性不会影响进度呢?在谷歌地图产品上,我们认为特性是非常重要的,但很少有特性能够重要到必须为其发布一个版本。如果能做到频繁发布,那么某个特性错过一次发布所带来的痛苦,要比让一次发布中所有新特性都延迟带来的痛苦小很多,更别说是当一个还没有完全准备好的特性被匆忙打包,让用户所感受到的痛苦了。在进行权衡时,开发人员对发布新特性的热情和紧迫感永远无法胜过现有产品的用户体验。这意味着新特性必须通过强力的契约,关注点分离,严格的测试,早期且频繁的沟通以及新特性验收,来与其他组件隔离开。
多年来,在我们所有的软件产品中,我们发现,更快就是更安全,这可能与直觉相反。你的产品的健康和开发速度实际上并不是对立的,更频繁且小批量发布的产品有更好的质量结果。它们能更快地应对发现的缺陷和意想不到的市场变化。不仅如此,更快的话,成本也会更低,因为一个可预测且频繁发布的火车会迫使我们努力降低每次发布的成本,并让放弃发布的成本非常低。仅仅是建立起来一种运作结构,让我们能做到持续部署,就可以产生大部分的价值,即使你实际上并没有向用户发布这些版本。这是什么意思呢?事实上,我们并不是每天都发布完全不同的搜索、地图或 YouTube 版本,但要做到这一点,需要有:- 有关于用户满意度和产品健康状况的准确和实时的指标,
- 一个协调一致的团队,对什么能发或不能发以及原因有明确的政策。
- 一个保障安全措施的工具链,它可以试运行验证、回滚 / 前滚机制,以及能可靠地打补丁。
往期推荐:
技术琐话
以分布式设计、架构、体系思想为基础,兼论研发相关的点点滴滴,不限于代码、质量体系和研发管理。