专业书籍精读 · 持续交付 · 第 5 章
Continuous Delivery · Ch 5 · Jez Humble & David Farley · 2010
你手机里的 App 隔三差五就更新一次。从某个程序员改完一行代码,到这行代码真的跑在你手机上,中间要走一段路。《持续交付》第 5 章讲的就是这段路该长什么样——作者管它叫部署流水线,这也是整本书的心脏。
把它想成工厂的质检流水线:一次代码改动就是一件刚下线的货,它得依次经过几个检验工位。越靠前的工位越快越便宜(瞄一眼外观、称个重,几分钟搞定);越靠后的越慢越贵(拆开做全套检测,得等大半天)。任何一个工位亮红灯,这件货当场停住,绝不许往下走。
这里有个反直觉的地方:这条流水线的目的不是证明「这版能发」,而是尽早、尽便宜地证明「这版不能发」。所以最快最糙的检查排在最前面——坏消息越早知道越省钱。
从前发新版本靠「发布日」:攒上三个月的改动,挑个周五半夜,一屋子人照着一份几十页的操作手册手工敲命令,敲错一步就通宵回滚。更糟的是,测试用的那台机器和真正对外的机器长得根本不一样,所以「测试环境测过了」根本不算数。于是没人敢发版,越不敢发就攒得越多,越多就越容易出事——恶性循环。
第一,只装配一次。检验合格的那一箱,就是最后发出去的那一箱,中途绝不重新做一箱。听着像废话,可老做法恰恰是每换一个环境就重新编译打包一次——结果上线的那个版本,其实谁也没测过。
第二,每一站用同样的动作。装到测试机上和装到线上机器上,用的是同一套自动脚本,区别只在「贴的标签」(各环境的配置)不同。装完还要立刻做一次自检:真的起来了吗?该连的东西都连上了吗?
第三,红灯就停线。哪一站亮红灯,这批货立刻停住,全组先把灯修绿再干别的——不许绕过去,也不许「先记着回头再说」。
三条合起来,发版就从「一场大型事故演习」变成「按个按钮」。而且任何时刻你都能一眼看到:这个版本走到哪一站了、卡在哪、能不能上线。亚马逊、Etsy 这些公司之所以敢一天上线几十上百次,靠的就是把这条路修好了。
当然它有代价:这条流水线本身要人搭、要人养,刚开始你会觉得更慢更烦、红灯还老响,好处得等到发布次数多起来才显出来。
把「从改代码到上线」修成一条固定的自动流水线:只装配一次、每站动作相同、红灯就停线。它不为证明你行,而是让不行的版本尽早、尽便宜地被拦下来——于是发布不再是大事,而是一个按钮。
想进到具体阶段划分、时间量级和示意图? → 切到精读版
部署流水线(deployment pipeline)是把「从提交到上线」的整个发布流程建模成一条自动化的、每次变更都走同一条路的传送带:每次提交产出一个候选版本(release candidate),它依次穿过若干道关卡,越靠前越快越便宜、越靠后越像生产越慢越贵。关键的心态反转是——流水线的任务不是证明这个版本能发,而是尽早、尽便宜地把不能发的版本证伪。这一章是全书的核心,后面所有章节都是在给这条流水线补零件。
本章是 Part II「部署流水线」的开篇,也是全书的枢纽:它把 Part I 打下的三块地基——配置管理(Ch2:一切进版本控制)、持续集成(Ch3:每天并回主干、红了停线)、测试策略(Ch4:四象限)——组装成一条端到端的传送带。往下,Ch7 展开提交阶段、Ch8 展开自动化验收测试、Ch10 展开部署与发布、Ch11 讲环境、Ch12 讲数据、Ch13 讲组件与依赖,全都是在给这条流水线补零件。现实中对应的是你每天打交道的 Jenkins、GitLab CI、GitHub Actions、Argo CD、Spinnaker——「流水线(pipeline)」这个词能成为行业通用语,出处就在这一章。
作者在开篇让你先画一张价值流图:把一个改动从「决定要做」到「用户用上」的每一步摊开,标上每步实际干活的时间和步与步之间的等待时间。几乎所有团队画完都是同一个结论——绝大部分时间不是在干活,是在等:等一个「能测的版本」、等测试环境空出来、等运维排期、等发布窗口。真正写代码、跑测试的时间只占零头。
等待的背后是四个具体的病:
不解决会怎样?看一个真实到残酷的量级:2012 年 8 月,做市商 Knight Capital 把一版新代码手工拷到 8 台生产服务器,漏了 1 台;那台机器上残留的一段废弃老代码被新版本复用的开关唤醒,45 分钟内产生约 4.6 亿美元损失,公司随即被收购。SEC 后来的裁定书把原因写得很直白:部署没有自动化、没有校验各台机器是否一致。这一章要解决的,正是这类问题。
定义很朴素:部署流水线是应用「构建 → 部署 → 测试 → 发布」全过程的自动化实现。每一次提交都自动创建一个候选版本,它必须顺次通过所有关卡才能到达生产;任一关卡失败,这个候选版本就此出局。
关卡的排布遵循一条明确的经济学:快而糙的在前,慢而真的在后。前面的阶段用几分钟排除掉大多数低级错误,把宝贵的、昂贵的后段资源留给真正值得跑的候选版本。
表 1 · 四类阶段:跑什么、多久、拦住什么、代价在哪
| 阶段 | 怎么触发 | 典型耗时 | 拦住什么 | 代价 / 风险 |
|---|---|---|---|---|
| 提交阶段 | 每次提交,自动 | 目标 < 5 分钟,上限 10 分钟 | 编译失败、单元测试回归、明显的代码问题 | 一旦塞太多东西而超时,开发就不再等结果,CI 名存实亡 |
| 自动化验收测试 | 提交阶段通过后自动 | 数十分钟 ~ 数小时 | 业务功能回归、组件间集成问题 | 慢且脆;环境不像生产就等于白测 |
| 容量 / 非功能测试 | 自动或按需 | 小时级 | 性能与容量退化、资源泄漏 | 环境成本高,结果噪声大、需要基线 |
| 手工探索测试 / UAT | 人按需自助拉取某个已通过前面关卡的版本 | 天级 | 可用性问题、没人想到过的场景 | 一旦退化成「签字排队」就变成最大瓶颈 |
| 生产发布 | 按按钮(或全自动) | 分钟级 | —— 靠冒烟测试与回滚兜底 | 数据迁移往往不可逆,回滚不是万能的 |
这是本章最该记住的一条,也是最常被违反的一条。制品在提交阶段构建一次,存进制品库,之后每个阶段都取用同一个包,只有配置随环境注入。
为什么?因为一旦每个环境各自重新构建,你就无法保证「测过的」和「上线的」是同一个东西——编译器小版本、依赖解析结果、构建机上的环境变量、某个 SNAPSHOT 依赖在两次构建之间被人改了……任何一点漂移,都会让前面几小时的测试失去意义。你测的是包 A,上线的是包 B,而你以为它们一样。
配套的两个细节:制品要能追溯到那次提交(用版本控制的修订号命名,出了事能一路查回去);配置不进包(同一个包配上不同环境的配置文件 / 环境变量,就能跑在任何环境上——这正是 Ch2 配置管理的用武之地)。
开发机、测试环境、预发、生产,用的必须是同一套部署脚本,区别只在传入的配置。理由很实在:如果生产部署走的是一条「只在发布日才跑一次」的独立路径,那它就是全系统里唯一没被反复演练过的环节——而它偏偏是风险最高的那个。反过来,同一套脚本每天在测试环境跑几十遍,等到用它上生产时,它已经是这条流水线上被验证得最充分的一段代码。
紧跟着的一条是部署完立刻跑冒烟测试:几十秒钟内验证「服务起来了、数据库连上了、消息队列通了、外部依赖能调」。它不测业务逻辑,只回答一个问题——这套东西是活的吗?Knight Capital 那 8 台机器漏掉的 1 台,正是任何一个像样的部署校验都能当场发现的。
验收测试跑在什么环境上,决定了它的结论值多少钱。测试环境越像生产,测试结论越可信——相同的操作系统与补丁版本、相同的中间件配置、相同的网络拓扑、尽量相同的数据形态。书里坦承这在 2010 年很贵,建议用虚拟化把成本压下来;今天容器与基础设施即代码(IaC)让这件事便宜了一个数量级,反而没了借口。
「即刻传播」是说:一个阶段通过后,自动触发下一个阶段,而不是等人手工点、等夜里的定时任务。定时构建(比如每晚一次)的隐性代价是:一晚上攒了 20 个提交,红了之后你得先花时间找出是谁的锅——反馈越晚,定位成本越高。而当后段阶段实在慢到跟不上提交速度时,正确做法是让它「取最新的可用版本」继续跑,而不是排队把每个版本都跑一遍。
这条直接继承自 Ch3 的持续集成纪律,并把它扩展到整条流水线:流水线红了,全组第一优先级是把它修绿,而不是「先绕过去、回头再说」。原因不是道德洁癖,而是数学:一旦允许带着红灯继续提交,后面的提交会叠在一个未知状态之上,几个小时后你面对的就不是一个缺陷、而是一团互相纠缠的缺陷。
把上面几条串起来,你得到的是三样在旧世界里稀缺的东西:可见性——每个版本走到哪一站、卡在什么问题上,全团队(包括测试和运维)一屏看清;可追溯性——生产上跑的每一个制品,都能一路查回它来自哪次提交、过了哪些测试;按需交付的能力——发布变成从一个已通过所有关卡的候选版本里挑一个、按下按钮。
与之配套的头号指标是周期时间(cycle time):从提交到它跑在生产上要多久。作者明确说这是最重要的度量——因为它同时反映了流水线的自动化程度、测试速度、环境可用性和组织摩擦,任何一处退化都会在它身上显形。(八年后 DORA 的《Accelerate》把它正式列为四大指标之一。)
表 2 · 构建策略:只构建一次 vs 每阶段重新构建
| 只构建一次(本章主张) | 每个环境重新构建 | |
|---|---|---|
| 测试有效性 | 测过的就是上线的 | 上线的那个制品从未被测过 |
| 耗时 | 构建一次,后续阶段直接取包 | 每阶段重复几分钟到几十分钟 |
| 配置怎么办 | 配置外置、按环境注入(Ch2) | 常把配置编进包里 → 每环境必须重编 |
| 出事能否追溯 | 制品名带修订号,一路查回提交 | 同一提交对应多个不同产物,难定位 |
| 代价 | 要建制品库,要把配置从代码里拆出来 | 看似省事,实则把风险推到上线那一刻 |
表 3 · 生产那道门放不放人:持续交付 vs 持续部署
| 持续交付(本书立场) | 持续部署 | |
|---|---|---|
| 上生产由谁触发 | 全部关卡通过后,由人按按钮 | 全部关卡通过后自动上线 |
| 核心主张 | 软件随时可发布(发不发是生意决定) | 能发就发,把批量压到最小 |
| 前提条件 | 自动化部署 + 类生产环境 + 可回滚 | 再加:极强的测试覆盖、生产监控、自动回滚、灰度 |
| 适合 | 受监管行业、客户端 / 嵌入式、发布有业务节奏 | 高频 Web 服务(Amazon、Etsy 一类) |
| 代价 | 按钮前仍会积压批量,攒得越久风险越大 | 缺陷直达用户,全靠监控与秒级回滚兜底 |
表 4 · 测试环境保真度:越像生产越贵
| 方案 | 保真度 | 成本 | 放在流水线哪一段 |
|---|---|---|---|
| 与生产同构 | 高:同拓扑、同配置、同数据形态 | 高 | 容量测试、上线前最后一关 |
| 等比缩小 | 中:节点数少,但拓扑与配置一致 | 中 | 大多数自动化验收测试的性价比之选 |
| 共享测试环境 | 低:多团队抢用,状态互相污染 | 低 | 只适合快速反馈,别拿它下结论 |
| 开发者本机 | 最低 | 最低 | 提交阶段前的自测 |
表 5 · 提交阶段该塞什么:10 分钟预算怎么花
| 该放进提交阶段 | 不该放进提交阶段 |
|---|---|
| 编译、打包成可部署制品 | 需要真实数据库 / 外部服务的端到端测试 |
| 全部单元测试(毫秒级、无外部依赖) | 跨浏览器 UI 测试、性能与容量测试 |
| 静态分析(明显缺陷、复杂度、重复度) | 需要人参与的检查 |
| 少量最关键的冒烟级集成检查 | 任何会把总时长顶过 10 分钟的东西 |
三张表背后是同一个判断:关卡越靠前越要快,越靠后越要真。把慢东西往前塞,开发就不再等结果、CI 退化成摆设;把该测的往后拖,缺陷就带着几小时的滞后被发现。选型时先问一句:这个检查平均能提前多久发现问题、每次要花多少分钟?
今天你用的每一个 CI/CD 工具,本质上都是这一章的具体实现:Jenkins 的 Pipeline、GitLab CI 的 stages、GitHub Actions 的 workflow、Argo CD 与 Spinnaker 的多阶段发布——「阶段」「关卡」「制品」「晋级(promotion)」这套词汇全都出自这里。制品库(Nexus、Artifactory、容器镜像仓库)的存在理由,就是「只构建一次」;容器镜像之所以成为行业默认,很大程度也是因为它把「同一个制品跑在所有环境」变成了物理事实。
面试与架构评审里,这一章是高频考点。被问到「你们的 CI/CD 怎么设计的」,一个能拿分的答法不是罗列工具,而是回答四件事:提交阶段几分钟?制品构建几次?各环境部署脚本是不是同一套?红了之后谁负责、多久修好?
① 一句话:部署流水线是「从提交到上线」全过程的自动化实现,每次提交产出一个候选版本,走同一条路、过同一批关卡。
② 心态反转:流水线的目的不是证明能发,而是尽早、尽便宜地证伪——所以快而糙的检查排在最前。
③ 阶段梯度:提交阶段(目标 5 分钟、上限 10 分钟)→ 自动化验收测试(数十分钟至数小时)→ 容量 / 手工 / UAT(按需拉取)→ 生产发布(按按钮)。
④ 规矩一 只构建一次:制品在提交阶段构建一次、存进制品库、后续阶段原样复用;否则「测过的」和「上线的」不是同一个东西。
⑤ 规矩二 同一套方式部署到每个环境:差异只在配置;生产部署路径不该是全系统里唯一没被反复演练过的那段。
⑥ 规矩三 部署完立刻冒烟测试,并部署到生产的副本上做验收测试——环境不像生产,测试结论就不值钱。
⑦ 规矩四 变更即刻传播 + 红了停线:定时构建让定位成本翻倍;带着红灯提交会把一个缺陷变成一团缺陷。
⑧ 流水线给你可见性、可追溯性与按需发布能力;头号指标是周期时间(cycle time)——后来成了 DORA 四大指标之一。
⑨ 真实系统:Jenkins / GitLab CI / GitHub Actions / Argo CD / Spinnaker 都是它的实现;制品库与容器镜像是「只构建一次」的物理保障。
⑩ 反面教材:Knight Capital 手工部署漏掉 1 台机器,45 分钟损失逾 4.6 亿美元——这一章的每条规矩都是有标价的。