Linear 把 CI 从瓶颈跑成了顺差:测试量翻 3.7 倍,机器时间反降 46%

Linear:测试量增长 3.7 倍的同时,单测试机器时间下降 46%

AI 写代码的速度把验证环节甩在了身后。Linear 的工程师 Mufeez Amjad 在 9 月 21 日发表的工程复盘(AI coding has made CI a bottleneck, so we reworked ours to keep up)里给出了一组被广泛引用的数字:年内测试套件涨到原来的 3.7 倍,PR 在 CI 上的等待时间从 6 分多钟降到 5 分出头,单测试机器时间砍掉将近一半。钱也省了——单是把七个零散检查合并成两个 job,每月就少烧 87,000 个 runner 分钟,占全公司 CI 用量的 11.8%。

这组数字之所以值得逐项拆开看,在于它把「AI 编程加快之后基础设施怎么跟上」这个抽象问题,落成了一份可复用的清单。下面的解读按原始文章的四个板块走:基础设施与工具链、关键路径上的门禁任务、重复 setup、测试执行效率。

先看总量:四个板块各贡献了什么

Linear 的代码库以 TypeScript 为主,文章把全部优化归入四类。官方汇总图给出了每一项的降幅:

Linear 官方汇总:从工具链到总账的逐项降幅

工具链与基础设施:type-checking 降 73%(换用原生编译器 tsgo)、API lint 降 68%(重写规则摆脱类型信息)、全部 job 换到更快 CPU 的第三方 runner 后平均提速 33%。

门禁任务:缓存写入移出必需检查降 89%、最慢的变更检测 gate 降 79%、超过 30 秒的 checkout 降 77%、直接砍掉 gate 任务里的 checkout 降 69%。

setup 与测试套件:单 shard setup 降 55%、最慢的 API shard 降 27%。

总账:单测试机器时间降 46%,开发者感知的 PR CI 时间降 12%(6 分钟级降到 5 分钟级)。

四个板块的杠杆率完全不同。工具链和门禁任务以 60%-90% 的单项降幅提供了早期收益;测试执行效率板块的单项数字看似温和,但它是用量最大的部分,机器时间降 46% 的总账主要由这里贡献。换 runner 是「同样的流水线换台快机器」,属于一次性买断;后面的板块才是结构性改造,也是这篇文章区别于普通「我们加速了 CI」分享的地方。

板块一:换机器与换工具链,最便宜的两笔收益

最早的收益几乎没动 CI 逻辑本身。Linear 把工作负载从 GitHub Actions 迁到第三方 runner——CPU 更快、存储和缓存基础设施更好。切换日期前后两天的同条件对比里,job 平均快了 34%,部分工作负载如 tsc 直降 52%。

工具链的收益更大。tsgo 是 TypeScript 的原生编译器实现,Linear 把每周中位数的 tsc 检查时间降了 73%,幅度大到 typechecking 从此不再是瓶颈环节。作为对照,这个数字也解释了为什么类型检查曾长期是 TypeScript 项目 CI 的最大单项。

lint 这笔账的解法更有参考价值。Linear 的一部分自定义 lint 规则依赖 TypeScript 类型信息——要么为了强制某种约束,要么为了做自动修复。这导致每次 lint 都要先构建完整类型图,lint 因此成了最耗内存的 CI 任务之一。团队的解法是把规则重写为基于 AST 的静态分析:识别函数式构造和守卫模式不需要类型信息。ESLint 由此甩掉了 TypeScript 依赖,API lint 时间降 68%,全仓 lint 时间降 55%,内存占用大幅下降。

这次重写还为后续迁移 Oxlint 铺了路——纯语法层面的规则移植成本极低,Oxlint 上线后进一步压低了 lint 的 runner 分钟数。

板块二:门禁任务——几十秒的优化为什么值得写

CI 是个系统,优化它的关键在那些「卡在所有事情前面」的小任务。每次运行都从「这个 PR 改了哪些路径」「这些测试是否已对相同输入通过」开始,8 个 API 测试 shard 全部等这两个检查结束才能启动——位置决定了即使很小的延迟也会被放大。

按需拉取。变更检测类 job 只需要工作树的一小部分,却在完整 checkout。Linear 给 fetch 加了深度上限,最慢的 gate 从 94 秒降到 20 秒;完全不需要工作树的 job 直接移除 checkout,从 27 秒降到 7 秒;对必须 diff 路径的 push 和 merge-queue 事件,改用带有限历史的 sparse、blobless checkout,再省约 11 秒。变更检测 job 的中位时长从 26 秒降到 8 秒,p90 从 31 秒降到 12 秒,最慢一次从 138 秒降到 37 秒。

checkout 抗网络抖动。迁到第三方 runner 后 checkout 反而变慢甚至挂起,原因是第三方 runner 在 GitHub 网络之外,依赖一条直连 IP 链路,链路间歇性劣化。Linear 用自己的 composite action 替换 actions/checkout:指数退避重试、设置 GIT_HTTP_LOW_SPEED_LIMITGIT_HTTP_LOW_SPEED_TIME 让停滞连接约 30 秒后中止而非永久挂起、并启用 checkout 缓存——在粘性磁盘上维护一个持久 git 镜像。

移出关键路径。缓存标记原本写在合并前最后一个检查里,PR 测试全过之后还要在 merge queue 里干等它写完。把它挪进一个测试 shard 结束后运行但不阻塞任何事情的 job,每个 API PR 和 merge-queue 条目的合并路径省 42 秒。

