持续改进和软件自动化

十多年前,我们在 Amazon 开展了一个项目,旨在了解我们的团队将创意转变为高质量生产系统的速度有多快。在该项目中,我们测量了软件吞吐量,以便借此提高执行速度。我们发现,从代码签入到生产平均需要 16 天。在 Amazon,团队首先提出一个想法,然后通常需要一天半的时间来编写代码,以将想法付诸实践。我们用了不到一个小时的时间来构建和部署新代码。之后大约 14 天的时间里,我们等待团队成员开始构建、执行部署和运行测试。项目结束时,我们建议将签入后流程自动化,以提高执行速度。这样做的目的是消除延迟,同时保持甚至提高质量。

这一建议的核心是一个持续改进计划,旨在提高执行速度。我们作出提高执行速度的决定符合 Amazon 领导力原则中的“最高标准”原则。该原则旨在制定严格的高标准,不断提高标准,交付高质量的产品、服务和流程。我们的领导力原则描述了 Amazon 如何开展业务、领导者的领导方式,以及我们如何秉持以客户为核心的决策导向。

Amazon 构建了一系列软件开发工具,有助于提升我们的软件工程师的工作效率。我们创建了一套集中托管式构建系统 Brazil,该系统在服务器上执行一系列命令,用于生成可部署的构件。目前,Brazil 还没有侦听到源代码更改。有待开始构建。我们还有一套部署系统 Apollo。在启动部署的过程中,相关人员需要将构建构件上传到该系统。业界开始关注持续交付,我们受此启发构建了 Pipelines 系统,用于在 Brazil 和 Apollo 之间实现软件交付流程自动化。

Pipelines:持续部署工具

我们启动了一个试点计划,对少数团队的软件交付流程进行自动化改造。该计划完成时,主要试点团队已将从签入到生产的总时间缩短了 90%。
 
该项目验证了管道的概念。管道是我们的团队定义向客户发布软件所需的所有步骤的一种方式。管道中的第一步是构建构件。然后,管道将通过一系列步骤运行该构建构件,直到将构件发布给所有客户为止。我们使用管道来降低新代码更改对客户造成负面影响的风险。管道中的每个步骤都应进一步确保构建构件没有缺陷。如果缺陷影响到了生产,我们希望尽快使生产恢复正常状态。
 
Pipelines 上线之初,它只能为每个应用程序的单个发布流程进行建模。这种限制推动了团队发布流程的一致化、标准化和精简化。这样可以降低缺陷。在我们转向使用管道之前,团队通常会在修复错误和发布主要功能时分别使用不同的发布流程。当其他团队看到试行自动化交付的团队取得成功之后,他们也开始将其手动管理的发布流程迁移到管道中,从而提高一致性。过去采用多种发布流程的团队现在有了一个供所有人使用的标准化流程。此外,当团队成员将发布流程移动到工具中时,他们经常会重新审视自己的方法并想办法简化流程。
 
Pipelines 团队的年度目标是让自家产品的使用量增长情况达到“采用率喜人”的程度。 换句话说,他们需要打造出尽善尽美的产品,让人们能够自发选择使用该产品。我们统计了使用管道将软件部署到生产阶段的团队数量,并根据管道的自动化程度进行了分类。这些团队的目标是使用管道来发布软件并努力实现发布流程完全自动化。但是我们也注意到,在某些组织中,我们评估质量的方式可能会导致团队在未经任何测试的情况下直接采用自动化的发布流程。

“多少测试才足够?”这个问题的答案因人而异。它要求团队理解自身所处的运行环境。为了应对这种情况,我们采用了另一条领导力原则“主人翁精神”。此原则要求我们从长远考虑,不会为了短期业绩而牺牲长期价值。Amazon 的软件团队设置了很高的测试标准,并为此付出了大量努力,因为我们知道拥有一款产品意味着要承担该产品中任何缺陷所带来的后果。如果问题对客户造成了影响,将会有两个披萨规模的软件团队负责处理该问题并实时进行修复。提高执行速度与响应生产中出现的问题之间的紧张关系让团队有动力去进行充分的测试。但是,过度投资于测试未必是一件好事,因为这样会使我们的发展速度落后于竞争对手。我们一直致力于改进软件发布流程,同时避免其成为阻碍企业发展的因素。

我们面临的另一个问题是,各团队之间没有互相学习彼此的软件发布最佳实践。我们鼓励两个披萨规模的团队独立自主工作,这意味着工程师需要独立解决部署问题。当他们找到满足软件发布需求的解决方案时,会通过电子邮件、运营会议和其他沟通渠道,将相应技术推广给其他工程师。但这种沟通方式存在两个问题。首先,这些渠道是“尽力而为”式的沟通渠道,意味着并非所有人都能学到新技术。第二,领导者虽然鼓励下属团队采用新的最佳实践,但他们无法了解团队是否已进行了实际采用最佳实践所需的准备工作。我们意识到,我们需要让所有工程师都能获得我们所学到的最佳实践,还要让领导者学会辨别需要关注的管道。
 
