Featured image of post AI 把写代码变快了,CI 第一个撑不住

AI 把写代码变快了,CI 第一个撑不住

Linear 公开了 AI 编码提速后的 CI 改造复盘。测试数量翻了近四倍,PR 等待反而更短,压力最大的环节已经从写代码挪到了验证。

用 Agent 写代码的人对这种节奏应该不陌生。上午提一个想法,下午回来,Agent 已经交了三个 PR,改动看着都挺像样。你去泡了杯咖啡,回来发现第一个 PR 还挂在 CI 上,第二个排在合并队列里,第三个连测试都没开始跑。

Linear 的工程师 Mufeez Amjad 今年碰上的也是这件事,只是规模大得多。年初,CTO 给他派了一张 issue,标题就叫「CI costs are high」,顺手还加了一句,把 CI 也弄快点。

Linear 在 9 月 21 日把这件事写成了一份复盘。Agent 让他们出代码的速度指数级上涨,可每个 PR 还得过 CI,开发一加速,先排队的是 CI,基础设施账单跟着涨,人和 Agent 一起等反馈。这一年他们测试套件几乎翻了四倍,PR 等待时间却从 6 分多钟压到 5 分出头,单个测试烧掉的机器时间大约砍半。

这些数字都是 Linear 自己公开的口径,看趋势可以,当行业平均值引用就不合适了。他们的代码库主要是 TypeScript,单仓库,Runner 也是特定的第三方环境。换一个仓库,结论未必一样。

Linear 官方复盘配图

图片来自 Linear 官方复盘

AI 写代码变快后 CI 排队拥堵的示意封面

卡住流水线的,经常是几个小任务

很多人遇到 CI 变慢,第一反应是测试写多了,删几个,或者让 AI 把测试跑快点。Linear 的复盘里最有意思的观察在别处。

每个 CI 运行的开头都有一个变更检测,判断这个 PR 动了哪些路径,同一份代码以前跑没跑过。它站在最前面。后面八个测试分片,全部等它完工,慢十几秒,八个分片一起干等。这个任务原本要把整个仓库 checkout 下来,实际上只需要一小部分。Linear 给 fetch 加了深度限制,最慢的一次从 94 秒掉到 20 秒;不需要工作区的任务干脆连 checkout 都去掉了。整体看,中位数从 26 秒到 8 秒,最差的一次也有 138 秒,改完落到 37 秒。

Linear 官方数据,变更检测任务优化前后的耗时分布

图片来自 Linear 官方复盘。中位数从 26 秒降到 8 秒。

变更检测门禁卡住整条 CI 流水线的示意

还有一处更隐蔽。他们的合并前检查里顺手写了一个缓存标记,结果 PR 测试全过了,还得在合并队列里等这个动作做完。把写入挪到一个不阻塞任何事的任务里,每个 API PR 的合并路径省了 42 秒。

这类改动单独看都很小,加起来大约一分钟。对一天几十个 PR 的团队,一分钟乘以几十,再乘以每天,就不是小钱了。

重复劳动是隐形大头

CI 的账单里有一大块花在重复上。每个任务都要起 Runner、装依赖、准备环境,一套下来往往一两分钟。有些任务干的活,只有几秒。

有几笔账,Linear 算得挺细。API 测试的每个分片,每次都用 apt 装一遍同样的 Postgres 客户端,7 到 8 秒。他们把它塞进基础镜像,这几秒就从每个分片里消失了。pnpm 装依赖,原来每次装整个 monorepo,要 44 到 73 秒;改成只装 API 包需要的部分,16 到 18 秒。

缓存那笔账最反直觉。直觉里缓存总比重装快,他们试了缓存 node_modules,发现重装反而更快。缓存 key 挂在经常变动的 lockfile 上,命中一次也要 28 秒恢复,过滤安装只要 7.5 秒。缓存没省下时间。保存要成本,恢复看运气。别的仓库不一定也是这个结论,可缓存到底划不划算,得实测,不能靠感觉。

