云原生部署策略 — 零停机发布的六种打法
云原生部署策略 — 零停机发布的六种打法
这一章讲什么: 版本要天天发,用户要无感知——这个矛盾怎么解。 答案是六种发布策略,每一种都是「新版本怎样见到用户」的一种不同安排: 有的慢慢替换,有的整批切换,有的先只给 5% 的用户试。 本章把每种策略的机制、取舍、K8s 实现和翻车的姿势全部讲透, 最后收在一个真实的金丝雀自动回滚故事里。
1. 主线例子:一个欺诈检查功能的上线
本章的主线例子,是原书结尾的完整实战:给支付服务加一个新的欺诈检查。
第 1 步:不是直接推给所有人,而是金丝雀:10% 真实流量走新版,90% 留旧版
第 2 步:前几分钟一切正常
第 3 步:几分钟后,仪表盘讲故事了:
金丝雀的错误率在缓慢爬升,支付成功率比平时略差
——不算灾难,但越过了事先约定的 SLO 阈值
第 4 步:阈值是提前接进发布流水线的——
系统自动停止推进,把流量全部拉回稳定版
没有半夜被叫起来,没有手工回滚,用户几乎无感
第 5 步:事后翻日志、修 bug、重发
图说:渐进式交付的全部意义就在第 4 步——
「回滚决策」在 calm time(平静时)做成规则,而不是在 incident 时临时靠人判断。
比例与流程沿原书;具体错误率数值原书未给,属于「越过阈值」的量级。要理解为什么这套能成立,先要有两个地基:应用长什么样(12-factor)才有资格玩这些策略, 以及策略共用的三个前提(兼容性、冗余、健康检查)。然后从最简单的策略一路讲到金丝雀, 最后是支撑它们的两个底座:服务网格与密钥管理。
2. 12-factor:入场资格证
12-factor app 是 Heroku 工程师提出的十二 SaaS 开发准则,十多年后依然是云原生的 「应用与基础设施之间的基线契约」1。十二条因子压缩成六条主线(全表照原书):
| 主线 | 覆盖的因子 | 核心要求 |
|---|---|---|
| 一份代码,多处部署 | Codebase | 所有环境出自同一个版本库 |
| 依赖显式声明 | Dependencies | requirements.txt / package.json + 容器化 |
| 配置在环境里 | Config | 密钥和环境特定值不进代码,运行时注入 |
| 后端服务是可换的 | Backing services | 数据库、队列、缓存换个实现不该改代码 |
| 构建/发布/运行三段分开 | Build, release, run | GitOps 是它的现代延伸 |
| 无状态、可抛弃、可并发 | Processes / Port binding / Concurrency / Disposability / Dev-prod parity / Logs / Admin processes | 状态外置、端口自包含、日志当事件流、管理任务一次性进程 |
原书给的 Node.js 例子(照抄结构):数据库连接的全部参数从 process.env.* 读,
K8s 里用 secretKeyRef 从 Secret 注进环境变量——同一份镜像,三种环境三种配置,
代码一行不改2。
3. 六种部署策略
3.1 重建(Recreate):全杀再起
全部旧 Pod 杀掉,再起新的。用户吃到停机时间——唯一一种故意有停机的策略。 适用:内部系统、凌晨没人用的请假系统3。
3.2 滚动(Rolling):默认档,分批换
K8s 默认策略:一次换一小批(maxUnavailable: 1 表示最多一个不可用、
maxSurge: 1 表示最多多出一个),服务全程可用4。
但滚动有一个硬前提,原书强调为最关键的要求:
前后向兼容性。 滚动期间新旧版本同时在跑、互相通信——新版要读得了旧版写的数据 (后向),旧版也要读得了新版的(前向)。数据 schema 变更是最常见的翻车点, 解法是 expand-migrate-contract 三步5:
Expand: 先加新列/新表,不破坏旧版(只增不改)
Migrate: 新版双读双写——读新旧两处,写也写两处
Contract: 全部实例都跑新版后,才删旧列旧表
图说:三步把「一次危险的 schema 变更」拆成「三次安全的变更」。
代价是中间态要维护两版并存——这是用工程量换零停机。3.3 蓝绿(Blue-Green):两套全开,一键切换
两套一模一样的生产环境:蓝(当前)和绿(新版)。绿全套部署好、冒烟测试通过后, 把流量开关从蓝拨到绿——原书的实现是改 Service 的 selector(一行 kubectl patch)。 回滚即时:把流量切回蓝即可6。
代价与三个坑7:
- 双倍基础设施成本(两套都在跑);
- 数据兼容性:schema 变更仍要走 expand-migrate-contract,两套共用同一个数据库时尤其;
- 会话陷阱(原书专门点名):会话存在应用内存里,切换瞬间全体用户被登出、 购物车清空——要么应用无状态,要么会话放共享存储(Redis),要么挑低峰切。
配套纪律:切换点要单一(Service selector / Ingress / LB),切换前对绿跑冒烟与回归 并接进 CI 门禁,SLO 一退立刻回拨。
3.4 金丝雀(Canary):小流量试错,逐步放量
先给 5–10% 流量,健康就 10% → 25% → 50% → 100%;任何一步 SLO 破了就退回。 两个实现(照原书)8:
- Istio(服务网格,见 §4):
VirtualService按权重 90/10 拆分,DestinationRule定义 v1/v2 两个版本子集; - NGINX Ingress(不用网格):注解
canary: "true"+canary-weight: "10"。
成功前提六条:渐进暴露(起步 1–10%)、细粒度流量控制、 SLO 门与自动回滚(每一步都挂在延迟/错误率/业务 KPI 上)、 版本偏斜兼容(canary 和 stable 的数据/API 要双向兼容,写操作要幂等)、 可观测性(canary 对照组比指标)、流水线自动化(权重调整走 PR + 策略检查)9。
3.5 影子(Shadow):复制流量,不应答
新版与生产并行,但真实请求被镜像一份给它——响应永远由稳定版发回, 影子版只默默处理、用于验证。零用户影响,适合压测、性能验证、ML 模型对照10。
两个硬要求(原书点出而容易忽略):影子实例要有独立数据库或写抑制——
否则一笔订单被写两遍;对比观测:拿影子和稳定的日志/延迟/错误做平行对照。
Istio 实现:mirror: 字段 + mirrorPercentage: 10011。
3.6 A/B 测试:不是发布策略,是实验策略
两个版本同时服务真实用户,按 header/cookie/特性开关路由
(原书例子:X-Experiment: B 的走实验版),目的是测用户行为与业务指标
(转化率、点击),而不是系统稳定性。工程与产品共同负责;
需要埋点、分流一致性(用 LaunchDarkly/Unleash/Flipt 这类特性开关服务)、
统计显著性(实验要跑到置信度再合并优胜版)12。
3.7 六策略对照
| 策略 | 用户感知 | 回滚速度 | 成本 | 何时用 |
|---|---|---|---|---|
| 重建 | 停机 | 慢(重新部署) | 最低 | 内部系统、可停机 |
| 滚动 | 无 | 中(重新滚) | 低 | 默认;前后兼容时 |
| 蓝绿 | 无(会话处理好) | 即时(流量切回) | 双倍基础设施 | 要即时回滚的关键系统 |
| 金丝雀 | 无 | 快(自动) | 低(只多 5–10%) | 高风险变更的日常默认 |
| 影子 | 完全无 | 不适用(没接流量) | 额外算力 + 写隔离 | 压测、模型验证 |
| A/B | 无 | 按分流回退 | 中等 | 业务实验,不为稳定性 |
4. 服务网格:不动代码的流量调度层
服务网格(service mesh):专门管服务与服务之间通信的基础设施层。 机制:每个 Pod 旁边注入一个轻量代理(通常是 Envoy),进出流量都过它—— 一堆代理组成数据面,中央的 Istio / Linkerd 管配置、策略、遥测(控制面)13。
它能给的(都不改应用代码)14:
- 统一的流量控制:路由、重试、熔断、限流;
- 安全:mTLS(双向 TLS:服务间通信自动加密加身份认证);
- 深度可观测:每个请求路径的标准指标和追踪;
- 韧性:熔断器、故障注入(故意注错做演练);
- 渐进交付的原生支持:金丝雀分流、A/B 正是靠它(§3.4 的 Istio 例子就是 mesh 配置)。
原书的 80/20 例子(照抄):VirtualService 把 payments 的流量 80% 给 v1、20% 给 v215。
判断(我们的,不是书里的): 本章六策略的选择,真正的变量只有两个: 变更有多危险(决定要不要金丝雀/影子)和回滚要多快(决定蓝绿还是滚动)。 A/B 混进这张表其实类别不同——它是实验方法不是发布方法,只是共享同一套分流设施; 把「验证系统」和「验证用户」两类目标分开,选型就不会纠结。 如果错,会错在: Serverless/函数平台上,这些策略大半由平台内建 (流量拆分是托管能力),「选策略」退化成「配平台」,本章的 K8s 中心化视角会失真。
5. 密钥与配置:分开放,都要管
这一节把第 8 章的 ConfigMap/Secret 展开成生产实践16。
配置管理(非敏感值):ConfigMap 存运行参数(端口、日志级别、各功能的启停), 五条例程——配置进 ConfigMap 不进镜像、按环境分 namespace、 清单进版本库但不含敏感值、用 Reloader 之类工具改完自动热加载、 部署前做 schema 校验(admission webhook)。
密钥管理(敏感值)四条理由必须正经做:凭证嵌在代码或镜像里极易被拖走; GDPR/PCI DSS/SOC 2 合规强制要求;多环境必须隔离; 流水线要动态取密钥且不能泄进日志17。七条控制18:
- 集中存储:AWS Secrets Manager / Google Secret Manager / HashiCorp Vault / Azure Key Vault;
- 接进 K8s:External Secrets Operator 或 Sealed Secrets 把外部密钥同步成 K8s Secret;
- 静态加密 + 传输加密(etcd 加密、全程 TLS);
- RBAC 细粒度权限;
- 自动轮换:定期换密钥;
- Git 里只放引用或加密占位符,永不放明文;
- ConfigMap 与 Secret 分开引用,边界清晰。
对照表(原书):配置 = 端口/功能启停/URL,常变进版本库;密钥 = token/证书, 加密、禁入版本库、定期轮换19。
6. 生产流水线:把上面所有东西串起来
成熟环境的发布是一条七段流水线,每段自动、可追、可逆20:
提交 → 构建+测试(含漏洞扫描)→ 推镜像仓库(ECR/Harbor)
→ CD 工具(Argo CD/Flux)应用 Helm/Kustomize 到预发
→ 冒烟 + 金丝雀放 10%
→ SLO 达标后晋升生产
→ 部署后持续验证(监控 + 回滚钩子)
图说:每个「晋升」都是一道闸;闸的标准是事先写下的指标,不是值班人的临场判断。密钥与配置在部署时动态注入(原书给了 Jenkinsfile 用 withCredentials 包住
三条 kubectl apply 的例子——凭证不进脚本)21。渐进交付由 Flagger、
Argo Rollouts 这类控制器自动化:流量自动搬、指标自动看、SLO 一退自动回22。
Argo CD 的 Application 声明式例子收尾:selfHeal: true 让集群永远追 Git23。
7. 边界与局限
- 重建策略几乎不该在云上出现——本章收录它只为对照;真实世界里连内部系统 也多数用滚动。
- 影子部署的写隔离只给了原则:双写怎么抑制、影子读什么数据,生产实现远比 两句话说到的复杂。
- 服务网格的成本面没讲:sidecar 的 CPU/内存开销、运维复杂度, 在中小规模常常 outweigh 收益,本章立场是「上了规模就该上 mesh」。
- A/B 的统计部分极浅(一句「跑到显著」),实验设计真正的方法论(样本量、 多重检验)需要别的材料。
- 会话粘滞(sticky session)与蓝绿的冲突提了一句,没有给「必须粘滞的老系统」 的迁移路径。
8. 可带走的
- 零停机的前提是 12-factor 式应用:无状态、配置在环境、日志是事件流;
- 滚动更新的命门是前后向兼容;数据库变更走 expand-migrate-contract 三步;
- 蓝绿 = 两套全开 + 一次流量切换;回滚即时,但双倍成本、会话陷阱要吃透;
- 金丝雀 = 小流量 + SLO 门 + 自动回滚;起步 5–10%,Istio 或 NGINX 注解都能做;
- 影子 = 镜像流量不应答;独立数据库/写抑制是红线;
- A/B 测用户行为不是系统稳定性,别拿它当发布策略;
- 服务网格 = sidecar 代理 + 控制面;不改代码获得 mTLS、流量控制、可观测;
- 密钥:集中存、动态取、自动轮换、Git 无明文;配置与密钥分开管;
- 回滚决策要在平静时写成规则(阈值接进流水线),不要留到事故时靠人。
9. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 12-factor 十二条 | Introduction(第 9 章) | text/27-fm-introduction.txt:90(搜「Codebase」) · text/27-fm-introduction.txt:293(搜「baseline contract」) |
| 环境变量配置例子 | Introduction(第 9 章) | text/27-fm-introduction.txt:215(搜「process.env.DB_USER」) |
| 滚动与兼容性三前提 | Introduction(第 9 章) | text/27-fm-introduction.txt:344(搜「maxUnavailable」) · text/27-fm-introduction.txt:411(搜「Backward compatibility」) |
| expand-migrate-contract | Introduction(第 9 章) | text/27-fm-introduction.txt:424(搜「Introduce」) · text/27-fm-introduction.txt:439(搜「expand, migrate, contract」) |
| 蓝绿与切换命令 | Introduction(第 9 章) | text/27-fm-introduction.txt:462(搜「two identical environments」) · text/27-fm-introduction.txt:613(搜「kubectl patch」) |
| 会话陷阱 | Introduction(第 9 章) | text/27-fm-introduction.txt:659(搜「forced logouts」) |
| 金丝雀与 Istio/NGINX 例子 | Introduction(第 9 章) | text/27-fm-introduction.txt:668(搜「5 to 10%」) · text/27-fm-introduction.txt:726(搜「VirtualService」) · text/27-fm-introduction.txt:786(搜「canary-weight」) |
| 影子部署与镜像 | Introduction(第 9 章) | text/27-fm-introduction.txt:872(搜「dark launch」) · text/27-fm-introduction.txt:941(搜「mirrorPercentage」) |
| A/B 与特性开关 | Introduction(第 9 章) | text/27-fm-introduction.txt:973(搜「split testing」) · text/27-fm-introduction.txt:1087(搜「LaunchDarkly」) |
| 服务网格架构 | Introduction(第 9 章) | text/27-fm-introduction.txt:1115(搜「sidecar proxy」) · text/27-fm-introduction.txt:1167(搜「80% of requests」) |
| 密钥管理七控制 | Introduction(第 9 章) | text/27-fm-introduction.txt:1321(搜「Centralize secret storage」) · text/27-fm-introduction.txt:1333(搜「Automate rotation」) |
| 七段流水线与金丝雀实战 | Introduction(第 9 章) | text/27-fm-introduction.txt:1483(搜「Code commit」) · text/27-fm-introduction.txt:1575(搜「new fraud check」) |
| Argo CD Application 例子 | Introduction(第 9 章) | text/27-fm-introduction.txt:1605(搜「payments-service」) · text/27-fm-introduction.txt:1647(搜「selfHeal」) |
Footnotes
-
出处:「Introduction」(第 9 章)第 84 段(
text/27-fm-introduction.txt:84,搜「Heroku」)与第 293 段(text/27-fm-introduction.txt:293,搜「baseline contract」)。 ↩ -
出处:「Introduction」(第 9 章)第 203–287 段(
text/27-fm-introduction.txt:215,搜「process.env.DB_USER」;text/27-fm-introduction.txt:251,搜「secretKeyRef」)。 ↩ -
出处:「Introduction」(第 9 章)第 311–335 段(
text/27-fm-introduction.txt:335,搜「HR leave application」)。 ↩ -
出处:「Introduction」(第 9 章)第 338–376 段(
text/27-fm-introduction.txt:344,搜「maxUnavailable」)。 ↩ -
出处:「Introduction」(第 9 章)第 411–439 段(
text/27-fm-introduction.txt:411,搜「Backward compatibility」;text/27-fm-introduction.txt:439,搜「expand, migrate, contract」)。 ↩ -
出处:「Introduction」(第 9 章)第 459–617 段(
text/27-fm-introduction.txt:462,搜「two identical environments」;text/27-fm-introduction.txt:613,搜「kubectl patch」)。 ↩ -
出处:「Introduction」(第 9 章)第 629–652 段(
text/27-fm-introduction.txt:629,搜「Environment parity」;text/27-fm-introduction.txt:649,搜「sticky sessions」)。 ↩ -
出处:「Introduction」(第 9 章)第 665–838 段(
text/27-fm-introduction.txt:668,搜「5 to 10%」;text/27-fm-introduction.txt:726,搜「VirtualService」;text/27-fm-introduction.txt:786,搜「canary-weight」)。 ↩ -
出处:「Introduction」(第 9 章)第 840–866 段(
text/27-fm-introduction.txt:850,搜「Progressive exposure」;text/27-fm-introduction.txt:856,搜「SLO-based gates」)。 ↩ -
出处:「Introduction」(第 9 章)第 869–885 段(
text/27-fm-introduction.txt:872,搜「dark launch」;text/27-fm-introduction.txt:884,搜「double-writes」)。 ↩ -
出处:「Introduction」(第 9 章)第 947–966 段(
text/27-fm-introduction.txt:957,搜「separate databases」;text/27-fm-introduction.txt:941,搜「mirrorPercentage」)。 ↩ -
出处:「Introduction」(第 9 章)第 970–1099 段(
text/27-fm-introduction.txt:973,搜「split testing」;text/27-fm-introduction.txt:1093,搜「Statistical significance」)。 ↩ -
出处:「Introduction」(第 9 章)第 1112–1135 段(
text/27-fm-introduction.txt:1115,搜「sidecar proxy」;text/27-fm-introduction.txt:1131,搜「mTLS」)。 ↩ -
出处:「Introduction」(第 9 章)第 1138–1158 段(
text/27-fm-introduction.txt:1148,搜「mutual TLS」;text/27-fm-introduction.txt:1154,搜「fault injection」)。 ↩ -
出处:「Introduction」(第 9 章)第 1167–1218 段(
text/27-fm-introduction.txt:1206,搜「weight: 80」)。 ↩ -
出处:「Introduction」(第 9 章)第 1281–1304 段(
text/27-fm-introduction.txt:1301,搜「Reloader」)。 ↩ -
出处:「Introduction」(第 9 章)第 1253–1276 段(
text/27-fm-introduction.txt:1269,搜「GDPR」;text/27-fm-introduction.txt:1275,搜「dynamically」)。 ↩ -
出处:「Introduction」(第 9 章)第 1321–1339 段(
text/27-fm-introduction.txt:1321,搜「Centralize secret storage」;text/27-fm-introduction.txt:1336,搜「Avoid plaintext in Git」)。 ↩ -
出处:「Introduction」(第 9 章)第 1442–1462 段(
text/27-fm-introduction.txt:1458,搜「stored in version control」)。 ↩ -
出处:「Introduction」(第 9 章)第 1480–1501 段(
text/27-fm-introduction.txt:1483,搜「Code commit」;text/27-fm-introduction.txt:1495,搜「Canary release」)。 ↩ -
出处:「Introduction」(第 9 章)第 1513–1546 段(
text/27-fm-introduction.txt:1519,搜「withCredentials」)。 ↩ -
出处:「Introduction」(第 9 章)第 1549–1584 段(
text/27-fm-introduction.txt:1552,搜「Flagger」;text/27-fm-introduction.txt:1581,搜「shifts traffic back」)。 ↩ -
出处:「Introduction」(第 9 章)第 1587–1650 段(
text/27-fm-introduction.txt:1647,搜「selfHeal」)。 ↩