CircleCI 配置优化缓存
yaml steps:
- restore_cache: keys: - v1-deps-{{ checksum "package-lock.json" }} - v1-deps-
- run: npm ci
- save_cache: key: v1-deps-{{ checksum "package-lock.json" }} paths: - node_modules
### 逐行解读
1. **恢复缓存**
`restore_cache` 会按顺序尝试匹配 `keys` 列表中的缓存键。上例中,先查找精确匹配 `v1-deps-<文件校验和>` 的缓存,如果不存在,则退回到以 `v1-deps-` 开头的**部分匹配**(最新生成的一个)。部分匹配允许你在依赖文件发生微小变化时仍能复用大部分缓存,只重新安装差异部分。
2. **安装依赖**
即使恢复到了部分缓存,也建议运行包管理器的安装命令(如 `npm ci`、`pip install`、`mvn dependency:resolve`)。这些工具自身具备增量更新能力,能补充缺失的依赖。
3. **保存缓存**
每次构建成功后,使用完全相同的精确键保存缓存。只有具备相同键的缓存才会被覆盖;CircleCI 会缓存指定 `paths` 下的所有内容。注意,`save_cache` 执行时如果缓存已存在且内容未变,并不会重复上传,非常高效。
## 最佳实践:设计健壮的缓存键
缓存键设计直接影响缓存命中率和存储效率。遵循以下原则可以最大化收益。
### 使用锁定文件作为缓存指纹
永远不要基于 `package.json` 这类可能频繁变动的文件创建缓存键,应使用锁定文件(`package-lock.json`、`yarn.lock`、`Gemfile.lock`、`pom.xml` 的校验和等)。锁定文件精确描述了依赖树,任何依赖变更都会反映在其校验和中。
```yaml
# 针对 Node.js 项目
key: node-cache-{{ checksum "package-lock.json" }}
# 针对 Java Maven
key: maven-cache-{{ checksum "pom.xml" }}
# 针对 Python pip
key: pip-cache-{{ checksum "requirements.txt" }}
使用多级回退键
为避免每次依赖更新都完全失效缓存,可以设计从一个宽泛到精确的回退链:
- restore_cache:
keys:
- npm-v2-{{ checksum "package-lock.json" }}
- npm-v2-{{ .Branch }}
- npm-v2-
这里先用精确依赖缓存,如果失败则尝试当前分支的最新缓存,最后使用任意分支的最新缓存。分支级缓存能加快功能分支的首次构建,因为可以从主分支继承大部分依赖。
版本前缀隔离
在缓存键中加入版本前缀(如 v1-、v2-)是一种低成本的手动缓存失效手段。当你更改缓存保存逻辑或调整了路径时,只需递增版本号,旧的缓存就不会被误用。
key: v2-gradle-{{ checksum "build.gradle" }}
不同语言与框架的缓存示例
Node.js (npm)
steps:
- restore_cache:
keys:
- npm-v1-{{ checksum "package-lock.json" }}
- npm-v1-
- run: npm ci
- save_cache:
key: npm-v1-{{ checksum "package-lock.json" }}
paths:
- ~/.npm
- node_modules
同时缓存 ~/.npm 全局缓存可加速 npm ci 自身,因为 npm 在安装时仍会查询本地缓存。
Python (pip)
steps:
- restore_cache:
keys:
- pip-v1-{{ checksum "requirements.txt" }}
- pip-v1-
- run: |
python -m venv venv
. venv/bin/activate
pip install -r requirements.txt
- save_cache:
key: pip-v1-{{ checksum "requirements.txt" }}
paths:
- venv
- ~/.cache/pip
Java (Maven/Gradle)
# Maven
steps:
- restore_cache:
keys:
- maven-v1-{{ checksum "pom.xml" }}
- maven-v1-
- run: mvn dependency:go-offline
- save_cache:
key: maven-v1-{{ checksum "pom.xml" }}
paths:
- ~/.m2
# Gradle
steps:
- restore_cache:
keys:
- gradle-v1-{{ checksum "build.gradle" }}
- gradle-v1-
- run: ./gradlew check
- save_cache:
key: gradle-v1-{{ checksum "build.gradle" }}
paths:
- ~/.gradle
Go
steps:
- restore_cache:
keys:
- go-mod-v1-{{ checksum "go.sum" }}
- go-mod-v1-
- run: go mod download
- save_cache:
key: go-mod-v1-{{ checksum "go.sum" }}
paths:
- ~/go/pkg/mod
Docker 层缓存:加速镜像构建
在 CircleCI 中构建 Docker 镜像时,使用 Docker 层缓存(DLC)可以避免每次全量拉取基础镜像并重建所有层。DLC 是一种付费功能,但所有计划均可按使用量开启。
启用 DLC
在 Job 配置中添加 docker_layer_caching: true:
version: 2.1
jobs:
build-docker:
docker:
- image: cimg/base:stable
steps:
- checkout
- setup_remote_docker:
docker_layer_caching: true
- run: docker build -t myapp:latest .
工作原理
启用 DLC 后,CircleCI 会在远程 Docker 环境中保留上一次构建产生的镜像层。当再次执行 docker build 时,已存在且未变更的层会被直接复用,仅重建变化的部分。这对于依赖基础镜像的层、复制大文件但很少改变的层尤为有效。
最佳实践
- 在 Dockerfile 中合理安排指令顺序,将变更频率低的层放在前面(如安装系统依赖),将代码复制放在最后。
- 配合
.dockerignore文件排除不必要的构建上下文,减少上传时间。 - 对于多阶段构建,DLC 同样有效,会缓存中间阶段产生的层。
缓存优化进阶技巧
缓存路径不要太泛
避免直接缓存整个工作目录(如 .),这会包括大量非依赖文件,导致缓存体积暴增、上传/恢复变慢。始终指定精确的目录,如 ~/.m2、node_modules。
利用 CircleCI 的缓存大小限制
单个缓存卷的上限约为 500MB。如果缓存太大,CircleCI 会自动分片,但过大的缓存仍会影响性能。清理包管理器的缓存(如 npm cache clean --force 在完成安装后)可以瘦身。
条件性保存缓存
有时只想在主分支保存缓存,避免分支缓存污染。可以使用 CircleCI 条件逻辑:
- save_cache:
key: my-cache-{{ checksum "lockfile" }}
paths:
- /path/to/deps
when: on_success # 默认行为,可省略
但通常不需要刻意限制,因为带分支信息的键已经做了隔离。
跨 Job 使用 Workspace 传递构建产物
当需要将构建生成的文件传递给后续测试或部署 Job 时,工作空间是比缓存更合适的选择:
# 构建 Job
- persist_to_workspace:
root: .
paths:
- build
# 测试 Job
- attach_workspace:
at: .
工作空间不计入缓存存储,且仅在同一个工作流内有效,适合短生命周期的大文件传递。
诊断缓存未命中的问题
如果发现缓存总是 MISS,检查以下几个方面:
- 锁定文件是否发生了变化
使用cat或md5sum命令在 Job 中打印锁定文件的校验和,确认与预期一致。 - 缓存键中模板变量求值错误
确保{{ checksum "file" }}中的文件路径相对于工作目录正确。可以使用pwd和ls调试。 - 分支与回退键顺序
调整回退列表,让更通用的键排在后面。 save_cache的执行时机
缓存保存必须在生成缓存内容的步骤之后、工作流结束之前。如果因前置步骤失败而跳过,需检查 Job 状态。
你可以在 CircleCI Web UI 的 Job 详情中观察每个 restore_cache 步骤的输出,其中会显示缓存键和命中/未命中状态。
完整配置模板汇总
Node.js 项目
version: 2.1
jobs:
build:
docker:
- image: cimg/node:18.17
steps:
- checkout
- restore_cache:
keys:
- node-deps-v1-{{ checksum "package-lock.json" }}
- node-deps-v1-{{ .Branch }}
- node-deps-v1-
- run: npm ci
- save_cache:
key: node-deps-v1-{{ checksum "package-lock.json" }}
paths:
- ~/.npm
- node_modules
- run: npm test
同时使用依赖缓存和 Docker 层缓存
version: 2.1
jobs:
test-and-build:
docker:
- image: cimg/openjdk:17.0
steps:
- checkout
- restore_cache:
keys:
- maven-v1-{{ checksum "pom.xml" }}
- run: mvn test
- save_cache:
key: maven-v1-{{ checksum "pom.xml" }}
paths:
- ~/.m2
- setup_remote_docker:
docker_layer_caching: true
- run: docker build -t myapp .