一、一份挑不出毛病的优化记录
最近评审了一份团队内部的 CI 优化记录,坦白说,做得很漂亮。
背景是一个 Rust workspace 项目的 GitLab CI,check job 整体耗时 100 多分钟。优化手段堪称教科书级别:串行 per-crate 编译改成 workspace 级单命令并行编译,消除底层依赖的重复编译;去掉重复执行的格式检查;把测试从按模块串行(日志里出现 7 次重复编译,约 23 分钟纯浪费)改成单命令跑完;缓存安装包、跳过不需要的工具链组件;每个阶段打印时间戳。
每一步都有数据:优化前 pipeline 100+ 分钟,优化后约 52 分钟。before/after 表格清晰,结论克制,连"runner 硬件才是瓶颈"这种不利于自己的观察也如实记录了。
如果只看这份文档,这是一次模范的效能改进。
但把它读了两遍之后,我意识到它有一个贯穿始终的隐含前提:所有的收益,都是用"耗时"这一本账计量的。 而一次 CI 优化至少要过两本账——这份记录只交了第一本。
二、第一本账:资源账——你的快,是谁买的单
优化手段其实分两类:
A 类:减少总工作量。 消除 7 次重复编译、删掉冗余步骤、跳过不需要的组件下载——系统真的少干活了。这类优化在固定硬件下是纯收益:每个 job 少占 runner 时间,所有人的排队都变短。
B 类:提高瞬时资源占用。 串行改并行,总计算量没变,只是把"一两个核跑一小时"压成"全核跑二十分钟"。这类优化的前提是这台机器此刻归你独占。而一台物理机并发跑多个 job 是最常见的 runner 配置——原来的串行编译其实是"有礼貌的",改成并行后默认吃满整机。
这个 job 的 22 分钟是真的;同机邻居 job 集体变慢、方差变大,也是真的。个体的快,是从邻居身上抢来的——只是账单寄到了别人家,所以进不了你的优化报告。
更微妙的是,文档自己记录了一个 300ms deadline 的测试偶发超时,作者正确判断为"高负载下的 flaky"——却没意识到并行化方案本身就在制造更多高负载时刻。优化在加重它记录的问题。
三、第二本账:语义账——有些代价不出现在耗时表格里
资源账已经够让人警醒了,但在这个项目里,真正贵的是另一本账。
先说这个项目的性质:它不是"早点上线抢市场"的互联网产品,而是一个仿真/测控类的工程系统——时序敏感、有真实硬件接口、可靠性是命门。这类系统是"正确性定价"的:它的价值锚点是正确,不是快。 编译快 10 分钟,省的是工程师的等待;一次时序 bug 被掩盖着漏进系统,赔的可能是整条数据链的可信度。收益是线性的,代价是非线性的。
用这个标尺重新看那些优化手段,会发现另一组分类:
无损的快(不改变任何语义):消除重复编译、升级 runner 硬件、用内容哈希做编译缓存。快得越多越好,稳不受损,纯赚。
有损的快(拿语义换速度):
--all-features并集:整个 workspace 的 feature 取并集一次编译,测的是一个"生产上从不存在"的组合——既可能撞互斥 feature,也会掩盖单个 crate 的 feature 声明缺失;- 测试并发度拉满:原来串行执行天然隔离,并发后共享全局资源(虚拟网卡、端口、单例)的测试互相干扰——更要命的是,那个 300ms 的 flaky 测试本身就是时序竞争的产物,提高并发恰恰是降低了这类竞争被暴露的概率。“快"在制造"没发现”;
- 缓存复用:用环境纯度换速度,“有缓存的绿"从此不等于"干净的绿”。
注意这类代价的共同特征:它不让任何东西变慢,所以不进耗时表格;它甚至不一定让 CI 变红,所以不进失败统计。它表现为未来某次事故复盘时的一句"这个场景 CI 其实测不到"。 这是所有成本里最难归因、也因此最容易被忽略的一种。
四、配置底座早就投过票了
有个细节很有意思。翻这个项目的测试配置:--test-threads=1(默认串行)、retries=0(不自动重试掩盖失败)、fail-fast;那个 flaky 测试,团队宁可调参也不敢 ignore。
这套配置底座,从头到尾都站在"稳"这一边。 团队的工程直觉一直是对的——稳是这个项目的底线。
问题出在最近一个优化 MR 里:它一边把 test-threads 从 1 改成 16(有损),一边给覆盖率 job 加缓存(基本无损),两笔账混在同一个提交里,一起过审、一起合并。这不是哪个人的判断失误,是评审机制里根本没有"这笔快,伤的是哪本账"这个问题。直觉保住了底线,但没有机制把直觉显性化,直觉就只能在每次 MR 里碰运气。
五、两本账合成一张表
资源账和语义账是两根独立的轴,把所有优化手段放进这张 2×2 里,该做什么一目了然:
| 语义无损 | 语义有损 | |
|---|---|---|
| 资源无损/节省 | 消除重复编译、升级硬件、内容哈希缓存 → 放开了做 | 缓存复用 → 配治理:缓存 key 设计、定期清理、保留无缓存基准 pipeline |
| 资源抢占 | 并行编译 → 配隔离:专用 runner、显式限制核数 | 弱共享 runner 上拉满测试并发 → 默认拒绝,设门槛单独评审 |
规则只有一句话:稳定手段(硬件、隔离、去重复)用尽之前,不动语义。
六、为什么研发牵头时容易漏掉第二本账
类似模式在效能和 DevOps 领域反复出现,而且有个规律:研发同学牵头的改进尤其容易中。这不是能力问题,是位置问题。
工具箱决定视野。 研发看到的是命令、日志、编译时间,优化自然落在命令行参数上;调度器、并发槽位、共享宿主机、测试语义——代价的落点在研发的日常界面里不可见。
叙事结构有偏向。 before/after 表格天然是单点叙事,清晰、可量化、好汇报。而全局代价弥散在别人的队列里、未来的事故里,无法归因到任何一次 MR,自然进不了任何优化记录。
度量选择了结论。 用"单 job 耗时"做指标,优化就朝单 job 收敛。优化了你看得见的,代价转移到你看不见的——度量方式决定了什么看得见。
约束理论有句老话:在非瓶颈环节做优化,系统吞吐不会变,只是浪费换了个地方。在 CI 场景下要补半句:有时候不只是浪费换地方,是成本换地方——从你的报表,换到别人的队列里、未来的事故里。
公平地说,研发牵头做效能改进绝对是好事,没人比写代码的人更懂构建的痛点。缺的不是能力,是把全局纳入视野的度量框架和评审习惯。
七、证据的不对称:沉默是这样被训练出来的
但还有一个更扎心的问题:很多时候,隐性代价并非没人看见——DevOps、QA 这些防守型角色常常看得清清楚楚。他们只是说了也没用,最后选择了沉默。
还原一下那个场景。评审会上,提速方把柱状图投到大屏:100+ 分钟对 52 分钟。主持人问:“还有意见吗?“你想说"并发拉满会打爆共享 runner"“测试语义变了,可能漏测”。对面只需要回一句:“你举一个已经发生的例子?”
你举不出来。漏测的样本天然为零——漏掉的就是没发生的。 更狠的是,优化本身会消灭证据:并发调度把时序竞争的触发窗口喂没了,那个 300ms 的测试从此每次都恰好赶在 deadline 前完成——不是 bug 没了,是你连"它原本会红"的证据都被自己的优化抹掉了。桌上是一张耗时下降 50% 的折线图,你手里是一句"万一”。数据对不上恐惧,恐惧就自动降级为"过度焦虑”。
一次两次之后,人就学会了闭嘴。沉默不是被说服,是被训练出来的——而且它有三重强化机制:
第一,KPI 对冲。 效能指标挂在提速方的绩效上,你反对提速,就是动别人的年终奖。
第二,归因不对等。 真出了事,复盘报告写的是"测试覆盖不足"“监控没跟上”,从来不会写"当初提速决策激进"。所以反对的收益恒为零、成本恒为正。
第三,沉默是个人的理性最优解。 开口会被记成"阻碍效能的人";闭嘴,最多半夜被叫起来兜底。用一位运维的原话说:“你们大屏上那个 52 分钟,我看着心慌——不是心慌你跑得快,是心慌出了事半夜被叫醒的是我,领效能奖的是你。”
所以"DevOps 选择了沉默"从来不是性格问题,是结构问题。在一个只奖励可举证收益的系统里,防守方的沉默就是均衡解。 而团队表面上看起来"评审高效、没有冲突",实际上是冲突被吞了下去——“安全地不说话"悄悄替代了"安全地说真话”。一位敏捷教练的话值得贴在每个评审会议室里:一个团队真正危险的不是有人反对,是反对的人学会了自己咽回去。
八、怎么办:让"万一"重新变成可对账的东西
破局的钥匙藏在上面的诊断里:既然沉默的根源是举证责任全压在防守方,而防守的对象是无法举证的反事实,那就别要求防守方举证。具体四件事:
1. 评审四问,例行化。 任何效能优化方案过审时问:总工作量变了吗?资源假设变了吗?测试语义、环境纯度、feature 组合变了吗?失败成本变了吗?四个问题写下来,比任何激情辩论都管用。
2. 举证责任倒置,但别一刀切。 触发线要提前划清楚:只有触及语义、契约、环境纯度的改动才触发倒置,纯去重复、加硬件这种不触发。倒置的形式也要现实——“证明无害"在逻辑上做不到(证明不存在是不可能的),所以降级成提速方提交一份风险边界清单:我考虑过哪些边界、为什么认为没碰到。举证成本降下来了,机制才推得动。而且一定要把它说成"团队共同的对账表”,而不是给提速方加的关卡。教练那句话说得透:举证倒置不是给提速的人加关卡,是让"我觉得这里有问题"这句话重新值钱。
3. 给风险留对照组,让反事实变事实。 这是把"万一"变成可对账数据的唯一办法:保留一条串行的 nightly 回归通道,让时序竞争类 bug 有一个稳定的暴露面——它不是倒退,是对照组;周期性做无缓存的 clean rebuild,对比"有缓存的绿"和"干净的绿"的差异集,差异非空就说明缓存在掩盖东西;提速导致的事故,复盘必须回溯到决策环节,让"当初有人反对过"事后能被看见。QA 的原话可以作为这一节的设计原则:“别把串行通道砍了,留它当夜里的狗,叫了你就知道有人在翻墙。狗不叫不代表没贼,可你把狗也优化没了,那我就真只能闭嘴了。”
4. 仪式层面,给沉默者一个开口的结构。 评审会固定留三分钟"红队时间",唱反调是角色而不是立场,与个人脱钩;回顾会先用静默写便签再轮流发言,逼沉默者先落字;Leader 要带头说"上次这个我也漏看了"——示范打破沉默不会被惩罚。
九、结语
那份优化记录我依然认为优秀——纸面上的每一项收益都是真的。问题从来不在纸面上,在纸面外。
局部优化和全局视角之间,差的不是智商,是账本的单位。不成熟的效能工作用"这个命令多少秒"记账;成熟的效能工作记的是:系统为这次验证支付了多少资源、排队的人等了多久、下次失败要赔多少——以及,这次变绿,还和上次变绿是同一个意思吗。
对一个正确性定价的系统,CI 的意义从来不是快,是可信。快不等于省,而可信度的折旧,是所有账单里最贵、也最看不见的一张。
最后留一个画面。下次评审会上,如果有人对着漂亮的提速数据一言不发,别把那份沉默当作没有意见——他可能只是早就计算过了:他的那本账,在这个会议室里永远无法举证。机制存在的意义,就是让这些人重新开口的成本,低于沉默的成本。
延伸阅读:这些观点从哪里来
这篇文章里的每个论点,在成熟的方法论里都有更早、更系统的表述。如果你觉得某个点说得不透,可以直接去读原著:
关于"局部优化 ≠ 全局变好"
- 《目标》(高德拉特,约束理论 TOC)——优化非瓶颈环节,系统吞吐不会变。“浪费换地方"的理论源头。
- 《精益软件开发》(Poppendieck)——七大原则第一条就是 “Optimize the Whole”,本文主旨的直接出处。
- 《This is Lean》(Modig & Åhlström)——“资源效率 vs 流动效率"这对矛盾讲得最透:并行吃满整机是高资源效率、低他人流动效率,本文的 A/B 类分类是它的变体。
- 《系统之美》(Donella Meadows)——“账单寄到别人家"本质是系统边界的划定问题。
- 《产品开发流程的原则》(Reinertsen)——排队论在研发流程中的应用,Little’s Law 与共享资源池。
关于"CI 的绿必须代表什么”
- 《持续交付》(Humble & Farley)——CI 的前提是构建状态是可信赖的质量信号。
- Hermetic Build(Bazel 设计理念)——可复现构建与环境纯度,缓存治理的理论源头。
- Google SRE Book 第 11 章 “Testing for Reliability”——测试通过和可靠性之间的认识论鸿沟。
- Martin Fowler《Eradicating Non-Determinism in Tests》——flaky test 的经典文献。
关于"稳如何变得可对账”
- SRE 的 SLI/SLO/Error Budget——把"稳"翻译成和"快"同一种度量语言,是"万一打不过表格"的官方解法;error budget 用完冻结发布,就是"红线 + 自动回退"的成熟形态。
- 《管理意外》(Weick & Sutcliffe,HRO 高可靠性组织理论)——研究对象是核电站、航空母舰、空中管制这类"正确性定价"的系统。第一条原则"Preoccupation with failure"和鼓励上报近失事件,对应文中的"已知边界清单”。
关于"沉默与举证不对称"
- 《无畏的组织》(Amy Edmondson,心理安全感)——“安全地不说话 vs 安全地说真话"是她研究的核心。
- Westrum 组织文化模型(病态型/官僚型/生产型,DORA 采纳为文化度量)——坏消息能否被说出来,是区分三类文化的关键指标。
- Just Culture(Sidney Dekker)——不指责复盘的理论基础,对应"复盘回溯到决策环节”。
- 举证责任倒置的现实原型不在软件行业,在药物审批(FDA)和适航认证(FAA):变更方承担举证责任,是高风险行业的通行设计。
- Premortem 事前验尸(Gary Klein)——“假设半年后失败了,现在写失败报告”,比临时唱反调更结构化的"红队三分钟"。
用一句话收拢这份书单:“两本账"是精益和约束理论的骨架,“沉默"是心理安全感和组织文化模型的血肉——方法论一直都在,本文只是记录了它们在"研发牵头的 CI 优化"这个具体场景里,是怎么集体失效的。