我们的解决方案是,将对最佳实践的检验添加到我们用于构建和发布软件的工具中,实现最佳实践学习机械化。我们发现,一个组织的最佳实践可能并不适合其他组织,我们对此很重视,因此我们允许各组织根据自身实际情况来配置这些检验。组织级别的最佳实践检验让领导者能够调整发布流程以满足业务需求。领导者如要鼓励或强制要求团队采用新的最佳实践,可以从向工程师日常使用的工具中添加警告信息开始。通过在工具中添加消息,可以有效保证团队成员能够了解最佳实践以及该最佳实践将于何时生效。我们发现,通过给团队留出时间去学习和辩论新的最佳实践,组织可以有机会对最佳实践进行检验和改进。这一举措最终提高了最佳实践的质量,并获得了工程社区的认可。
 
我们系统性地确定了待施行的最佳实践。我们的一个顶尖工程师小组列出了导致无法正常发布的常见原因,并且确定了确保正常发布的步骤。然后,我们用该列表构建了一组最佳实践检验。通过这个过程,我们意识到,尽管我们希望新的软件版本能够快速轻松地交付给客户,同时又不会降低可用性,但我们还是将可用性放在了第一位,然后才是速度,让我们的工程师能够更轻松进行交付。

降低缺陷蔓延到客户的风险

我们的发布流程(包括管道和部署系统)必须设计为能够尽快识别缺陷,并防止缺陷对客户造成影响。为此,需要确保正确配置发布流程,并且构建构件能够按预期运行。

部署安全机制:部署测试的最基本形式,可确保新部署的构件正常启动并响应工作。作为部署后工作流程的一部分,我们运行快速检查,确保新部署的构件已启动并正常提供流量。例如,我们使用 AWS CodeDeploy AppSpec 文件中的生命周期事件挂钩来触发简单脚本,以停止、启动和验证部署。我们还会检查用于提供客户流量的容量是否充足。我们在 CodeDeploy 中构建了正常运行的最少主机数等技术,用于确保我们始终留足为客户提供服务所需的容量。最后,如果部署引擎检测到故障,系统应回滚更改,从而最大程度地缩短客户发现缺陷的时间。

生产前测试:Amazon 的最佳实践之一,可实现单元、集成和生产前测试自动化,并将这些测试添加到我们的管道中。我们坚持要执行负载和安全性测试,并倾向于将这些测试也添加到我们的管道中。这里的单元测试指的是您可能需要在构建计算机上执行的所有测试,包括代码格式检查、代码覆盖率和代码复杂度测试等。集成测试包括所有开箱即用型测试,例如故障注入和自动化浏览器测试等。关于单元测试和集成测试,已经有很多优秀的文章对其进行了详细的介绍,在此不再赘述。

单元测试和集成测试旨在验证构建构件的行为能够正确实现相应功能。我们执行的验证越多,缺陷暴露给客户的风险就越低。为了缩短将产品交付客户的时间,我们会尽力在发布过程中尽早发现缺陷。通常,这意味着如果您的测试规模较小且速度较快,就可以更快地收到有关更改问题的反馈。

在 Amazon,我们还使用一种称为生产前测试的技术。在将更改部署到生产环境中之前,生产前环境是测试的最后一站。生产前环境测试使用系统的生产配置,因此其行为与生产系统相差无几。这种方法有两个好处。首先,生产前环境通过测试生产配置,确保服务可以正确连接到所有生产资源,包括生产数据存储。其次,它确保系统与其所依赖的生产服务的 API 能够正确交互。生产前环境仅限拥有该服务的团队使用,并且绝不会向客户发送流量。运行生产前测试可有效确保相同的代码和配置能够在生产环境中正确运行。

生产中验证:当我们向客户发布代码时,不会一次性完成。一次性向所有客户发布代码会使代码中的缺陷影响的范围太大。于是,我们采用部署到单元(服务的完全独立实例)的方式。当向第一个单元中第一批客户部署更改时,我们非常谨慎。我们只会让少数客户看到新的更改,并且会收集有关新代码是否正常的反馈。我们会监控 Canary 版本部署后服务发出的错误数量。如果错误率上升,我们将自动回滚更改。例如,在继续部署之前,我们需要等待结果中出现 3000 个正常数据点,并且没有异常数据点。

如果您的自动化测试缺少使用案例,则可能会导致问题复杂化。我们努力通过结构化和可重复的测试(包括自动测试和手动测试)来捕获所有错误。但是,即使我们竭尽所能,还是难免会漏掉一些缺陷。为了测试这些测试,我们将生产中发生的新更改保留一段固定的时间,并观察非团队成员能否发现问题。我们花了很多时间来讨论是否应该让更改仅在生产中进行,或者在 Canary 版本部署之后应等待多长时间才能部署到其余部署组。我们的许多团队决定在进行例行部署之前,除了等待收集正常数据点外,再额外等待一段固定的时间。管道的等待时间高度取决于团队。一些团队会等待数小时,而另一些则只会等待几分钟。缺陷造成的影响越大,修复问题所需的时间就越长,发布流程也就越慢。

