GitHub Actions 缓存失效的根因与解法:同一个 workflow 为什么时而快时而慢(临时标题)

封面图

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 了。

现在你可以做什么

  1. 立即检查:打开你项目里最近的 workflow run,看日志里有没有 Cache not found——如果有,说明缓存失效是拖累速度的主因
  2. 加 restore-keys:在现有缓存 step 里加上 restore-keys,格式是「精确 key 在前 + 通配 key 兜底」
  3. 分离缓存 key:如果项目有多级依赖,把 pip/npm/build 分开设 key,避免改一行代码所有缓存全部重建
  4. 加 debug 输出:在缓存 step 后加一行 ls 验证缓存目录是否真的被填充了

GitHub Actions 的缓存机制不复杂,但细节魔鬼:key 的写法、restore-keys 的顺序、系统路径差异,每一处都可能让缓存悄悄失效。跑 workflow 时感觉忽快忽慢,先看日志里的 Cache 状态,十有八九是这个原因。

© 版权声明
THE END
喜欢就支持一下吧
点赞14 分享