七个各自只干几秒钟活的小检查,原来每个都独占一个 Runner,走一遍 checkout 和依赖安装。合并成两个任务以后,按 6 月用量算,一个月省下大约 87,000 个 Runner 分钟,占他们全部 CI 用量的 11.8%。

数据库那边也一样。每个容器每次都重放完整的迁移历史,哪怕这个 PR 根本没碰 schema。换成加载生成好的 schema 快照,12 秒变成 1 到 2 秒。

分片的算术

想跑得快,直觉做法是多开几个分片。Linear 把 API 测试从 4 个分片加到 8 个,关键任务快了大约 19%。复盘里把这件事的前提讲得很清楚,分片数量翻倍,setup 的总时间也跟着翻倍。

原来每个分片的启动成本是 110 到 140 秒,8 个分片光 setup 就要 15 到 19 分钟,比测试本身还长。把前面那些重复安装的问题解决掉,每个分片启动降到 40 秒左右,8 个分片的 setup 总和才第一次低于原来 4 个分片的开销。并行度不是白来的。先把每个分片的固定开销压下去,多开分片才开始划算。

Linear 官方数据,分片从 4 个加到 8 个前后的 setup 总耗时对比

图片来自 Linear 官方复盘。启动成本压下来之后,8 个分片的 setup 反而少于原来 4 个。

分片固定启动成本与并行收益的算术示意

Vitest 按文件分配任务,几个特别大的测试文件能拖住整个分片。Linear 把大文件拆小,又调了分片配置,一周后最慢的分片从 5.25 分钟降到 4.33 分钟。

隔离放宽到哪一步,代价谁担

整个复盘里最大的一笔单项收益来自放宽测试隔离。Vitest 默认每个测试文件独立跑,各自的模块注册表都要重建。Linear 开了一个 opt-in 的项目配置,让声明安全的文件共享模块状态。最慢分片从 300 到 379 秒掉到 195 秒左右,API 分片的总 Runner 时间从每次 32.8 分钟降到 22 分钟。

这也是正确性风险最高的一步。他们的做法是把资格写明白,每个参与共享的文件都要加显式标记,配套补上共享状态的清理。用了 fake timer 或者共享状态理不清的文件,留在原来的隔离项目里不动。

复盘里有一句话我最在意。Linear 说,他们的大部分测试如今出自 Agent 之手,所以连给 Agent 用的技能文档也一并更新,让生成的测试默认遵守这套约束。工具的行为规范没法只写在人的脑子里,还得写进 Agent 能读到的规则里。团队改流程,等于同时给人和 Agent 改。

普通团队能带走什么

Linear 的具体手段没法直接照搬。仓库、工具链、Runner、用量,每家都不一样,缓存该不该用、分片开几个、隔离放不放宽,答案都会不同。能带走的是方法。

想判断 CI 是否已经变成瓶颈,先记三个数。PR 从提交到反馈的平均等待,每天花在 Runner 上的分钟数,还有关键路径上最慢的那个门禁。有了一个月的走势,哪里在恶化一目了然,优化也才有对照。

然后看关键路径。排在流水线最前面的任务,哪怕只慢十几秒,后面所有任务都得等它。把流程图画出来,找出所有「每个人都得等它」的节点,先动它们。没人等的任务,慢一点也不要紧。

现在多了一个变量。代码产量的增长没有上限,验证资源的增长有预算上限。以前 CI 慢一点,忍一忍就过去了。现在一个 Agent 可以不停提 PR,CI 顶不住的时候,它不会自己停下来等,它只会继续往队列里塞。排队的人从工程师变成了工程师,外加一队不知疲倦的机器人。

复盘结尾还有一笔反事实的账。年初要没动手,今天跑一遍测试要 11 分钟,差不多是现在的两倍。他们每周还在新增大约 2,000 个测试。这笔账没有终点,下个季度大概率还要再算一遍。

验证系统接不住产量的时候,写代码再快,也只是把队列堆得更高。

参考来源

以上数据均来自 Linear 的公开自述,其环境为 TypeScript 单体仓库加特定第三方 Runner,数值结论不一定能迁移到其他仓库和工具链。

RSS Feed 使用 Hugo 构建
主题 StackJimmy 设计