中位数 2.2 分钟,均值 9.9 分钟:AI 时代的等待时间去了哪

About 16 min read

读 CircleCI 2026 的近 2800 万条流水线、Incredibuild 的千级开发者抽样,以及那个被引用了十几年、却查不到出处的「23 分钟」。

Contents · 7 sections · jump to a heading

2026 年 2 月 18 日,CircleCI 发了第七份 State of Software Delivery。样本是 2025 年 9 月前 28 天里的近 2800 万条 workflow,来自 149 个国家的 22000 多个组织。

同一份报告里有两个方向相反的数字。全平台日均吞吐同比涨了 59%;主分支成功率跌到 70.8%,是五年最低。

第三个数字更值得停下来看:workflow 的中位时长是 2.2 分钟,比上一年快了 18%;而均值是 9.9 分钟。

流水线本身在变快,人花在等它上面的时间却在变长。这两件事同时为真。

这篇文章想回答三个可以被验证的问题:

  1. 流水线到底快不快?
  2. 那十几分钟被谁吃掉了?
  3. 被引用了十几年的「23 分钟才能回到心流」,站不站得住?

我做不到什么

  • 我没有那 2800 万条流水线的原始数据。 所有数字都是 CircleCI 自己发布的,我只是把它们放在一起核对。我做的那点算术会标出来是算术。
  • 我没有复现 Incredibuild 的抽样。 他们的样本量、问卷设计和统计口径以发布的报告为准,我只引用报告里印出来的数。
  • 我查不到「23 分 15 秒」的同行评审来源。 这一节会把我找到的东西和没找到的东西分开写,不合并成一句结论。
  • 下面那份 YAML 没有在你们的 CI 上跑过。 它在本机做过语法检查,跑没跑通,取决于你们的锁文件和测试框架。

先看三个数字

CircleCI 这份报告里,我认为最该被记住的是这三个。

指标2025 年同比
workflow 中位时长2.2 分钟快 18%(2024 年为 2.7 分钟)
workflow 均值9.9 分钟快 10%(2024 年为 11 分钟)
主分支中位 MTTR72 分钟慢 13%

第一个和第二个数字之间的差,就是这篇文章的全部内容。中位数 2.2 分钟意味着一半的流水线在两分半内结束;均值 9.9 分钟意味着剩下那一半里,有一批跑得极慢。均值被中位数甩开 4.5 倍,这不是正态分布,是一条长尾。

长尾从哪里来,报告自己给了线索:feature 分支上的 MTTR 从 2024 年的 64 分钟涨到了接近 80 分钟,涨幅 25%。 报告给的解释是 feature 分支上的测试套件反馈通常不完整。翻译一下:主干上跑得快的那些检查,在分支上没有跑,所以问题要等到更晚才暴露,暴露之后的修复窗口更长。

再看吞吐。全平台日均吞吐涨 59%,但这个涨幅高度集中:

  • 前 5% 的团队:6.8 次/天 → 13.4 次/天,接近翻倍
  • 前 10%:+47%
  • 前 25%:+25%
  • 中位数团队:+4%
  • 后 25%:无可测量的增长

报告用了一个说法:AI 不是在制造性能差距,是在暴露它。这句话有点公关味,但数字本身是硬的——代码产出速度的提升是普遍的,把它变成可交付变更的能力是集中的。

时间被谁吃掉

把一次流水线拆开,时间花在四类地方,性质只有两种。

content/_code/ci-speedup/pipeline.yml

1
name: ci
2
3
on:
4
pull_request:
5
push:
6
branches: [main]
7
8
concurrency:
9
group: ci-${{ github.ref }}
10
cancel-in-progress: true # ← 同一分支推第二次,直接掐掉上一轮,不排队

上面这段是「并发排队」那一类的最便宜的解法:cancel-in-progress。同一分支上推了第二次,第一次的任务直接取消,不再排队等机器。四行配置,省掉的是纯粹的空等。

剩下三类,性质不同:

冷启动与装包,属于「重复劳动」。 每次运行从公网重新拉一遍依赖,磁盘和网络 I/O 吃掉了时间,而这些依赖上一次运行刚刚拉过。这类时间的特征是它可以被缓存掉,且缓存掉没有信息损失。

串行测试,属于「并行度不足」。 单线程跑一条测试套件,CPU 只用一个核。这类时间的特征是可以靠分片摊平,但摊平有固定开销。

Flaky 重跑,属于「假信号」。 一条用例因为动画时序或网络抖动飘红,整轮重跑。这类时间最贵,因为它不只吃掉等待,还吃掉信任——红灯多了之后,团队开始习惯性地点 Re-run,真正的问题会被淹没在噪音里。

三类对应的动作写在 pipeline.yml 里,分片那一段长这样:

content/_code/ci-speedup/pipeline.yml

1
test:
2
needs: scope
3
if: needs.scope.outputs.affected == '1'
4
runs-on: ubuntu-latest
5
strategy:
6
fail-fast: false
7
matrix:
8
shard: ${{ fromJSON(needs.scope.outputs.shards) }}
9
steps:
10
- uses: actions/checkout@v4
11
12
- uses: pnpm/action-setup@v4
13
with:
14
version: 9
15
16
- uses: actions/setup-node@v4
17
with:
18
node-version: 22
19
20
- name: 复用 pnpm store
21
uses: actions/cache@v4
22
with:
23
path: ~/.local/share/pnpm/store
24
key: pnpm-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }} # ← 锁文件没变就命中
25
26
- run: pnpm install --frozen-lockfile
27
28
- name: 跑第 ${{ matrix.shard }} 片
29
run: pnpm test:run --shard=${{ matrix.shard }}/4