在第一个单元中的发布取得不错效果之后,我们将向越来越多的客户公开新的代码更改,直到完全发布为止。类似我们在 Canary 版本部署时的做法,我们等到对第一个新单元的部署情况良好之后,再进行下一个单元的部署。随着对构建构件的信心逐渐增强,验证代码更改所需的时间也随之减少。这样就形成了一种模式,这种模式的目标是尽可能缩短从代码签入到发布到第一位生产客户的时间。但是,在投入生产后,我们会将新代码慢慢地发布给客户,在逐步提高部署到其余客户的速度的同时,加强对我们工作的信心。

为确保我们的生产系统能够持续满足客户的需求,我们在系统上生成了合成流量。如果我们的服务无法正常运行,我们需要及时的反馈,因此我们至少每分钟运行一次合成测试。我们设计合成测试是为了确保运行中的流程都能正常运行,并且所有依赖项都经过了测试,而这通常需要测试所有面向公众的 API。

控制软件发布时间:为了控制软件发布的安全性,我们建立了一套机制,可让我们控制更改在管道中的移动速度。我们使用指标、时间窗口和安全检查来控制发布软件的时间。

可将管道配置为在指标发生更改时触发警报并阻止部署。管道中有大量的指标,并且针对系统、单元、可用区和区域的运行状况以及所有其他您能想到的项目都设置了警报。当重要指标触发警报时,我们会将管道配置为暂停部署代码。但是,有时团队需要部署修复程序,以解决系统警报。对于这种情况,团队会覆盖警报以防止更改在管道中移动。

我们的管道支持指定时间窗口,在指定的时段内允许更改在管道流程中前移。团队可以通过设置时间窗口,限制将更改发布给客户的时间。AWS 团队喜欢在有很多人可以快速响应并缓解由部署引起的问题的情况下发布软件。为此,AWS 团队通常会设置时间窗口,使得仅在工作时间内进行部署。Amazon 的其他团队则喜欢在客户流量较低时发布软件。如果需要,也可以覆盖这些时间窗口。

我们还支持根据构建构件的内容来暂停管道。例如,我们可以阻止包含已知错误包或特定 Git 引用的构建构件。当发现对包的更改导致性能下降时,我们便使用了此功能。如果只从包存储库中删除包,那么已经包含有缺陷的包的管道仍会将错误的更改部署给客户。

如何加快执行速度

我们发现各团队都非常希望实现自动化。我们全心全意地构建并向客户发布功能,旨在提高客户的生活品质。持续交付让这一目标具有了可持续性。自动化替代了恼人、易出错又费时费力的手动工作,让工程师节省了时间。事实表明,持续部署有助于提升质量。自动化让团队可以频繁地一次发布一项更改,从而更容易识别回归情况。

当采用新系统时,大多数团队成员通常都能够了解待测试的范围,降低了一些手动测试的难度。但是,随着系统日益复杂以及团队成员的变动,自动化也变得愈发重要。我们喜欢让我们的系统实现自动化,如此一来,我们可以专注于为客户创造更多价值,而不用花时间手动管理将更改发布给客户的流程。

多年来,Amazon 一直在执行持续改进计划,并将重点放在向客户发布软件的速度和发布的安全性上。持续改进工作并非以本文中提到的一系列风险检查和测试开始。随着时间推移,我们发明了识别和降低风险的方法。

持续改进计划由组织中不同级别的业务领导者负责。各业务领导者可以调整其软件发布流程,平衡风险和对业务的影响。我们的一些持续改进计划跨 Amazon 的大量部门运行,有时一些规模较小的组织的领导者也会运行自己的计划。我们知道规则总是有例外。我们的系统具有选择性退出机制,因此我们不会降低需要永久或临时例外情况的团队的速度。归根结底,我们的团队对自己软件的行为负责,同时也负责在软件发布流程中进行适当的投入。

我们从分析问题的根源开始,解决问题并进行迭代。为了使这项工作实现可持续化,我们需要将工作逐步铺开,并随着时间的推移不断改进。Amazon 最初开始使用管道时,许多团队不确定持续部署是否会奏效。为了让团队接纳这一方式,我们鼓励团队在管道中对当前发布流程、手动步骤以及所有内容进行编码。对于许多团队来说,其管道充当着发布流程的可视化界面,而不会通过发布流程自动提升构建构件。随着各团队信心的增强,他们逐渐在管道的不同阶段启用自动化,直到完全淘汰管道中需要手动触发的步骤。

视线回到今天。如今,Amazon 团队在编写新代码时就力图实现完全自动化。对我们来说,自动化是我们得以持续发展业务的唯一途径。
 


关于作者

Mark Mansour 是 Amazon Web Services 的软件开发高级经理。他于 2014 年加入 Amazon,从事过重点针对软件部署的各种内部和外部服务,以及根据规模需求持续交付软件方面的工作。工作之余,Mark 喜欢研究手表、玩桌游和打高尔夫球。

确保部署期间安全回滚