专业书籍精读 · 持续交付 · 第 10 章
Continuous Delivery · Ch 10 · Jez Humble & David Farley · 2010
你手机里的 App、你每天打开的网页,背后每周甚至每天都在悄悄换新版本。这一章讲的就是怎么换——怎么把新版本推到几千台机器上而不惊动任何一个正在用的人;以及更要紧的那半句:推错了,怎么在两分钟内退回去。
过去发新版本像餐厅打烊装修:晚上十点挂出「暂停营业」,一群人熬到凌晨三点,第二天开门祈祷别出事。这一章教的是另外两招。
一招叫蓝绿:不关门,在隔壁开一家一模一样的新店,装修好、试运行过,然后只把街口的指示牌改一下——客人自动走进新店;发现不对劲,牌子再改回来,十秒钟的事,旧店一直留在那儿。
另一招叫金丝雀(名字来自矿工下井带的那只鸟:鸟先不对劲,人就赶紧撤):新菜先只端给一百桌里的两桌,盯住反应;没问题再慢慢铺开,有问题就撤掉——最坏情况也只有两桌客人吃到过。
难的从来不是「推上去」,是没排练过。以前的发布是一份几十页的手册,由一个人在凌晨照着敲;测试环境一套步骤、生产环境另一套——于是生产这一次,成了整场流程里唯一没有彩排的演出。
更要命的是退路。手册最后一页通常写着「如需回退,请把上面倒着做一遍」,而这个倒序流程从来没有人真的跑过:它第一次被执行,就是在所有人最慌的那个凌晨。很多重大事故就是这个形状——不是坏在新版本上,是坏在退不回去。
第一,上线的动作必须和平时排练的一模一样。测试、预生产、生产用同一套自动脚本装同一个包,不同的只有几行配置。同一个动作练过上百遍,做第一百零一遍时手才不会抖。
第二,能退回去比能上得去更重要。回滚不该是躺在文档里的预案,而该是天天在用的普通功能——一条常走的路,才在下雨天靠得住。
第三,别一次全换。先换一小块、盯着仪表盘,好了再扩大:把「发布」从一个非开即关的开关,变成一个能慢慢拧的旋钮。
代码能秒退,数据退不回去:新版本按新格式写进去的东西,旧版本可能根本读不懂——真正难的从来不是把程序换回去,而是让同一份数据同时被新旧两版认得。
把发布从「一年几次、全员通宵的大事」变成「随时能做、一键能撤的小事」:同一套脚本部署所有环境,先小范围试放,而退回去的那条路要天天走。
想看蓝绿到底怎么切、金丝雀放多少比例、回滚为什么总卡在数据上? → 切到精读版
你以为发布的难点是「怎么把新版本推上去」,其实是「推错了怎么退回来」。这一章把发布从一次性的高风险事件,改造成可重复、已排练、可撤销的日常动作:同一套自动化流程部署包括生产在内的所有环境、同一个制品逐级晋级、回滚是一等公民;再用蓝绿(blue-green)与金丝雀(canary)把「一次全量切换」拆成可观察、可撤回的小步。
本章是《持续交付》Part II「部署流水线」的收官章。Ch5、Ch7、Ch8 讲的都是「怎么用尽可能便宜的方式证伪一个候选版本」,这一章讲最后一米:怎么把已经绿灯的版本真正推进生产、并随时能退回来。它把两个最大的未解题抛给 Part III:环境怎么管(Ch11)、数据怎么跟着演化(Ch12)。现实对应物是今天所有人都在用的东西——蓝绿、金丝雀、滚动升级、Spinnaker / Argo Rollouts、GitOps。
先摆一次 2010 年前后典型的企业发布:四十多页的操作手册、周五 22:00 起 4–6 小时的停机窗口、一屋子人加一个通宵电话会议、季度发一次——单次上线打包着几千个变更。三个结构性缺陷互相加固:
不解决就掉进一个自我强化的螺旋:发布频率越低 → 单次风险越大 → 流程越重 → 频率更低。本章的全部手段都是为了打断它,让发布变成一件频繁、无聊、可撤销的事。价签是公开的:2019 年 7 月 2 日,Cloudflare 一条 WAF 规则跳过了常规的逐步放量、几秒内全球生效,27 分钟里全球流量掉了 82%(详见 §6)。
本章开篇最反直觉的一条:发布策略(release strategy)应当在项目启动时就动笔,由开发和运维共同拥有,并随每次迭代更新——它要回答:谁部署、谁批准晋级;部署与回滚的步骤(两者同等详细);配置怎么版本化;数据怎么迁移、失败怎么办;上线后盯哪几个指标。配套的硬要求是:第一个迭代就把「部署到一个生产类环境」跑通——把最容易被推迟的事拉到最前面,此后每次迭代都重复它。
这是 Ch5「只构建一次」在最后一米的兑现:二进制包只在提交阶段构建一次,此后字节再不改动,只在环境间晋级。唯一变的是配置——而配置同样必须版本化、同样跟着晋级,绝不允许有人上生产机手改一行。
为什么这么较真?因为只有生产和预生产共用同一套部署脚本,前面所有测试才算数。附带两个好处:脚本每天被跑几十遍,它自己就成了被充分测试的代码;以及什么版本、谁批的、上到哪里全部天然留痕。
本章最硬的主张:没有被真正演练过的回滚方案,就不算发布计划。书里给了两条路。路一:用同一套自动化重装上一个已知良好版本——前提是部署全自动、旧制品还在库里、数据向后兼容;配套一条小规矩:删除旧文件不如把它移走,让上一版的现场留着。路二:蓝绿切回(下节展开),改回路由,秒级完成。
而那堵墙是数据:新版本一旦按新格式往库里写过东西,旧版本就可能读不懂它,回滚被物理堵死。所以真正的命题不是「怎么退代码」,而是怎么让同一份数据同时被新旧两版读懂:改 schema 时先只做加法(加可空的新列),让两版共存;等新版本稳定若干天、确认不会再退,再做减法。这套「先扩展、后收缩」本书在 Ch12 展开,后来成了通行的 expand / contract 模式。
由此引出一条纪律:紧急修复也必须走同一条流水线。一次手工上生产会让生产环境与版本库永久分叉,从此没人知道线上跑的到底是什么。而且在压力之下,回滚往往比 hotfix 更快也更安全——凌晨三点的判断力最不可靠。
蓝绿部署:备两套完全相同的生产环境。此刻「蓝」在服役、承载 100% 流量;新版本部署到闲着的「绿」上,跑冒烟测试、预热缓存、确认它是活的——用户全程无感。确认之后只改一次路由,流量瞬间转到绿;回滚就是把路由改回蓝,同样一瞬间。
蓝绿把停机压到切换的一瞬(秒级),并给了全书最快的回滚。代价要诚实面对:基础设施翻倍(2010 年意味着买两倍物理机,这是当年蓝绿难普及的主因,今天在云上便宜得多);数据库通常是共享的——书里的办法是切换瞬间把蓝置为只读、复制到绿,或让 schema 同时兼容两版(后者才是今天的主流);外加两个现实的坑:靠改 DNS 切换会让 TTL 把「秒级」变成几十分钟,所以要在负载均衡器上切;老连接还需要优雅排空(drain)。
金丝雀发布:不一次推给所有人,而是先推给一小撮服务器或用户——比如 2% 的流量——然后盯住错误率、延迟与关键业务指标看一段时间;正常就逐级放量(2% → 5% → 25% → 100%),异常就把金丝雀摘掉。回滚在这里几乎不用「做」:把那 2% 切回基线即可,最坏情况也只有 2% 的用户见过坏版本。
它还顺带解锁两件事:用一台真机做容量测试(导入已知比例的真实流量,就能推算全量所需机器数),以及A/B 测试。代价同样实在:生产里同时存在两个版本,可能持续几小时到几天,于是 schema、消息格式、对外协议都必须兼容两版——和回滚是同一堵墙;而且你得能按版本切分指标,否则 2% 的异常会被 98% 的正常淹没在同一张大盘里。
把这条路走到底就是持续部署(continuous deployment):每个通过流水线的版本自动进生产,不需要人按按钮(书里的例子是 IMVU,详见 §6)。但要分清:本书主张的持续交付是「随时可以发布」,把发不发留给业务;持续部署是「每次都自动发」,是它的一个特例,而非必然终点。
本章最有迁移价值的观念是那条分界线:部署是技术动作,发布是业务决定,两者可以拆开。蓝绿与金丝雀已经把这条缝撬开——代码早就装在生产机上,只是还没把流量给它。彻底撑开(用功能开关让代码上线但功能不可见)本书放在 Ch13,本章只走到「按流量比例控制」这一步。
最后是一组不起眼却极实用的细节:部署后要有预热期(缓存、连接池都要时间,别刚部署完就打满流量);脚本要快速失败,别装到一半才发现环境不对;部署活动全部记日志;写部署脚本的人里必须有真正执行部署的人;服务端程序不要带图形界面,一切操作都得能脚本化。
表 1 · 四种上线方式:停机、成本、回滚速度与各自的硬前提
| 就地替换 | 滚动升级 (书成之后的云上默认) | 蓝绿 | 金丝雀 | |
|---|---|---|---|---|
| 停机时间 | 整个部署过程,分钟到小时级 | 0(前提是多实例) | 切换的一瞬,秒级 | 0 |
| 额外资源 | 无 | 约 1 个实例的余量 | 双份生产环境 | 一小撮实例(如 2%) |
| 回滚速度 | 再跑一次完整部署 | 反向滚一遍,与部署同量级 | 改回路由,秒级 | 摘掉金丝雀,秒级 |
| 爆炸半径 | 100% 用户 | 已滚动到的那部分 | 切换后 100% | = 放量比例,可调 |
| 硬前提 | 能接受停机窗口 | 新旧版本能共存 | 数据库兼容两版;别用 DNS 切(TTL 会毁掉「秒级」) | 新旧共存 + 能按版本切分指标 |
| 适用 | 内部系统、有天然低峰窗口 | 无状态服务的默认选择 | 要求切换干净、秒级回滚 | 变更风险高、用户量大、观测能力强 |
表 2 · 出事之后的三条退路(本章给的默认答案是第一条)
| 做法 | 耗时量级 | 前提 | 什么时候选它 |
|---|---|---|---|
| 重新部署上一个好版本 | 一次完整部署:几分钟到几十分钟 | 部署全自动 + 旧制品还在库里 + 数据向后兼容 | 默认选项:原因不明、影响正在扩大时先止血 |
| 蓝绿 / 金丝雀切回 | 秒级 | 旧环境还没拆、数据兼容两版 | 已经付了双份资源的钱,就该拿走这个收益 |
| 向前修复(fix forward) | 不可预测:改代码 + 走完整条流水线 | 你确信根因、且流水线足够快 | 只在「退不回去」时选;绝不能因此绕过流水线直接改生产 |
三条路之外还有一条总判据:先问你的回滚演练频率,再决定上线方式。一个每天被真实执行的重新部署,比一个写在文档里、从未跑过的蓝绿切换可靠得多——技术先进程度不构成可靠性,被反复执行的次数才构成。
今天几乎所有云上发布工具的形状都能在这一章找到原型:Kubernetes 的滚动升级与就绪探针(对应「冒烟测试 + 预热期」)、Argo Rollouts / Spinnaker 的金丝雀与红黑发布、AWS CodeDeploy 的蓝绿与自动回滚。SRE 领域的「自动金丝雀分析」——用统计方法比较金丝雀与基线、自动决定放量或回滚——就是这一章那个「观察窗」的工业化版本。
面试与架构评审里这是高频考点。被问到「你们怎么发布」,能拿分的答法不是报工具名,而是五个事实:生产和预生产是不是同一套部署脚本?上一次回滚是什么时候、花了多久?金丝雀放多少比例、观察窗多长、看哪几个指标?schema 变更怎么保证能退回去?紧急修复走不走流水线?
① 这一章把发布从「一次性高风险事件」改造成可重复、已排练、可撤销的日常动作;难点不在推上去,在退回来。
② 发布策略是项目第一天就该动笔的产物,开发与运维共同拥有;第一个迭代就要把「部署到生产类环境」跑通。
③ 骨架规矩:一个制品、一套脚本、走遍所有环境,生产不例外;差异只在版本化的配置里。
④ 回滚是一等公民:默认路是用同一套自动化重装上一个好版本;删旧文件不如移走它;紧急修复也走流水线,绝不手工改生产。
⑤ 真正的墙是数据:代码能秒退、数据不能。schema 变更要先只加、后再删,让新旧共读一份数据,回滚才是真的。
⑥ 蓝绿:两套生产环境轮流服役,上线与回滚都退化成改一次路由;代价是双份资源,数据库切不动、别用 DNS 切。
⑦ 金丝雀:先给 2% 流量、过观察窗再逐级放量(2% → 5% → 25% → 100%),爆炸半径 = 放量比例,还顺带解锁容量测试与 A/B。
⑧ 观念分界线:部署是技术动作、发布是业务决定;走到尽头是持续部署(IMVU:每 9 分钟一版进生产),但持续交付要的是「可以随时发」,不是「必须自动发」。被反复执行的次数才构成可靠性。