一条排满待合并卡片的传送带在窄闸口前减速,两侧分别立着计时器和计数牌

一、瓶颈换了一项,不是产能问题

据公开报道,一家公司披露内部日均合并的代码变更在一段不长的时间里涨了十几倍,原有的源码管理系统跟不上,团队用数人规模、数月时间重造了一套。本篇把倍数、次数与人数全部当作背景,不作为任何一条判断的依据;更不打算得出”所有团队都该重造工具链”这种结论——绝大多数团队的真实约束不在那个量级上,重造一套系统的成本也换不回来。可复用的是它暴露出来的机制:当变更产出量上一个数量级,卡住的位置会从”能不能写完”挪到”能不能安全合进去、并且出事时能撤回来”。

这个区分决定了整改方向完全相反。产能不足要加输入,容量不足要改结构。写代码这件事变快了,下游四处的容量并没有跟着变大:评审人的注意力、CI 的并发与排队、合并队列的串行化与语义冲突、发布通道的批次大小。四处任意一处先到顶,整条管道的产出就停在它的门口。所以我们做吞吐改造时,第一步不是统计写了多少,而是把四处的容量口径定下来,让”哪一处先撑不住”变成一个可以回答的问题。

还有一点要先说清:吞吐量本身不是目标。一个团队一天合进一百次,如果回滚要两天才能生效,它的真实交付能力比一天合二十次、十分钟回滚的团队差得多。指标只看合得进,就会奖励把大块拆成没人看的碎块。

二、四类瓶颈点先要有可比的测量口径

四处瓶颈的共同麻烦是:它们的表现都不像”慢”,而像”排队””重试””莫名失败”。没有统一口径,各团队报上来的就是三种互不相干的形容词。下面这张表是我们给内部项目定测量口径时用的形状,左列固定,右三列按团队填。

瓶颈点 计数范围 一线指标 先撑不住时的表现
评审人注意力 从变更标记为待评审到给出首次结论的时间与同时挂在身上的条数 首次响应分位值、单人同时持有的待评审数、被打回重评的比例 长尾变更堆积,评审退化为”看标题点头”,意见全是格式与命名
CI 并发与排队 从触发到拿到执行机开始跑之间的等待,加执行本身 排队时长分位值、失败重试次数、被取消的任务数与取消原因 排队时间超过执行时间,开发者开始攒批提交以避免重复排队
合并队列 变更通过全部门禁到真正落到主干之间的等待与重验次数 队列长度、单条在队列中的平均停留、因他人合入而重验的次数 队列长度持续非零,出现”先合的人把后面所有人重跑一遍”的连锁
发布通道 从合入主干到对最终用户生效之间的批次等待 批次大小、批次间隔、单次发布的变更条数 一次发布打包上百条,故障定位只能靠猜,回滚连带无关改动

这张表的价值不在数字本身,在第三列到第四列的关系。有了它,”我们卡在哪一段”这种问法会被换成”哪一条一线指标越过了哪一条线”。在洹拓的内部项目里,我们把越过线这件事写成明确阈值,并且要求阈值必须能报警,只放在周报里看等于不设防。

三、小步提交在代写场景下从好习惯变成硬要求

过去说”小步提交”,理由多是协作礼貌:让人好读、好聊。当变更的大量比例由模型代写,这条理由升级成三条硬约束。

其一是差异可读性。评审人要能在有限时间里判断这段改动的意图与副作用,代码是不是他亲手写的并不影响这一点,影响的是改动范围能不能一眼覆盖。一百多行的改动和两千行的改动,风险差的不只是时间。其二是可二分定位。回归引入点只能靠二分查找定位,一次合进二十个不相关意图的变更,二分出来的区间就没有意义了。其三是可单点回滚。线上要撤的是”造成问题的那一件事”,如果它和另外九件事捆在同一版本里,回滚就等于连带砍掉九项无害改动。

由此推出的操作要求很具体:一条变更只服务一个意图;格式化、重命名与行为改动不混在同一批;不能独立回滚的改动要么拆细要么加开关;提交说明里写清”这件事单独被撤销时会发生什么”。前三条是可以被门禁检查的,第四条可以被模板要求。

四、测试选择必须分三档,每次都跑全量的最先崩

