CircleCI 配置优化缓存

FreeGuideOnline 最新 2026-07-13

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 同样有效,会缓存中间阶段产生的层。

缓存优化进阶技巧

缓存路径不要太泛

避免直接缓存整个工作目录(如 .),这会包括大量非依赖文件,导致缓存体积暴增、上传/恢复变慢。始终指定精确的目录,如 ~/.m2node_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,检查以下几个方面:

  1. 锁定文件是否发生了变化
    使用 catmd5sum 命令在 Job 中打印锁定文件的校验和,确认与预期一致。
  2. 缓存键中模板变量求值错误
    确保 {{ checksum "file" }} 中的文件路径相对于工作目录正确。可以使用 pwdls 调试。
  3. 分支与回退键顺序
    调整回退列表,让更通用的键排在后面。
  4. 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 .