今年早些时候,Linear 的 CTO 给工程师 Mufeez Amjad 派了一个单行标题的 issue:「CI 成本很高」,顺便把 CI 也做快一点。9 月 21 日,他把整个优化过程写了出来。这篇文章之所以值得逐条复盘,是因为它精准命中了 AI 编码时代所有工程团队正在撞上的同一堵墙:产出的加速远快于验证的加速,瓶颈从写代码转移到了验代码。 Linear 的解法不是堆机器,而是一次覆盖基础设施、门禁结构、环境准备、执行效率四个层面的系统级重构。
瓶颈的形状:先看清问题
Agent 让产出代码的速度指数级上升,但验证节奏没跟上——每个 PR 都得过 CI,开发越快,CI 越堵,成本越高,人和 agent 一起等反馈。Linear 的量化背景:自年初以来测试套件规模接近翻了四番,目前每周新增约 2000 个测试。他们优化的目标变量有两个:PR 在 CI 上的等待时长、runner 时间的消耗量。最终成绩:等待从 6 分多钟降到 5 分出头,每测试 runner 时间约减半。
值得强调的是他们的对照估算:如果年初不做这轮优化,今天这套测试要跑约 11 分钟。也就是说,这轮重构的真实收益不是「快了 1 分钟」,而是在测试规模翻四番的情况下守住了等待时间。这是系统级优化和个人小打小闹的本质区别。
路线一:基础设施与工具链
最早的收益几乎不需要动 CI 逻辑本身。把工作负载从 GitHub Actions 迁到第三方 runner(更快 CPU、更强存储、更好缓存)后,做前后各一天的同负载对比:任务平均快 34%,其中 tsc 一项降了 52%。
工具链现代化的收益更可观。换用原生 TypeScript 编译器 tsgo,把周中位 tsc 检查时间砍掉 73%,直接把瓶颈从类型检查上挪走。lint 侧的做法更具启发性:少数自定义 lint 规则依赖类型信息,导致每次 lint 都要构建完整类型图。他们把这些规则重写为基于 AST 的纯语法分析,让 ESLint 完全摆脱 TypeScript——API lint 降 68%,全仓 lint 降 55%,内存占用大降。去类型化还有一个连带好处:后续迁移到 Oxlint 时,纯语法规则几乎可以无损平移。
路线二:优化卡在门口的门禁任务
CI 作为一个系统看,最要命的是那几个「所有任务都在等它」的小任务:路径变更检测、输入相同则跳过的判断。它们被放在关键路径上,八个 API 测试 shard 全都得等它们结束。
三个针对性动作:其一是按需拉取——变更检测类任务原本要 checkout 完整工作树,限制 fetch depth 后最慢的门禁从 94 秒降到 20 秒,完全不需要工作树的任务直接去掉 checkout(27 秒降到 7 秒),需要 diff 的场景用 sparse、blobless 的浅 checkout 再省 11 秒。变更检测中位耗时从 26 秒到 8 秒,p90 从 31 秒到 12 秒,最慢一次从 138 秒到 37 秒。其二是checkout 韧性——第三方 runner 在 GitHub 网络之外,直连链路间歇性劣化会卡死关键路径;换成自带退避重试、设置低速超时(约 30 秒主动失败而非无限挂起)、使用持久磁盘上的 git 镜像缓存的合成 action。其三是关键路径瘦身——把「写缓存标记」这类动作从合并前最后检查挪到测试 shard 结束后的非门禁任务里,每条 API PR 和合并队列条目省 42 秒。
路线三:消灭重复 setup
每个 job 都要启动 runner、装依赖、准备构建环境,几秒钟的有用工作可能消耗几分钟的基础设施。Linear 的四板斧:
- 预装进基础镜像:每个测试 shard 都在 apt 装 Postgres 客户端(7 到 8 秒),直接做进含 Node 的 CI 基础镜像,顺手把原生编译头也装进去,消除偶发下载挂起的长尾。
- 按需安装:pnpm workspace 的 API 测试原本装全仓依赖,限定只装 API 包及其依赖后,pnpm install 从 44 到 73 秒降到 16 到 18 秒。
- 该重建就重建:缓存 node_modules 实测不如重建——见上方 FAQ 展开。
- 跳过未变化的 setup:API 容器每次重放完整数据库迁移历史,改为 schema 未变时直接加载预生成的快照文件,数据库准备从约 12 秒降到 1 到 2 秒。
另外,七条各自启动 runner、checkout、装依赖、然后只干几秒活儿的短检查,合并成两个 job 内部并发执行。按 6 月用量估算,这一项每月省约 87000 个 runner 分钟,占总 CI 用量的 11.8%。三项 setup 优化叠加后,单 shard 准备时间从 110 到 140 秒降到 67 到 73 秒,降幅约 44%。
路线四:测试执行效率
setup 便宜了,激进并行才划算。Vitest 按文件而非按耗时分配任务,几个超大测试文件就能拖死整个 shard。他们把大文件拆小,shard 从 4 个加到 8 个,关键任务快了约 19% 也便宜了约 19%,一周后最慢 shard 从 5.25 分钟降到 4.33 分钟。
单条最大的优化是共享模块状态:给安全文件开一个 isolate: false 的 opt-in 项目,同 worker 内的文件共用模块注册表,省掉每个文件重建实体、GraphQL、装饰器图的开销。收益约 17% 的月度节省,最慢 shard 从 300 到 379 秒降到约 195 秒,API shard 总 runner 时间从每次约 32.8 分钟降到 22 分钟。但这也是正确性风险最高的一步:每个入选文件要显式 opt-in、补齐共享状态的 teardown,用 fake timer 或共享状态改不干净的文件留在隔离项目里。还有一个细节体现了时代的变化——现在团队里大部分测试是 agent 写的,所以他们直接更新了 agent skill,让生成的测试默认遵守这套性能 opt-in 约束。
最后一步棋点透了整个系统:sharding 的上限是 setup 开销。shard 翻倍,setup 也翻倍——在每 shard 110 到 140 秒的年代,8 个 shard 光 setup 就要 15 到 19 分钟,比测试本身还贵;setup 压到 40 秒后,8 shard 的总 setup 比原来 4 shard 还少,并行翻倍才是净赚。先降固定成本,再加并行度,这个顺序在所有「水平扩展」场景里都成立。
写在最后:给 AI 编码团队的三条迁移建议
Linear 的做法未必能逐条照搬,但三个判断可以立刻带走。第一,把 CI 当系统而不是当任务列表:先测量关键路径,再决定动哪里,他们的多数收益来自「挪出关键路径」而不是「跑得更快」。第二,固定成本是并行的天花板:任何想加并发的地方,先看每单位工作携带多少一次性开销。第三,也是这个时代特有的——agent 写代码的速度只会继续涨,测试量会继续涨,CI 优化不是一次性项目,而是与产出增长赛跑的持续工程。Linear 现在每周还在新增约 2000 个测试,下一个瓶颈已经在路上了。