专业书籍精读 · 持续交付 · 第 3 章
Continuous Delivery · Ch 3 · Jez Humble & David Farley · 2010
一个软件不是一个人写出来的,是十几、几十个人同时在改同一堆东西。问题来了:各改各的,最后怎么合到一起?《持续交付》第 3 章讲的就是这件事,而它的答案有点反直觉——别攒着,每天都合。
五个人合写一本书,每人认领几章。第一种干法:各写各的三个月,最后一周凑到一起拼。你能想象那个场面——同一个人物在第三章死了、第七章又活着。拼书那一周有多痛苦,没人能提前估出来,因为谁也不知道埋了多少矛盾。
第二种干法:每人每天下班前,把当天写的内容并进那份共同的稿子,并进去就当场从头通读一遍。读不通,全组停下来先把它读通。这样每次要处理的矛盾都只有一天份,小到当场能解决。持续集成就是第二种干法。
为什么大家宁可攒着?因为合并很烦,攒着显得「先把手上的事做完」更有效率。可矛盾不会因为你不看就消失,它只是在暗处越滚越大。更要命的是:第一种干法下,稿子在绝大部分时间里拼不起来、交不出去,只有拼完那一小段时间才好用——而你永远说不准还要拼多久。
这一章要把这个默认状态倒过来:让稿子在任何时刻都通顺、能交付,偶尔坏一下、马上修好。做法是两条。
第一条:每人每天把改动并回那份唯一的共同版本,并回去就自动从头检查一遍。第二条更关键,也是最多团队做不到的:一旦检查没通过,全组立刻停下手上的事去修它,修好之前谁也不许往上面加新东西。这就像工厂流水线上任何人发现次品都能拉停线——听着耽误产量,实际上正因为停得起,次品才不会一路流到最后。修不好也有兜底:先退回昨天那个好用的版本。
软件从「大部分时间是坏的」变成「大部分时间是好的」;出了问题,嫌疑范围只有最近这一小步。诚实的代价是:这要靠一整队人长期守纪律——它是一种习惯,不是买个工具装上就有的,而那盏红灯一旦被习惯性无视,整套东西立刻退化成没人看的摆设。
与其攒三个月再痛苦地合一次,不如每天合一次、每次合完当场验一遍;一旦验不过,全组停下来先修好它。软件的默认状态,应该是「它能用」。
想进到具体纪律、时间预算、对比表和示意图? → 切到精读版
你以为持续集成(continuous integration,CI)是「装个 Jenkins、每次提交跑一遍构建」,其实这一章讲的是一条纪律:把「集成」从项目末期那个工期不可测的阶段,拆成每人每天多次的小动作——每次提交都触发一次完整构建加全套自动化测试,一旦变红,全队停下来先修好它。目标是把软件的默认状态从「它不工作、偶尔可用」翻转成「它随时可工作、偶尔坏一下」。原书把话说死了:CI 是一种实践,不是一个工具——效果取决于团队纪律,与你买了什么无关。
main)。Part I「基础」的第 3 章,夹在第 2 章配置管理与第 4 章测试策略之间:第 2 章给的版本控制与自动化构建正是本章的前提,本章要求的那套快速自动化测试由第 4 章展开;而「提交 → 构建 → 提交测试」这一环,到第 5 章会被命名为部署流水线的提交阶段(commit stage)、成为流水线第一关。今天的对应物:GitHub Actions / GitLab CI / Jenkins、Google 的 TAP、GitHub 的合并队列(merge queue),以及被 DORA 反复验证的主干开发。
先摆个场景。20 人团队做一套交易系统,分 4 个小组各开一条特性分支,约定「做完这期再合」。三周后合并那天你会连撞三件事:语义冲突(两组各自重构了同一个接口,文本上不冲突、合起来跑不通);合并工期完全估不出来,可能半天也可能一周;以及最贵的一件——某个根本性的设计矛盾其实第 3 天就写下了,你到第 21 天才知道。缺陷的修复成本随「写下」与「发现」的间隔单调上升,攒批集成等于把这个间隔人为拉到最大。
原书对旧世界的判词很冷:大多数项目里软件的默认状态是「它不工作」,只有偶尔一次集成后的短暂窗口它才可用。你失去的不只是效率,更是可预测性——无法承诺发布日期,无法回答「现在能不能上线」,也无法把任何测试结论安在一个具体版本上。
原书的定义可以拆成两句。第一句是机器做的:每当有人提交任何改动,整个应用都要被重新构建一遍,并对它跑一整套全面的自动化测试。第二句是人做的,也是真正的分水岭:一旦构建或测试失败,团队停下手上的事,立刻把它修好。
为什么第二句才是灵魂?因为只有它能保证「主干随时可工作」这个不变量成立。红灯若可以挂着,构建结果就退化成装饰性指标,CI 的价值当场归零。这条纪律的精神原型是精益生产的停线(stop the line):任何人发现次品都能把整条产线拉停,看着损失产量,实则保证次品不会流到成品。
动手之前需要三样东西。版本控制:与项目有关的一切放进同一个版本库,而不是「代码在 Git、配置在某人机器上」(第 2 章的主题)。自动化构建:一条命令就能把整个应用构建并测试完,不依赖在 IDE 里点按钮——因为 CI 服务器只会敲命令。团队共识:所有人认同这是优先级最高的活动。第三样最难也最常被跳过,于是就有了「CI 天天亮着红灯、大家绕着走」的团队。
原书另给四条持续做下去的前提:经常签入(至少每天一次,越频繁越好);建立全面的自动化测试(否则构建通过只说明「能编译」);保持构建与测试过程简短(超过十分钟就有人不等它);管好开发工作区(本地结果要和 CI 一致,否则「在我机器上是好的」会吃掉所有信任)。
把纪律落成动作:拉取主干最新代码并在本地合并 → 本地跑一遍构建和提交测试 → 绿了才提交 → CI 服务器从主干干净检出、重新构建、跑全套提交测试 → 红了全队停线,绿了这份制品才有资格进入下一关。
三个细节值得单独说。本地先跑一遍,是别让自己的疏忽变成全队的停线。CI 要从主干干净检出,因为本地总有你没意识到的残留(未提交的文件、本地装的依赖),只有一台从零开始的机器才能证明「换个人拿这份代码也能构建」。提交完不能立刻走人——否则你已沉浸在新任务里,回头修红灯代价大得多,期间别人还会往红灯上继续提交。
表 1 · 原书列出的必备纪律及其道理
| 纪律 | 为什么 |
|---|---|
| 不在红灯上提交 | 红灯期间提交会让「谁弄坏的」无从判断;等它绿了再提交。 |
| 提交前本地跑完提交测试 | 把疏忽挡在自己机器上;或让 CI 服务器替你先跑(预提交构建)。 |
| 等提交测试通过再干别的 | 反馈到手前你还没脱离上下文,此时修最便宜。 |
| 绝不带着红灯回家 | 一夜无人修复,第二天全队被堵在门外——原书最著名的一条。 |
| 随时准备回滚 | 回滚是永远可用的兜底,前提是主干每个历史版本都曾是绿的。 |
| 修复设时限,超时就回滚 | 十分钟没修好就退回上个绿色版本,别让全队陪你调试。 |
| 不许注释掉失败的测试 | 等于删掉一条已知约束、还伪装成绿灯,比红灯危险得多。 |
| 为自己引发的一切失败负责 | 你的改动可能弄坏别人写的测试,那也是你的活。 |
原书还把测试驱动开发(TDD,先写测试再写实现)列进这一节,理由很实在:CI 的效力取决于那套自动化测试有多全面,而 TDD 是让测试真的被写出来的最可靠办法。
提交测试跑多久,直接决定这套纪律能不能活下来。原书给的门槛是十分钟:超过它,人们就开始「攒几个改动一起提交」,提交频率一降,每次红灯的嫌疑范围就变大,正反馈链条断掉。所以提交阶段只放最快、最能定位问题的那批检查,慢的往后放。
原书另给几条「值得加进去」的做法,共同点是把口头约定变成会让构建变红的规则。架构违规即失败:约定「界面层不许直接访问数据库」,就写一条自动检查,违反即红——否则这类约定只会活在 wiki 上。慢测试即失败:给单个测试设时间上限,防止测试套件在几个月里悄悄从 3 分钟涨到 20 分钟。警告与风格违规即失败:一次性开全会淹死人,原书给的是棘轮(ratcheting)——把当前警告数记为基线,比基线多就变红,于是这个数字只能单调下降。
本章成书于 2010 年,Git 这类分布式版本控制(DVCS)刚刚流行。原书的态度是「工具很好,但它让『不集成』变得太容易」:DVCS 鼓励开一堆本地分支、长期不推,而只要改动没并回那条共同的主干就不叫集成,不管你本地提交了多少次——这正是今天那句「用了 Git 不等于在做 CI」的源头。对分布式团队,原书的答案同样是集中的:一个中心化的 CI、一条共同的主干。
表 2 · 三种分支策略:CI 的效力如何随分支寿命衰减
| 主干开发 | 短命分支 + PR | 长期特性 / 团队分支 | |
|---|---|---|---|
| 分支寿命 | 几小时,或直接提交主干 | ≤ 1 天 | 数周至数月 |
| 冲突暴露时刻 | 写下后几小时 | 写下后一天内 | 数周后,且成批到来 |
| CI 实际在测什么 | 真正的集成结果 | 合并后的结果(需合并队列保证) | 只是分支自己,不是集成结果 |
| 代价 | 半成品要靠功能开关藏起来;评审要另想办法 | 多一次合并与等待 | 合并工期不可测;语义冲突集中爆发 |
| 适用 | 纪律强、测试全的团队;原书首选 | 今天多数团队的现实折中 | 只在必须长期隔离时(如大版本分叉) |
要点是:CI 的效力随分支寿命衰减。分支活一天,你损失一天的反馈;分支活三周,CI 测的其实只是那条分支自己,「绿」并不代表集成后也绿。
表 3 · 构建红了,四种处理方式
| 做法 | 后果 | 何时用 |
|---|---|---|
| 立刻修(限时十分钟) | 最短路径;超时会拖住全队 | 默认动作,原因一眼可见时 |
| 回滚到上个绿色版本 | 主干立刻恢复可用,问题转为个人任务 | 十分钟没头绪时的标准兜底 |
| 注释掉失败的测试 | 反模式:删掉一条约束还伪装成绿灯 | 不用;确认是片状测试则隔离并建单跟踪 |
| 挂着红灯继续提交 | 反模式:责任无从归属,主干长期不可用 | 不用;这是 CI 的典型死法 |
表 4 · 提交测试到底在哪跑
| 本地跑完再提交 | 预提交构建 / 合并队列 | 直接提交、由 CI 跑 | |
|---|---|---|---|
| 主干变红概率 | 低 | 接近零(不通过则不入主干) | 高 |
| 开发者等待 | 本地几分钟 | 排队,繁忙时可达几十分钟 | 零 |
| 基础设施成本 | 低 | 高:每个候选改动一套完整构建 | 低 |
| 今天的对应物 | 本地 make test / 预提交钩子 | GitHub merge queue、OpenStack Zuul | 小团队、测试极快时才可接受 |
这一章是全书那条流水线的第一块地基:没有「主干随时是绿的」,后面所有关卡都失去起点——你无法回答「拿哪个版本去做验收测试」。它也是今天工程实践的共同底色:GitHub Actions / GitLab CI / Jenkins 把「每次推送触发构建」变成默认配置,合并队列把「本地先绿」工程化成基础设施。面试里问「你们 CI 怎么做的」,考的往往不是工具名,而是本章这几条:多久提交一次、红灯谁负责、提交到反馈多长、有没有人注释过失败的测试。
420 万 个测试、每天约 1.5 亿 次测试执行;而 Google 自己在论文里承认,即便投入如此资源也无法为每次提交回归全部测试,只能靠选择性执行压住工作量——既验证了价值,也标出了规模上限。J. Micco「The State of CI Testing @Google」, 2017 ↗ · 「Taming Google-Scale Continuous Testing」, ICSE-SEIP 2017 ↗100% 生产 Web 机器,改动先内部灰度、再到 2% 金丝雀、最后全量——「主干随时可工作」能一路撑到生产发布。Engineering at Meta「Rapid release at massive scale」, 2017 ↗1.5% 的测试执行是片状的、约 16% 的测试曾表现出片状——这正是「不许注释掉失败测试」最常被破戒之处;Google 的答案不是关掉测试,而是识别、隔离、专门治理。Google Testing Blog「Flaky Tests at Google」, 2016 ↗① 一句话:CI 把「集成」从项目末期那个工期不可测的阶段,拆成每人每天多次的小动作,目标是把软件的默认状态从「它不工作」翻转成「它随时可工作」。
② 定义有两半:机器那半是每次提交都完整构建 + 跑全套自动化测试;人那半是红了全队停下来立刻修——后者才是分水岭。
③ CI 是实践不是工具:三个前提是版本控制、一条命令跑完的自动化构建、团队共识;共识最难也最常被跳过。
④ 四条持续前提:经常签入(至少每天)、有全面的自动化测试、构建加测试不超过十分钟、本地结果与 CI 一致。
⑤ 八条纪律里最该背下来的三条:绝不带着红灯回家、十分钟修不好就回滚、永远不许注释掉失败的测试。
⑥ 建议实践的共同思路:把口头约定变成会让构建变红的规则——架构违规、慢测试、代码风格(用棘轮法只许变好)。
⑦ 关键权衡:CI 的效力随分支寿命衰减;长期特性分支上的绿灯不代表集成后也绿,现实折中是一天以内的短命分支加合并队列。
⑧ 实证与位置:Google 每天约 1.5 亿次测试执行却仍无法为每次提交跑全量,Meta 已做到从 master 持续发布覆盖 100% 生产,DORA 显示活跃分支 ≤3 条与高效能强相关;本章是第 5 章部署流水线的第一关。