不在每次合并(merge)时就部署到生产环境(prod),简直近乎精神错乱 http://x.com/i/article/2085419327348711424


每次变更都部署到生产环境(prod)可能令人害怕,但不直接部署到生产环境更可怕。
这是我在 Amazon 带领一个团队为数亿客户部署时所采用的流程。通过我的咨询工作,我曾带领工程团队从每两周一次的定时发布,转变为每次合并(merge)即交付。
让我们从最显而易见的地方说起:
你一定会引发故障(outage)。这不是“会不会”的问题,而是“何时”的问题。
无论多少单元测试(unit testing)、集成测试(integration testing)、内部试用(dogfooding)、端到端(end-to-end)测试,或是向部署之神献祭,都无法抓住每一个 bug。
一周前针对你的功能所做的测试,并没有覆盖你队友最新的变更。
你的测试是针对队友服务的预生产(pre-prod)版本进行的,而那个版本现在已经发生了变化,并且包含了一项不向后兼容的变更。
等待的时间越长,一次发布中堆积的变更就越多。一旦需要回滚(rollback),你就得回滚两周的变更,而不是一两个小时的工作量。
如果我们接受故障不可避免这一前提,那么将大量资源投入到对一次发布进行 QA 就远没有那么合理了;相反,我们应该把资源集中在监控与观测(monitoring & observing)发布上,并做好在故障发生时应对它的准备。
现在,来说说我们如何做到:
先决条件(Prerequisites)
CI/CD
测试的重要性远比你想象的要低。测试无法证明你的变更在生产环境中是安全的,什么都证明不了。测试的作用在于让失败的成本变得低廉。在 CI 中发现的 bug 只花费几分钟;而在生产环境中发现的 bug 会耗费你一个晚上去做回滚(rollback)。
因此,每次合并时都要运行完整的测试套件,或至少将其作为流水线(pipeline)的一部分:单元、集成、端到端测试。Bug 在流水线中走得越远,解决它的成本就越高。
监控/可观测性(Monitoring/Observability)
关键在于尽可能快地发现回归(regression)。要做到这一点,你需要出色的监控(monitoring)。它具体表现为:
- 指标(metrics):错误、延迟、可用性
- 日志,附带关联 ID(correlation ids)
- 基于上述两项的 sev-3 和 sev-2(Paging)告警
调整告警阈值(alarm thresholds)既是一门艺术,也是一门科学。你需要在灵敏度与对真实事件的响应速度之间取得平衡。你的 sev-2 告警目标时间应为 5 到 10 分钟。
一开始,你很可能会设置错误,也可能过于敏感。不幸的是,这主要靠反复试错(trial and error)来掌握,所以最初你可能会在凌晨两点被叫醒几次。
功能开关(Feature Flags)
对于任何有风险的功能变更,你都应该将其放在功能开关(feature flag)/ 远程配置(remote config)之后发布。功能开关让你能在几分钟内回滚并关闭某项变更,而不必回滚整个部署。此外,如果你的功能开关服务支持(它应该支持),你可以按百分比或按用户群(cohort)逐步推出该功能,从而进一步降低不良变更的影响。
这使我们可以将代码的部署(deployment)与代码的激活(activation)解耦。这很微妙,但对于降低风险却是颠覆性的改变。
注意:你需要一个清理这些开关的流程。理想情况下,每创建一个开关,就要创建一张移除工单(removal ticket)。否则,当你的功能开关服务宕机时(它一定会宕机),你就会遭遇严重的回归(regression)。别问我是怎么知道的。
自动回滚(Automatic Rollback)(部署时熔断器(Deploy time circuit breaker))
部署时熔断器(Deploy time circuit breaker)是一种功能,允许你在向整个集群(fleet)滚动推出部署时,如果看到一定数量的错误或一定百分比的错误,就回滚该部署。现在大多数云提供商都只需勾选一个复选框就能提供这个功能。
向后兼容的变更(Backwards Compatible Changes)
你本来就应该这样做,但每次提交即部署会强制你践行这一点。在滚动部署(rolling deployment)期间,旧版本和新版本会同时运行。每一项变更都必须与上一个版本协同工作。你那种在午夜部署以避开这个问题的花招不再奏效了。
部署策略(Deployment Strategies)
现在,有了这些基础,我们可以来看几种不同的部署策略(deployment strategies),它们能帮助你在推出我们的变更时降低风险。
单机(金丝雀)(One box (canary))
单机部署(One box deployment)将你的变更部署到大规模集群(fleet)中的一台机器(box)上。这样可以将任何不良变更的影响范围缩小到仅一台主机(host)。
你完成部署后,让它运行一段时间,承接整体流量中的一小部分。你需要在这台机器上配置好监控和告警(monitoring and alerting),一旦出现问题就会触发告警。
滚动部署(Rolling Deployments)
滚动部署(Rolling Deployment)允许你随着时间按百分比逐步推出,这样一旦出现灾难性错误,你就能在它影响所有机器之前发现,然后我们可以开始将它们回滚。
区域渐进发布(Regional Rollout)
随着公司发展,你最终会拥有多区域(multi-region)部署。与其同时向所有区域部署,不如先部署到某个特定区域(通常是流量最低的区域)。
不适用的场景
App Store
发布移动应用与这套指导并不完全兼容。应用商店(App Store)的审核队列会限制你的部署节奏(deployment cadence),因此需要不同的策略。
认证环境(Certified Environments)
医疗设备、航空电子设备、工业控制等。如果你需要监管机构来认证构建版本,就无法进行持续部署(continuously deploy)。
本地部署 / 自托管(On-prem / Self-hosted)
你无法控制升级过程。你仍然可以对你运营的所有系统进行持续部署,但你仍然必须为每次变更标记版本(version),而由你的客户决定何时采用。
从哪里开始
不要一次性做完所有事情。顺序很重要:
- 让 CI 保持绿色且快速。理想情况下控制在 15 分钟以内
- 针对错误率、延迟和可用性设置指标(metrics)和告警(alarms)。这是整个实践中最重要的一部分
- 将任何有风险的功能变更都放到开关(flag)后面
- 添加单机(one-box)+ 自动回滚(automated rollback)
- 删掉发布日历(release calendar)
- 既然你不再安排发布日程了,找点别的事情来利用那些多出来的时间吧
我合作过的大多数团队大约需要一个季度才能走完这套流程。工具部分是最简单的。组织流程,以及打破“定时发布很安全”这一幻觉,才是困难的部分。
如果你的团队还在用发布日历(release calendar),并且想要摆脱它,那正是我从事的工作。DM 我。