让构建从 8 分钟降到 2 分钟:缓存
构建慢了先别急着加 CPU。绝大多数慢,是每次都在重下依赖。
一、先分清楚两种缓存
| 类型 | 作用范围 | 怎么开 |
|---|---|---|
| 节点缓存 | 当前构建节点内 | 代码自动缓存;文件靠 docker.volumes 声明 |
| 跨节点缓存 | 所有节点共享 | 把缓存内容做成 Docker 镜像,用 docker:cache |
默认情况下,流水线会分配到 3 个左右的构建节点(数量随资源使用动态调整)。节点缓存只在自己那台上有效,所以命中率不是 100%——这是平台在"单点故障风险"和"命中率"之间的折中。
二、节点缓存:一行配置搞定
.cnb.yml
main:
push:
- docker:
image: node:20
volumes:
- /root/.npm:copy-on-write
stages:
- name: install
script: npm install
- name: test
script: npm test
copy-on-write 是基于 OverlayFS 的即时复制,高并发下也能安全复用同一份缓存。
同类写法:
| 技术栈 | 缓存目录 |
|---|---|
| npm | /root/.npm |
| yarn 1.x | /usr/local/share/.cache/yarn |
| pnpm | /root/.local/share/pnpm/store |
| pip | /root/.cache/pip |
| Go | /root/go/pkg/mod、/root/.cache/go-build |
| Maven | /root/.m2/repository |
| Gradle | /root/.gradle/caches |
| Cargo | /usr/local/cargo/registry |
代码本身不需要声明,CNB 会自动缓存并复用。
三、跨节点缓存:要 100% 命中就上 Docker
节点缓存受限于"分配在哪台节点"。真要做到所有节点都命中,把缓存目录做成镜像:
.cnb.yml
main:
push:
- stages:
- name: restore cache
type: docker:cache
options:
image: ${CNB_DOCKER_REGISTRY}/${CNB_REPO_SLUG_LOWERCASE}/node_modules:cache
path: node_modules
- name: install
script: npm ci
它把 node_modules 打成一个 Docker 层推到制品库,下一次构建直接拉下来。适用于依赖巨大、且很少变动的项目。
代价是每次构建多一次拉/推镜像的时间。依赖中等规模时,节点缓存更划算。
四、提速的优先级顺序
不要一上来就到处加缓存。按这个顺序做,收益最大:
- 看清瓶颈:构建日志里每个 stage 的耗时,先找出最慢的那一个
- 砍掉不必要的工作:文档改动没必要跑全量测试(用
ifModify或[ci skip]) - 并行化:一条事件下写多条 pipeline,它们并发执行;Stage 之间是串行的,别把独立任务串成一串
- 加缓存:先节点缓存,命中不够再上跨节点缓存
- 换节点规格:最后才考虑加
cpus,而且只对编译类任务有效 - 换更大更全的镜像:把依赖预装进镜像,比每次装更快
五、一条容易被忽略的
docker.build 用 Dockerfile 建镜像时,系统会自动做哈希缓存:镜像内容没变就直接复用远端缓存,不会重复 build。所以自定义镜像并不等于每次都很慢。
但它的前提是 Dockerfile 和构建上下文稳定。把 COPY . . 放在 RUN npm install 之前,会让缓存每次都失效——这是最典型的 Dockerfile 反模式:
# 反例:改一行代码就要重装依赖
COPY . .
RUN npm install
# 正例:先装依赖,再拷代码
COPY package.json yarn.lock ./
RUN yarn install
COPY . .