注意 hashFiles('pnpm-lock.yaml') 这个 key。缓存命中与否完全由锁文件决定,不掺时间戳、不掺分支名。缓存 key 的写法决定了缓存是资产还是负债。

「23 分钟」这件事,需要单独说

我最初写这篇的时候,开头准备直接引用那个数字:被打断后平均需要 23 分 15 秒才能回到原来的任务,所以等 CI 十分钟的代价是 23 分钟。

我去找了原始出处。找不到。

公开可追溯的源头是 2006 年 Gallup 对 Gloria Mark 的一次采访。她在采访里提到了这个时长,它没有作为正式的研究发现发表过,也就没有经过同行评审。

有人把常被引作来源的几篇论文逐篇查了一遍,结论是没有一篇包含这个数字。其中被引用最多的是《The Cost of Interrupted Work: More Speed and Stress》,而这篇的结论方向和流行说法相反:

  • 有中断时,原任务耗时 20.31 到 20.60 分钟
  • 无中断时,原任务耗时 22.77 分钟
  • 也就是说,被打断的人更快完成了任务,代价是压力水平显著更高

Mark 自己在 2005 年的研究给出的平均恢复时间是 25 分 26 秒,返回原任务前平均插入 2.26 个任务。她 2008 年那篇 CHI 论文的摘要写的是同一件事:被中断的任务完成时间更短,质量没有差异。

有人统计过 23 篇引用「23 分 15 秒」的博客:9 篇错误引用了论文,2 篇引对了,9 篇追溯到那次采访,2 篇引的是《华尔街日报》的转述,还有 1 篇引用了在被引论文里根本找不到的话。

所以这个数字的正确用法是:不要当测量值用。

代价在于,砍掉它之后,「等 CI 很贵」这个结论就少了一根支柱。它剩下的支柱是报告里的实测数字:中位 MTTR 72 分钟、主分支成功率 70.8%、500 次/天改动的团队在 60 分钟恢复时间下等于损失 12 个全职工程师的工作量。这些数字有明确的样本和口径,比一个查不到出处的 23 分钟好用。

收益是,论证变干净了。原来的链条是「被打断要 23 分钟 → 所以等 CI 很贵」,中间靠一个来源不明的数字撑着。现在的链条是「流水线失败后中位要 72 分钟才回到绿色 → 所以恢复环节才是瓶颈」,每一步都能查到出处。

四个动作,和它们各自的账单

前面几节都在讲哪里能省时间。这一节讲代价,因为省时间从来没有免费的版本。

缓存依赖。 省下的是每次运行重复下载依赖的时间,通常能砍掉最大的一块固定开销。代价是缓存会掩盖「干净环境」下的问题:本地能过、CI 也能过,但换一台没有缓存的机器就炸。另外缓存 key 一旦写错(比如掺进了 github.run_id),命中率会静默归零,你以为在省时间,实际一次都没命中。上线缓存之后要去看命中率,不是看配置写没写。

测试分片。 四片并发理论上把 10 分钟摊成 2.5 分钟。代价有三个:分片有固定开销,机器越多开销占比越高,超过某个片数总时长反而回升;同一用例在不同分片上的时序不同,flaky 会变多而不是变少;分片数写死在 workflow 里,测试量涨了要记得回来改。上面那份 YAML 把分片数写成一个 outputs,就是让改动只发生在一处。

变更感知。 只跑被改动文件影响到的测试,在大型仓库里省下的时间最多。代价是它需要一张真实的依赖图,而依赖图会过期。图错了的后果是不对称的:多跑几分钟只是慢,漏跑一条是直接上生产。这条的默认值应该是保守的——图不确定时,按全量跑。

调整测试金字塔。 把脆而慢的 UI 级测试砍到只留一条核心路径,逻辑交给毫秒级的接口契约测试。代价是丢掉真实渲染路径的覆盖:API 契约测试能告诉你接口返回了正确的 200,不能告诉你按钮被一个弹窗盖住了。冒烟 E2E 的价值不在覆盖率,在它挂掉时你会得到一张截图和一份 trace。

pipeline.yml 里最后那一段就是为这件事写的:

content/_code/ci-speedup/pipeline.yml

1
- run: pnpm exec playwright test e2e/smoke.spec.ts --trace=retain-on-failure
2
- uses: actions/upload-artifact@v4
3
if: failure() # ← 只在红灯时留证,绿灯不占存储
4
with:
5
name: playwright-trace
6
path: test-results/
7
retention-days: 7

--trace=retain-on-failure 加 if: failure() 的组合,是这套改动里性价比最高的一处:绿灯时零开销,红灯时你手上有一份可以直接回放的证据。没有它,冒烟测试就只是一盏灯。

收尾

这份报告的数据采集窗口是 2025 年 9 月的前 28 天,明年这些数字都会变。CircleCI 是卖 CI 的公司,它的报告同时是一份营销材料,读的时候要把这层关系算进去。

但有两件事不随版本变化。

第一,中位数和均值之间的差距,永远比中位数本身更值得看。 中位 2.2 分钟、均值 9.9 分钟这个组合告诉你,优化对象不是「把平均流水线做快」,而是「把那条长尾剪掉」。长尾通常来自少数几个又慢又脆的任务,不是来自所有任务的均匀分布。

第二,等待的成本不在算力账单上,在恢复窗口上。 中位 MTTR 72 分钟意味着一次失败之后,从红灯到绿灯的平均成本是 72 分钟,而不是那 2.2 分钟。想缩短它,动作不在「让测试跑得更快」,在「让失败更难发生、发生之后更容易定位」。

剩下的距离,全在测试设计的那一侧。

参考来源

Share this note on Twitter / X · Browse all articles · Feedback via @qianyuhe.

Thanks for reading.

– 千羽鹤