大多数故障都始于一次部署。代码通过了评审和测试,然后遇上了真实的流量、真实的数据和真实的依赖。部署策略决定了有多少流量会同时遇上新版本、你多快能发现问题,以及多快能撤销它。
本文对比大多数团队会在其中做选择的三种策略:滚动更新、蓝绿部署和金丝雀发布。文中展示每种策略下流量如何移动、回滚是什么样子,最后给出一张金丝雀发布的架构图,其中继续还是回滚的决定是自动做出的。
滚动更新
每次替换几个实例,逐步把旧版本换成新版本。Kubernetes Deployment 默认就是这样做的:在 RollingUpdate 策略 中,maxSurge 和 maxUnavailable 控制发布过程中允许多出和缺少多少个 Pod,两者默认都是 25%。
- 流量: 发布期间,两个版本同时处理请求,比例取决于各自的 Pod 数量。
- 回滚: 再做一次滚动更新,回到旧版本。需要几分钟,而不是几秒。
- 成本: 几乎没有额外成本,最多是 surge 的那部分容量。
- 风险: 任何用户都可能很早就碰到新版本,而唯一的自动关卡是就绪探针。一个启动正常、却在负载下失败的版本会被完整地发布出去。
对于低风险的变更,以及测试和探针都完善的内部服务,滚动更新是合适的默认选择。让就绪探针检查服务正常工作所需的一切,而不只是进程是否在运行;否则发布过程会若无其事地用坏掉的 Pod 替换健康的 Pod。
蓝绿部署
运行两套完整的环境。蓝环境服务生产流量;绿环境部署新版本,并在原地测试。然后由负载均衡器或 DNS 一次性把全部流量从蓝切到绿。Martin Fowler 对 蓝绿部署的描述 是经典参考。
- 流量: 先全部在蓝,再全部在绿。不会同时在两边,至少不会持续很久。
- 回滚: 切回仍在运行的蓝环境。只需几秒。
- 成本: 两套环境同时运行期间需要双倍容量。
- 风险: 切换会一次性移动 100% 的用户,所以只在真实流量下才出现的问题会影响所有人。数据库是共享的,因此模式变更必须同时兼容两个版本。
金丝雀发布
把一小部分真实流量(比如 5%)发往新版本,与稳定版本对比;如果表现良好,再逐步扩大比例。这个名字来自煤矿里的金丝雀;参见 Fowler 网站上的 金丝雀发布文章。
- 流量: 按权重、请求头或用户分组来划分。
- 回滚: 把金丝雀的权重设为零。几秒即可完成,而且只有百分之几的用户受到影响。
- 成本: 几个额外的 Pod,加上流量划分和分析的工具。
- 风险: 只要盯住正确的指标,就是三者中最低的。没人盯着的金丝雀,只是一次缓慢的滚动更新。
横向对比
| 滚动 | 蓝绿 | 金丝雀 | |
|---|---|---|---|
| 接触到坏版本的用户 | 逐步增加,最多全部 | 全部,一次性 | 一小部分 |
| 回滚时间 | 几分钟 | 几秒 | 几秒 |
| 额外容量 | 仅 surge | 切换期间 100% | 几个 Pod |
| 是否需要流量划分 | 不需要 | 一个切换开关 | 加权路由 |
| 数据库变更 | 向后兼容 | 向后兼容 | 向后兼容 |
最后一行在三种策略中都一样,这是有原因的:无论哪种策略,两个版本的代码总会在某个时刻面对同一个数据库。使用“先扩展、后收缩”(expand-and-contract)的迁移方式:先添加新的列和表,部署同时兼容两种结构的代码,再在之后的版本中移除旧结构。
金丝雀之前的影子流量
有些团队会在金丝雀之前加一步:把一部分生产请求复制给新版本,并丢弃它的响应。Envoy 和 Istio 都可以这样镜像流量。这能在完全不影响用户的情况下发现崩溃和延迟退化,但只适用于读请求和幂等请求。被镜像的写请求会重复产生副作用,比如邮件、支付和库存变动,所以要把它们排除在镜像之外,或者让影子版本连接沙箱中的依赖。
自动化金丝雀决策
金丝雀的价值来自对比,而人并不擅长盯着仪表盘看一个小时。Argo Rollouts 和 Flagger 等工具在 Kubernetes 上把这件事自动化:它们分步调整流量,在每一步之间查询你的指标提供方,并根据你定义的阈值决定提升还是中止。
好的分析指标数量少,而且贴近用户:
- 金丝雀的错误率(5xx 响应),与稳定版本对比。
- 高百分位的延迟,比如 p99。
- 如果可能,再加一个业务信号,比如每分钟完成的结账数。
拿金丝雀和稳定版本对比,而不是和一个固定数值对比,这样某个依赖状态不佳的夜晚就不会让每次发布都失败。
架构图
图表内容
- CI pipeline
- Rollout controller
- Users
- Ingress
- Stable v1
- Canary v2
- Metrics
- Canary within thresholds?
- Next step: 25, 50, 100 percent
- Abort: all traffic to v1
- image v2
- 95%
- 5%
- error rate, p99 latency
- yes
- no
- set weights
这张图的读法如下:
- 上方,流量路径: 用户到达 Ingress,Ingress 把 95% 的请求发往稳定的 ReplicaSet,5% 发往金丝雀。两者都向指标系统上报数据。
- 下方,控制回路: CI 把新镜像交给发布控制器。控制器设置 Ingress 的权重(虚线连线),读取两个版本的错误率和 p99 延迟,然后做出决定。
- 决策: 在阈值之内就进入下一步;超出阈值就中止,并把全部流量切回 v1。
- Markdown 块 列出了各个步骤和中止规则,这些内容应当纳入每个服务发布配置的评审。
功能开关是第四种策略
部署策略控制的是哪些服务器运行新代码。功能开关(feature flag)控制的是哪些用户看到新行为,与部署无关。频繁发布的团队会把两者结合起来:用滚动更新或金丝雀部署,把有风险的功能放在开关后面,再逐步打开开关。开关是更快的回滚方式,因为不需要重新部署任何东西。
部署图上应该画什么
- 连线上的流量百分比。 它们在发布过程中会变化;画出第一步即可。
- 谁来修改权重。 控制器、流水线还是某个人。
- 中止条件。 写数字,而不是“看起来不对就停”。
- 数据库, 并注明模式变更采用先扩展、后收缩的方式。
CI/CD 流水线图 展示了在此之前的构建和测试阶段,Kubernetes 架构图 介绍了集群本身,可观测性架构图 则展示了指标从何而来。
在 Archboard 中构建
- 使用 Google 或 GitHub 登录,并新建一张图。
- 添加一个 人员 表示用户,再添加 Kubernetes 节点包中的一个 Ingress 和两个 ReplicaSet 节点,分别表示稳定版和金丝雀。
- 添加 DevOps 节点包中的 指标 和一个 流水线,再添加 C4 模型节点包中的一个 容器 表示发布控制器。
- 添加流程图节点包中的一个 判断、一个 处理 和一个 开始 / 结束,表示提升和中止。
- 在 Ingress 的连线上标注流量权重,用虚线画出权重的调整,再添加一个基础节点包中的 Markdown 块,写明各个步骤。
这个示例共用 11 个图表节点,在 Free 套餐的限制之内。
常见问题
哪种部署策略最好? 取决于风险和工具。滚动更新适合大多数低风险变更;金丝雀适合坏版本代价高昂的面向用户的服务;蓝绿适合需要瞬间完整切换、并且负担得起双倍容量的系统。
蓝绿部署和金丝雀发布有什么区别? 蓝绿部署一次性切换全部流量,并保留旧环境以便即时回滚。金丝雀先移动一小部分流量,只有在新版本证明自己之后才扩大比例。
部署期间如何处理数据库迁移? 让每次模式变更都向后兼容:先扩展,部署同时兼容两种结构的代码,再在之后的版本中收缩。
金丝雀分析应该使用哪些指标? 与稳定版本对比的错误率和高百分位延迟,如果能快速测量,再加一个业务指标。
功能开关算一种部署策略吗? 它是一种发布策略。它把开启功能和部署代码解耦,并且能与三种部署策略中的任何一种很好地配合。