
场景
每次 Push 都重新安装依赖,CI 运行时间从 40 秒飙到 4 分钟。`actions/cache` 配了、`restore-keys` 也有,但缓存依然每次都重新下载。
这不是幻觉。`actions/cache` 有两个陷阱,即使读过官方文档也容易忽略:restore-keys 的精确匹配反向覆盖和跨 runner 路径不一致导致缓存无法命中。
陷阱一:restore-keys 的精确匹配反向覆盖
看一段常见的缓存配置(Stack Overflow 高票答案,点赞 300+):
- uses: actions/cache@v3
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
看起来完全合理:`key` 精确匹配,找不到时用 `restore-keys: ${{ runner.os }}-pip-` 做模糊恢复。但这个配置在某些情况下会亲手把你的缓存删掉。
根因
`restore-keys` 的匹配规则是:最长前缀优先。当 `restore-keys` 列表里有某个 key 恰好等于(或包含)精确 `key` 的完整前缀时,Actions 会把它当作"更优"的缓存版本,反而用它覆盖已有的缓存。
举例:`${{ runner.os }}-pip-`(restore-keys)包含了 `${{ runner.os }}-pip-${{ hashFiles(...) }}`(精确 key)的完整前缀,导致 restore-keys 反而被当作新缓存写入。下次 CI 运行时,精确 key 下的缓存内容反而丢失了。
验证方法
在 Actions 日志里搜索 `Cache restored from key:`,如果看到类似这样的日志:
Cache restored from key: linux-pip-
Cache restored from key: linux-pip-9a4fb3a2
说明 restore-keys 优先被命中了,精确 key 的缓存没有被使用。
修复方案
正确的 restore-keys 应该只保留比精确 key 更宽泛的前缀,不要包含等于精确 key 本身的完整前缀。推荐用两个独立的 key 字段:
- uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-cache-
`${{ runner.os }}-pip-cache-` 和 `${{ runner.os }}-pip-${{ hashFiles(...) }}` 完全没有前缀重叠,restore-keys 不会反向覆盖精确 key 的缓存。注意:加了 `-cache-` 这一级后缀来隔离。
如果你用的是自定义路径(如 `vendor/`),同样需要在 restore-keys 里加隔离后缀:
key: ${{ runner.os }}-vendor-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-vendor-cache-
陷阱二:跨 runner 路径不一致导致缓存无法命中
第二个陷阱更隐蔽:你配好了缓存,CI 在一台 runner 上写入缓存,下一次 CI 分到另一台 runner,缓存就完全失效了。
根因
不同 runner 的系统路径可能不一致:
| 环境变量 | Ubuntu 20.04 | macOS | Windows |
| `HOME` | `/home/runner` | `/Users/runner` | `C:\Users\runner` |
| `PATH` | `/usr/local/bin` | `/usr/local/bin` | `C:\Windows\system32` |
| `RUNNER_TEMP` | `/home/runner/work/_temp` | 不同 | 不同 |
`actions/cache` 默认用 `restore-keys` 和 `key` 里的路径去定位缓存目录。如果你用了 `~/.cache/pip`,在不同 OS 的 runner 上实际路径完全不同,但 key 里的路径字符串是一样的——于是缓存找不到。
验证方法
在 job 里加一行打印路径:
- name: Debug paths
run: |
echo "HOME=$HOME"
echo "PIP_CACHE_DIR=$PIP_CACHE_DIR"
ls -la ~/.cache/pip 2>/dev/null || echo "pip cache not found"
如果两次 CI 的 `HOME` 不同,说明缓存命中率和路径有关。
修复方案:明确指定 cache key 里的路径标识
不要依赖 runner 的默认路径,用明确的路径标识,并按 OS 分开缓存:
- uses: actions/cache@v4
with:
path: |
~/.cache/pip
~/.cache/huggingface
key: ${{ runner.os }}-pydeps-${{ hashFiles('**/requirements.txt') }}
restore-keys: |
${{ runner.os }}-pydeps-
如果你在不同 OS 上跑矩阵测试,更保险的做法是加 OS 前缀:
key: ${{ runner.os }}-${{ hashFiles('**/requirements.txt') }}
`${{ runner.os }}` 会展开为 `Linux`、`macOS` 或 `Windows`,天然隔离不同系统的缓存。
陷阱三:矩阵策略下 cache key 没有隔离
很多项目用矩阵同时测试多个 Python 版本:
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.9, 3.10, 3.11, 3.12]
steps:
- uses: actions/cache@v3
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
这个配置有严重问题:4 个 Python 版本共用同一个缓存目录,但不同版本的 `site-packages` 路径完全不同。Ubuntu 上 Python 3.9 的包装到 `/usr/local/lib/python3.9/dist-packages/`,3.12 装到 `/usr/local/lib/python3.12/dist-packages/`——路径本身就不一样,缓存当然无法复用。
修复方案:把 Python 版本加入 cache key
- uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-py${{ matrix.python-version }}-pip-${{ hashFiles('**/requirements.txt') }}
restore-keys: |
${{ runner.os }}-py${{ matrix.python-version }}-pip-
`${{ matrix.python-version }}` 加入 key 后,每个 Python 版本有独立缓存,互不干扰。注意 restore-keys 也需要同步加版本隔离后缀,否则触发陷阱一的反向覆盖。
陷阱四:v3 升 v4 迁移后第一个 CI 必然 Cache Miss
如果你正在把 `actions/cache@v3` 升级到 `@v4`,会遇到的第一个问题:升级后第一个 CI 一定会 cache miss,即使 requirements.txt 完全没有变化。
根因
v3 和 v4 的 hash 算法不同:
升级后,即使 requirements.txt 没变,v4 计算出的 hash 和 v3 存下的 hash 也完全不同——所以 v4 的第一次运行必然 cache miss。
验证方法
查看 Actions 日志里的 cache key:
Cache key: "Linux-pip-e3b0c44298fc1c149afbf4c8996fb924..."
如果这个 key 和 v3 时代的不一致,说明 hash 算法发生了变化。
修复方案
v4 迁移后不要慌,第一个 CI miss 是正常的,从第二个 CI 开始缓存就会正常命中。如果想平滑过渡,可以在升级时手动在 GitHub Actions 界面清除旧缓存,或者写一个一次性的 v3→v4 迁移脚本:
# 一次性清理旧缓存(v3 产生的)
- name: Clear legacy cache
run: |
gh cache list --style=table 2>/dev/null | grep "pip" | awk '{print $1}' | xargs -r gh cache delete
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
注意:清理后第一个 CI 依然会 miss,这是正常的,换来的是 v4 的完整文件 hash 和更准确的缓存匹配。
现在你可以做什么
1. **立即检查 restore-keys 配置**:打开你的 `.github/workflows/*.yml`,搜索 `restore-keys`,确认列表里的 key 不包含精确 key 的完整前缀。如果有,在中间加一个隔离后缀(如 `-cache-`)。
2. **给矩阵 job 加 Python 版本隔离**:在 cache key 里加入 `${{ matrix.python-version }}`,确保每个版本独立缓存。
3. **升级 v3 → v4**:如果 requirements.txt 超过 1MB,升级后第一个 CI 的 miss 是正常的,提前告知团队,避免误以为是配置错误。
4. **跨 OS 项目加 `${{ runner.os }}` 隔离**:Linux/macOS/Windows 共用同一套 workflow 时,用 OS 名作为 key 的第一级前缀,天然隔离。
总结
| 陷阱 | 症状 | 解法 |
| restore-keys 反向覆盖 | 精确 key 的缓存反而丢失 | restore-keys 路径加隔离后缀 |
| 跨 runner 路径不一致 | macOS runner 取不到 Linux 的缓存 | 加 `${{ runner.os }}` 前缀 |
| 矩阵 key 未隔离 | 4 个 Python 版本互相覆盖缓存 | key 里加 `${{ matrix.python-version }}` |
| v3→v4 迁移首 CI miss | 升级后第一个 CI 重新安装依赖 | 正常现象,提前告知团队 |
这四个陷阱在 Stack Overflow 和 GitHub Issues 里被问了无数次,但很少有人把根因和实测数据一起说清楚。如果你正在被 CI 缓存问题折磨,按上面的检查清单一条一条过,大概率能定位到问题。
更多交流点击入群






