专业书籍精读 · 持续交付 · 第 14 章
Continuous Delivery · Ch 14 · Jez Humble & David Farley · 2010
一个软件,几十上百号人同时改同一堆文件,最后还得拼成一个能发出去的整体。让这件事不乱套的工具叫版本控制。这一章不讲怎么用工具,只问一个所有团队都逃不掉的问题:大家该各自开一份副本分头干、最后再合到一起,还是每天都把手头的活并回共用的那一份?
两个同事分头改了两个月,合并那天工具很爽快地说「没冲突」,一行都不用手动处理。结果代码一跑就出错。
为什么?工具只会看两边有没有改到同一行字。一个人把某个共用功能的算法悄悄换了,另一个人在别处新写的逻辑正好指望着老算法——两边动的文件都不一样,工具当然说没冲突,可合出来的东西已经坏了。「合并顺利」和「合并正确」是两回事。
分头干的痛苦不是按时间平摊的,而是滚雪球。各自干一周,两人多半碰的是不同角落;各自干两个月,两边都把同一批文件翻新过一遍,合并就从「几处打架」变成「谁也说不清合出来的还对不对」。更坏的是它会让人变懒:你明知道某段代码该整理,可一整理别人合并时就要多遭一遍罪——于是谁都不动它。
书的主张很干脆:每个人每天都把自己的改动并回大家共用的那一份,让两边分开的时间短到来不及长出麻烦。
「可活没干完就并进去,不就露馅了吗?」这一章的价值全在这个追问上,它给了三个办法:先把门关上——新功能的代码早早并进去,只是暂时没有任何入口能走到它,做完了再开门;把大改拆成一串小改——每一步都完整、随时能发布;搭一层脚手架——要换掉一个到处在用的老零件时,先在中间加一层转接,新旧两个零件同时挂着,把用户一点点挪到新的那边,全挪完了再把老零件和转接层一起拆掉。装修不搬家,一间一间来。
分开几个小时到一天,问题不大;分开几周几个月,才是真正的敌人。所以今天流行的「开个分支、当天就合回去」的做法,和这本书的主张其实是一路的。
代价也得说清楚:那些开关和脚手架本身就是债——用完必须拆干净,不然哪天有人误碰一个早该删掉的旧开关,麻烦会比当初分头干还大。
分头干看着是在隔开风险,其实是在攒着风险、最后一次性付。分开的时间越长,合到一起时越危险——而且危险常常不报警。所以办法是:每天都合回去,用「关着门开发」和「搭脚手架换零件」来解决「活没干完」这个真问题。
想进到具体机制、四种分支模型的账单和示意图? → 切到精读版
《持续交付》第 14 章要建立的认知是:版本控制的真正难点不在工具,而在分支策略——它决定了团队的集成节奏。分支看上去在隔离风险,实际是在把集成的痛延后、并按分歧时长滚雪球式放大。所以本章的默认答案是在主干上开发(develop on mainline):靠隐藏入口、增量改造和抽象分支(branch by abstraction),让「大改动」也能一小步一小步落进主干,而不是靠一条长期分支把它藏起来。
main。本章是 Part III「交付生态」的倒数第二章:上承第 13 章(组件与依赖),下启第 15 章(管理持续交付)。它其实是第 3 章「持续集成」的背面——第 3 章立下「每人每天至少向主干提交一次」的规矩,第 14 章回答那句必然的追问:那分支怎么办。对应现实里 git-flow / GitHub Flow / 主干开发之争,以及每个团队迟早要做的选择:功能做完再合,还是边做边合。
2010 年前后的行业常态,是 Subversion / ClearCase / Perforce 那一套「长期分支 + 定期大合并」:每个团队或每个功能拉一条分支跑上几周到几个月,临发布前集中合并——这段时间有专门的名字,集成期或代码冻结,短则几天长则数周,全靠人力解冲突、修回归。它当年被当成秩序的象征:分支把人隔开了,互不干扰。
不解决会怎样?第一,合并代价不随分歧时长线性增长,而是滚雪球:各跑一周,改动多半落在不同文件上;各跑两个月,双方都重构过同一批目录,冲突就从「几处文本打架」升级成「谁也说不清合出来的还对不对」。第二,它反噬工程实践本身——长期分支让重构变成不划算的事:你明知某个类该拆,但一拆就意味着别人合并时多受一遍罪,于是没人动它。
本书的判断很直白:分支和持续集成是此消彼长的。你在分支上待得越久,CI 那句「主干是绿的」对你就越没意义——它只证明了别人的代码能一起工作,完全没证明你的能。
版本控制工具能替你喊出来的,是文本冲突(textual conflict):两边改了同一个文件的同一片行,工具认不出该留谁,停下来交给人。这层痛是可见的、有边界的——再难也知道难在哪。
真正要命的是语义冲突(semantic conflict):两边改的位置根本不重叠,工具一声不吭就合了,但合出来的东西是坏的。
最好认的形态:分支 A 把一个共用方法改了名、调了参数顺序,分支 B 那边新写了三处对旧签名的调用。两边文件不同、行不同,合并零冲突——编译立刻挂。这还算走运,因为编译器会喊。更阴的是能编译也能跑,只是行为悄悄变了:A 把某个共用方法的取整从四舍五入改成向下取整,B 新写的对账逻辑正好指望着旧行为。这类问题只有测试抓得到,而两边分支上的测试当时都是绿的——它们各自跑在合并前的那个世界里。
代价之所以超线性,就在这里:文本冲突大致随双方改动量增长,语义冲突随双方改动的交互组合增长。所以本书开的药方不是「更好的合并工具」,而是把分歧窗口压到最小。
本书把分支按「为什么要拉」分类,并给出了明确的偏好排序:
主干开发不是「大家硬挤在一条线上互相踩」,它得配一套让主干始终可发布的手法。本书给了三把刀:
① 先把门关上。新功能的代码早早并进主干,只是暂时没有任何界面、路由或菜单能走到它,做完了再开门。今天的通用形态就是特性开关:代码在主干里、开关默认关、灰度时按比例打开。它把「功能完成」和「功能可见」彻底解耦了。
② 把大改拆成一串各自可发布的小步。换数据库、换框架不要憋一个大的:每一步都保证主干构建通过、当天就能上线。它比一次性大改更费脑子——但费的是设计的脑子,换掉的是合并那天的赌博。
③ 抽象分支(branch by abstraction)。全章最有用的一招,也是名字最误导的一招——它根本不涉及版本控制里的分支。要替换一个被到处调用的组件时:先在调用方和旧实现之间插一层抽象,让所有调用都走这层;在这层下面写新实现,新旧并存;再一部分一部分把流量切过去;全部切完后删掉旧实现和那层抽象。整个过程每一步都在主干上、都可发布。
本章写于 Git 刚满五岁的 2010 年,但它对 DVCS 的判断出奇地站得住。改变的:提交和共享被解耦了——你可以本地提交二十次、整理成三个干净的提交再推出去,中间不污染任何人;分支的创建成本几乎归零,于是催生了 fork + 拉取请求(pull request)这种今天的默认协作形态。
没改变的:分支便宜的是创建,不是合并。代价从来在合并侧,而它由分歧时长决定,不由工具决定。Git 让你能轻松拉一百条分支,这件事本身并不会让第一百次合并变容易。工具把创建的摩擦降到零之后,唯一还拦着你拉长期分支的只剩纪律。至于企业采用 DVCS 的两个顾虑——没有单一真相源、审计困难——本书给的解法今天已成标配:指定一个受祝福的仓库(blessed repository)作为唯一权威,CI 只认它、发布只从它出。
表 1 · 四种分支模型的账单(按本书的偏好排序)
| 模型 | 集成延迟 | 主要风险 | 需要的纪律 | 本书态度 |
|---|---|---|---|---|
| 主干开发 | 近乎为零(按小时 / 天计) | 主干上留下未完成代码与开关债 | 每天至少合一次;配隐藏入口 / 增量改造 / 抽象分支 | 默认推荐 |
| 为发布拉分支 | 零(开发全程在主干) | 热修复忘了合回主干 → 下版本回归 | 分支上只修关键缺陷,改完立刻合回 | 认可 |
| 按团队拉分支 | 中(队内即时,跨队按天 / 周) | 跨队语义冲突攒到合并日才爆 | 每条分支一套 CI + 一套盯主干;强制高频合回 | 可行但昂贵 |
| 按功能拉分支 | 高(按功能周期,可达数周) | 与 CI 定义上抵触;重构被抑制 | 寿命按天算;频繁从主干往分支合;重构走主干 | 最保留 |
表 2 · 「活没干完」的四种落地方式——什么时候用哪个
| 手法 | 怎么做 | 代价 | 什么时候用 |
|---|---|---|---|
| 隐藏入口 / 特性开关 | 代码进主干但没有入口能到达,或由配置开关控制 | 代码路径数翻倍;开关必须有下线纪律 | 新增功能,且不改动已有行为 |
| 增量改造 | 把大改拆成一串各自可发布的小步 | 要重新设计改造路径,更费设计精力 | 改动能被自然切分(加字段、扩接口) |
| 抽象分支 | 插抽象层 → 新旧并存 → 逐步切流 → 删旧 | 临时多一层抽象和两套实现,必须真的拆掉 | 替换被到处调用的组件(换库、换存储) |
| 组件化 | 把这块独立成组件,按版本号集成(第 13 章) | 丢掉「一次提交、一次全量验证」,靠钉死版本买回 | 边界清晰、能独立发布演进的模块 |
压成一句可带走的判据:真正的敌人不是分支,是分歧时长。今天「拉个分支、当天就合回主干」的短命做法和本书是同一路——它只是把「每天向主干提交一次」实现成了「每天合一次」。所以判断一个团队是不是在做主干开发,别看它有没有分支,看分支寿命中位数和同时存活的分支条数。
这一章在 2010 年是逆流之论——当时「分支纪律严明」还被当成成熟度的标志。十几年后它基本赢了:主流互联网公司的默认形态是主干 + 短命分支 + 评审后即合,git-flow 那种长期 develop / feature / release 并行的模型,退回到了「有明确版本号、要长期维护多个已发布版本」的场景。面试或架构评审里,它能换算成三个问句:你们的分支平均活多久?同时活着几条?重构提交走哪条线?
注:本次运行的出口代理拦截了上述来源的域名,故只保留文字出处、不放未经核实的链接。
git branch。① 版本控制的难点不在工具,在分支策略决定集成节奏:分支是把集成的痛延后并放大。
② 合并有两层:文本冲突工具会喊;语义冲突工具不喊,合得干干净净却是坏的——它随双方改动的交互组合增长,这才是代价超线性的原因。
③ 药方不是更好的合并工具,是把分歧窗口压到最小——敌人是分歧时长,不是分支本身。长期分支还抑制重构:一拆类别人合并就更痛,于是没人动。
④ 四种分支理由的排序:为发布(认可)> 按团队(昂贵)> 按功能(与 CI 定义上抵触)> 按环境(反对,该用配置管理)。
⑤ 默认答案是主干开发,配三把刀解决「活没干完」:隐藏入口 / 特性开关、增量改造、抽象分支(插抽象层 → 新旧并存 → 切流 → 删旧,全程在主干)。
⑥ DVCS 改变了创建分支的成本,没改变合并的成本。判断一个团队是否在做主干开发,看分支寿命中位数与同时存活分支数(DORA:少于三条、短于一天),而不是看它有没有分支。
⑦ 开关和抽象层都是临时债,必须真的删掉——Knight Capital 的 4.6 亿美元是这条纪律的反面教材。