吞吐一上来,第一个死掉的通常是”每条变更跑全量测试”。不是因为全量不好,而是因为全量的时间成本乘上变更条数之后,会把 CI 排队推到不可用的位置,开发者随即开始攒批提交,反而把变更做大了。测试选择因此必须分层,且三档的触发条件要写成规则而不是靠工程师记性。

  1. 改动相关的快速档:目标是在几分钟内给出”这条变更明显不对”的信号,覆盖被改动文件与其直接依赖的单测,附带静态检查。判定失败的后果是当场挡下。
  2. 受影响范围的中间档:按依赖图取受影响子集的集成测试与契约测试,在变更进入合并队列前必须通过。这一档的意义是把语义冲突暴露在小范围里。
  3. 全量的夜间档:完整套件、跨平台矩阵、长时性能用例,按固定批次跑,失败时先归因到当天哪一条变更,再决定是否阻断主干。

三档之间要有明确的升级关系:夜间档反复发现某类问题,就把对应用例提到中间档;中间档耗时越线,就把其中稳定的部分降回快速档。测试选择本身是被吞吐驱动的策略,不是一次性配置。另外要留一条逃生口:主干出现紧急修复时,走加急通道跑最小必要集合,但加急的次数与事由必须记录,否则它会变成常态。

五、评审带宽按风险分级,合并队列要当一等对象

评审带宽是四格里最贵的,因为唯一有效的资源是熟悉这块代码的人的注意力,加人解决不了,新人反而增加解释成本。能做的只有两件事:把注意力按风险分配,把机械性判断交给门禁。

变更风险级别 判定依据 必须到场的人 门禁动作 能否降级
高:改对外接口、数据结构、权限与资金路径 影响面跨服务,或变更涉及不可逆操作 模块负责人加安全或数据侧一人 三档测试全跑,要求书面影响说明与回滚方案 否
中:单服务内部逻辑改动 影响面可枚举,行为变更可被测试覆盖 模块内一名评审 快速档加中间档,夜间档事后补 可在队列紧张时延后
低:纯机械性改动 由工具产出、可用规则验证,如依赖版本、格式化、批量替换 无须人工,规则即评审 自动门禁通过即合入,保留抽样复核 可
待定:判定依据缺失 提交说明不足以判断意图 退回补充,不进队列 阻断并记录退回原因 否

分级的前提是把评审意见结构化。散文化意见无法统计,也就无法知道带宽消耗在哪一类问题上。我们的做法是每条评论挂一个类型字段:意图不清、边界漏项、性能顾虑、可读性、与既有约定冲突。三个月就能看清哪一类在吃时间,进而把其中可自动化的部分下沉成检查项。

合并队列常被当成一个平台自带的黑盒,其实它的工程细节决定成败。三件事必须自己盯:语义冲突检测,不能只判文本冲突,两条各自通过的改动叠在一起可能整体失败,因此队列里每条变更都要针对已经落到基线上的版本重验;按需重验,重验成本极高,要靠影响面判断决定重跑哪一档,而不是无脑全跑;队列长度必须作为一线指标报警,一旦它持续非零,说明重验吞吐小于合入请求吞吐,这是结构问题,加机器只是延缓。

六、回滚粒度的四个前提,以及那把尺子的两种失真

吞吐翻上去之后最容易出事的地方是回滚,因为大家默认”回滚是一个按钮”。它成立需要四个前提,少任何一个,回滚就退化成”再合一次修复”。

  • 一次变更只做一件事:这是粒度基础,捆了三个意图的变更无法单独撤销。
  • 未完成功能被开关隔离:功能开关让”合入”与”生效”解耦,回滚可以是关开关而不是退版本。
  • 数据库变更可逆:删列、改类型、加约束这三类必须先想好回退路径;不可逆的变更要与代码变更错开批次合入,并单独登记。
  • 回滚演练真做过:只在事故里第一次使用的流程一定会现出短板,包括权限不在值班人手上、回退后数据不一致没人判定这类问题。

再回到”日均合并次数”这把尺子。它有两种典型失真,第一种是分母膨胀:把本应一次完成的改动拆成若干条无人细看的碎块,次数上升、风险不变、评审质量下降;第二种是幸存者偏差:只统计合入成功的次数,被队列丢弃、被门禁挡下、被作者自己撤回的都不在账上,于是数字越漂亮越可能说明管道在大量空转。两种失真都不是理论问题,它们的共同特征是首次响应时间和重验次数同时上升而合并次数继续涨——看到这组组合,正确的动作是回头改结构,而不是继续把次数当能力指标报上去。