
GitHub Actions 缓存失效的根因与解法:同一个 workflow 为什么时而快时而慢
同一个 workflow,第一次跑 3 分钟,第二次跑 40 秒,第三次又回到 3 分钟。换了台机器反而更快。不是网络问题,不是 runner 负载——是缓存悄悄失效了,但你不知道为什么。本文深入拆解 GitHub Actions 缓存失效的几种根因,并给出经过实测的解法。
先说结论:缓存失效的三大元凶
GitHub Actions 的缓存机制并不复杂,但有 三个地方最容易踩坑:缓存 key 写死导致 hash 变化时整体失效、restore-keys 配错了导致缓存无法命中、以及依赖目录变了但你没感知到。下面逐个拆解。
场景一:依赖版本变了,但缓存 key 还是旧的
最常见的场景:改了一行 requirements.txt,缓存直接全部失效。
# 错误的缓存 key,只用了文件名
- name: Cache pip packages
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: pip-${{ hashFiles('requirements.txt') }}
这个 key 本身没问题,但当 requirements.txt 变了,hash 变化,key 跟着变,缓存自然 miss。问题在于——你可能根本不知道每次跑 workflow 时依赖变了。
真实踩坑记录:
# 第一次跑 - 命中缓存
Cache restored successfully: key: pip-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
Run time: 41s
# requirements.txt 加了一行 requests
# 第二次跑 - 缓存完全 miss
Cache restored successfully: key: pip-a87ff679a2f3e71d9181a67b7542122c
Run time: 3m12s
解决方案:配合 restore-keys 做模糊匹配,层层回退。
- name: Cache pip packages
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: pip-${{ runner.os }}-${{ hashFiles('requirements.txt') }}
restore-keys: |
pip-${{ runner.os }}-
pip-
restore-keys 的作用:精确 key miss 时,会依次尝试去掉 hash 部分的 key,最后到 pip- 通配符,保证至少能命中部分缓存,不会完全从零装。
场景二:restore-keys 顺序写反了,缓存永远用不上
restore-keys 的匹配规则是从上到下按顺序尝试,写反了等于没用。
# 错误写法:精确 key 写在通配 key 后面
restore-keys: |
pip-${{ runner.os }}-
pip-${{ hashFiles('requirements.txt') }} # ← 这行永远轮不到
# 正确写法:精确 key 在前,通配 key 在后兜底
restore-keys: |
pip-${{ runner.os }}-${{ hashFiles('requirements.txt') }}
pip-${{ runner.os }}-
pip-
另外注意:restore-keys 里不要写 exact 精确 key(即完整 hash 的 key),因为那个 key miss 了就是 miss 了,写在 restore-keys 里纯属浪费匹配次数。
场景三:缓存目录变了,但你没意识到
这个问题在 Node.js 项目里特别常见:node_modules 的路径在不同系统上不一样。
# Linux runner
path: node_modules
# macOS runner(如果有)
path: ~/Library/Caches/node_modules
如果 workflow 跨平台运行,Linux 和 macOS 的缓存完全隔离,在 Linux 上命中的缓存在 macOS 上根本不存在。
解法:用 runner.os 区分缓存 key,或者统一用 ${{ runner.os }}- 前缀。
key: node-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
restore-keys: |
node-${{ runner.os }}-
node-
场景四:缓存手动失效了,但 workflow 不知道
有时候你手动删了缓存(GitHub 界面或 API),但 workflow 里的 key 逻辑没变,runner 还在用旧的 key 尝试命中——当然 miss 掉了,然后重新缓存。
排查方法:看 workflow run 日志里的 Cache restored 字段。
# miss 了的情况
Cache not found. Action will create one. ← 明确显示缓存不存在
# 命中了的情况
Cache restored successfully: key: ...
如果手动清空过缓存,第二次跑一定 miss,这是正常行为,不是 bug。
进阶:多步骤依赖的缓存 key 设计
一个真实项目通常有多个缓存点:pip、npm、缓存编译产物。如果每个都用 hashFiles(),改一行代码所有缓存都失效,体验很差。
推荐做法:分离缓存、独立 key。
# 步骤一:依赖缓存(稳定,变化少)
- name: Cache Python packages
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: pip-${{ runner.os }}-${{ hashFiles('requirements.txt') }}
restore-keys: pip-${{ runner.os }}-
# 步骤二:源码缓存(每次 hash 不同,但可以用 restore-keys 兜底)
- name: Cache build artifacts
uses: actions/cache@v4
with:
path: build/
key: build-${{ runner.os }}-${{ github.sha }}
restore-keys: |
build-${{ runner.os }}-
build-
这样:依赖变了只影响 pip 缓存,源码变了只影响 build 缓存,互不干扰。
验证:你的缓存真的命中了吗?
加一行 debug 输出即可验证:
- name: Check cache status
run: |
echo "Cache key: ${{ runner.os }}-${{ hashFiles('requirements.txt') }}"
echo "Cache path: ~/.cache/pip"
ls -la ~/.cache/pip 2>/dev/null || echo "Cache dir empty or not exists"
如果 ls 输出了文件列表,说明缓存命中了;如果提示目录不存在,说明完全 miss 了。
现在你可以做什么
- 立即检查:打开你项目里最近的 workflow run,看日志里有没有
Cache not found——如果有,说明缓存失效是拖累速度的主因 - 加 restore-keys:在现有缓存 step 里加上
restore-keys,格式是「精确 key 在前 + 通配 key 兜底」 - 分离缓存 key:如果项目有多级依赖,把 pip/npm/build 分开设 key,避免改一行代码所有缓存全部重建
- 加 debug 输出:在缓存 step 后加一行
ls验证缓存目录是否真的被填充了
GitHub Actions 的缓存机制不复杂,但细节魔鬼:key 的写法、restore-keys 的顺序、系统路径差异,每一处都可能让缓存悄悄失效。跑 workflow 时感觉忽快忽慢,先看日志里的 Cache 状态,十有八九是这个原因。
更多交流点击入群






