构建缓存加速 分层 远程缓存与并行
蝶语森林
6 分钟阅读
构建缓存加速 分层 远程缓存与并行
构建慢的根因通常是“每次都从零来一遍”:依赖重新下载、中间产物反复生成。加速的本质是让该复用的步骤命中缓存、该并行的任务同时跑。分层缓存管本地复用,远程缓存管团队和 CI 共享,并行管时间轴压缩。三者叠加,构建时间可以从十几分钟压到一两分钟量级,但前提是每一步都配得对,否则缓存失效比全量还坑,而且问题更隐蔽——它看起来“构建成功”却用了错误的中间产物。
分层缓存利用层的不可变性
以容器构建为例,Dockerfile 的层是按指令顺序缓存的,改动越靠后的指令,前面没变的层就越能命中:
FROM node:20
WORKDIR /app
COPY package.json package-lock.json ./ # 依赖层单独拷贝
RUN npm ci # 依赖不变这层就命中
COPY . .
RUN npm run build
把 COPY . . 放到 npm ci 之后,源码改动不会让依赖层失效,重新构建只重跑最末两步。经验上,变动频率越低的内容越往上放,是分层缓存能命中的前提。还要注意 RUN 命令里如果带时间戳、随机值,每次都变,那一层永远不命中,等于白缓存。把易变输入和稳定输入分开,是分层能命中的核心心法。
构建内的挂载缓存
现代构建器支持在构建过程中挂载临时缓存目录,避免重复下载工具链:
RUN --mount=type=cache,target=/root/.npm \
npm ci
这类挂载缓存不进最终镜像层,只在构建期可用,既能加速又不会把缓存污染进产物。对 Maven、Go module、pip 同样适用,把各自的缓存目录挂出来即可。它和分层缓存互补:分层缓存跨构建保留层,挂载缓存跨构建保留工具下载,两者一起用效果最好。
远程缓存跨越单机边界
本地缓存只在单机有效,CI 每次是新机器就退化成全量构建。远程缓存把层或产物推到共享存储,别人构建时直接拉:
docker buildx build --cache-from type=registry,ref=registry/app:cache \
--cache-to type=registry,ref=registry/app:cache .
命中远程缓存后,依赖安装这类 IO 密集步骤可跳过。注意缓存键要稳定:用 lock 文件而非泛指目录,避免把上游变更漏掉或把脏状态带进来。远程缓存跨人共享,一旦有人推了错误的缓存,全队都中招,所以缓存来源要可信,最好按提交内容做内容寻址,而不是按分支名覆盖。
并行压缩时间轴
任务间无依赖就该并行。Make 用 -j,CI 用矩阵:
make -j4 # 四个独立目标同时编
# CI 矩阵并行示例
strategy:
matrix:
task: [lint, unit, build, e2e-fast]
并行度受机器核数和 IO 带宽限制,盲目拉满会互相抢资源,反而变慢。量级上,并行数接近 CPU 核数附近收益最大,超过后边际递减,且 IO 密集型任务并行多了磁盘成瓶颈。给并行任务配独立的输出路径,否则产物互相覆盖难排查。
单仓库的构建策略
大单仓库里只改了一个包,却要全量构建最浪费。按受影响范围做增量构建,只编被改包及其下游,能省下大量时间。这需要构建工具支持依赖图分析,不是简单 -j 能解决的。没有依赖图的粗粒度并行,常常重复编了没改的模块,看着并行了实际没省。增量构建的正确性前提是依赖图准确,漏声明依赖会导致用旧产物编出新错。
缓存失效的边界
缓存不是万能,有些输入变化不会体现在文件时间戳上:编译器的版本、环境变量的取值、被 COPY 进来的隐藏文件。这些变化时缓存仍命中,就会产出与预期不符的产物。把这类隐式输入显式写进缓存键或强制 no-cache,才能避免“本地好、CI 坏”的诡异问题。
缓存投毒与隔离
远程缓存是共享资源,谁都能推就意味着谁都可能推错。对缓存仓库做写权限控制,按项目隔离命名空间,关键发布不走共享缓存,用独立的内容寻址缓存,避免被日常构建的脏状态波及。
CI 产物与缓存分离
构建产物(要发布的包、镜像)和构建缓存(中间层)生命周期完全不同:产物要长期保留、可追溯;缓存可随时淘汰、只为加速。混在一起管理,清理缓存时会误删产物,保留产物又会撑爆缓存存储。分开存储、分别定策略,是成熟 CI 的基本功。
踩坑清单
- 把会变的文件(如时间戳、本地配置)写进本该稳定的层,缓存永远失效。先 COPY 锁文件再装依赖是固定套路。
- 远程缓存没设清理策略,缓存镜像无限增长占满存储。按时间或 tag 滚动保留。
- 缓存命中后跳过了某步,但该步其实有副作用(如写版本号),产物里缺了关键信息。缓存键要包含这类输入。
- 并行任务共享同一个输出目录,互相覆盖产物。并行必须配独立的输出路径。
- 误把远程缓存当成制品仓库,缓存可被随时淘汰,正式发布产物要走独立的版本化存储。
- 跨平台构建时缓存不互通,在 x86 机器上推的缓存到 arm 机器用不了,要按架构分别存。
- 隐式输入(编译器版本、环境变量)变了缓存仍命中,产出不符预期。要把隐式输入纳入失效判断。
缓存能放大“正确构建”的速度,会放大“错误构建”的传播。命中前确认缓存来源可信、键语义正确,否则省下的时间会在排查时加倍奉还。
浏览 0
点赞 0
评论 0