这三项加起来,cache miss 时 API PR 的必需检查缩短约 1 分钟,runner 启动次数也随之减少。

板块三:重复 setup——每个 job 都在白付的固定成本

一个只干几秒钟有用功的 job,加上启动 runner、装依赖、准备构建环境,能吃掉整整一分钟的机器时间。Linear 的三个动作:

预装共享依赖。每个 API 测试 shard 每次运行都花 7-8 秒用 apt 装同一个 Postgres 客户端。把它做进含 Node 的 CI 基础镜像,shard 从「就绪环境」启动;后来又发现运行时下载 native 构建头文件偶发挂起,把头文件也预进镜像,收掉了长尾。

按 job 装依赖。Linear 是 pnpm workspace 管理的 monorepo,API 测试工作流却在装整个工作区。限制到 API 包及其依赖后,pnpm install 从 44-73 秒降到 16-18 秒。同样的模式应用到 API 周边任务,它们各自全量装仓库、上传几乎永远命中不了的依赖缓存。

缓存不划算就别缓存。Linear 测试了缓存 node_modules,结论是重建更快:缓存键挂在频繁变动的 lockfile 上,命中也要约 28 秒恢复,而过滤安装只要约 7.5 秒。缓存在白付保存时间和变异性,没有可辨识的收益。

三项合计,单 shard setup 从 110-140 秒降到 67-73 秒,降幅约 44%。还有更彻底的免重复:API 容器每次运行都重放完整数据库迁移历史,即使 PR 根本没动 schema——改用生成的 schema 快照加 bootstrap 文件后,数据库 setup 从约 12 秒降到 1-2 秒。

板块四:测试执行——46% 总降幅的主要来源

setup 的固定成本降下来之后, aggressively 并行化才变得划算。这一板块有两处最值得细读的设计决策。

按测试运行器的视角平衡负载。Vitest 按文件而非单个测试的时长分配任务,几个异常大的测试文件就能主导一个 shard、拖住整个套件。Linear 把大文件拆小(保持测试结构),并把 API 套件从 4 个 shard 扩到 8 个:关键 job 在基准测试中快了约 19%、便宜了约 19%;一周后最慢 shard 从 5.25 分钟降到 4.33 分钟。

短检查合并前后:job 图从七个独立启动收敛为两个

共享模块状态,但要立规矩。Vitest 默认隔离每个测试文件,代价是每个 worker 都要重建 entity、GraphQL 和 decorator 图。Linear 引入 opt-in 的 vitest project(isolate: false),让安全文件在每个 worker 内共享模块注册表——这是全篇最大的单项收益,按他们的用量约值月度省 17%,最慢 shard 从 300-379 秒降到约 195 秒,单次运行的 API shard 总 runner 时间从约 32.8 分钟降到 22 分钟。

风险与收益同样突出。Linear 的做法是把资格显式化:每个共享文件必须带 opt-in 注释、补齐共享状态所需的 teardown;少数用 fake timers 或以无法安全解耦的方式共享状态的文件,留在隔离 project 里。他们还更新了内部 agent skills——现在 Linear 内部多数测试由 agent 编写,生成测试也默认遵守这套约束。

shard 数量被 setup 成本锁顶。shard 加倍,setup 时间也加倍,所以并行化只有在单 shard 固定成本足够低时才有收益。优化前 setup 110-140 秒时,8 个 shard 光 setup 就要 15-19 分钟,超过测试本身;现在 setup 约 40 秒,8 个 shard 的总 setup 时间比优化前 4 个 shard 还少,并行度却翻倍。

没做这批优化会怎样

文章结尾给了一组反事实数字:不优化的话,今天跑一遍测试套件要约 11 分钟,接近开发者实际等待时间的两倍;而且这套件的增速没有放缓——Linear 目前每周新增约 2,000 个测试。CI 优化不是一次项目,是跟着代码库增长持续做的工作。

把 46% 的机器时间降幅和 11.8% 的单点用量节省放在一起,这件事的财务侧也说得通:runner 分钟是按量计费的基础设施开支,测试量 3.7 倍的背景下,不做的成本不是「慢一点」,是按倍数膨胀的账单和排队的开发者、agent。

可复用的清单

按投入产出排序,其他团队可以对照自检:

  1. 换更快的 runner——不动逻辑,同条件对比就能拿到 30% 上下的提速
  2. 工具链升级——原生编译器(tsgo 这类)往往有数量级差异
  3. 给 lint 脱掉类型依赖——AST 静态分析重写规则,单项 55%-68%
  4. 审门禁任务——变更检测类 job 别做完整 checkout,fetch 深度设上限
  5. checkout 加退避重试和低速中止——外部 runner 的网络抖动不该挂死整条流水线
  6. 依赖装进镜像——按 job 过滤安装范围,缓存键挂在 lockfile 上时算清楚命中率
  7. 短检查合并——七个各起一个 runner 的检查合成两个,setup 只付两次
  8. 测 shard 平衡——按运行器实际分配视角(文件级)切分负载,再扩 shard 数
  9. 共享模块状态——用 opt-in 注释显式圈定安全文件,配 teardown 约束

这套清单的前提条件同样明确:Linear 的数字来自 TypeScript monorepo + Vitest + pnpm workspace 的具体组合,门禁和 setup 的绝对秒数对小型项目未必适用。但「把 CI 当系统而不是若干独立 job 来优化」「先降固定成本再谈并行」这两条方法论,不受技术栈限制。

来源:Linear — AI coding has made CI a bottleneck, so we reworked ours to keep up(Mufeez Amjad,2026-09-21)