GitHub Actions 的 “秒级启动” 承诺常常死在第一行 sudo apt-get update。官方 runner 每次从全新虚拟机开始,系统包管理器必须从零拉取索引、解析依赖、下载 deb—— 一条链轻松烧掉 30–180 秒。更隐蔽的是,当多架构源出现网络抖动,apt-get 会卡死 6 小时直到 Actions 超时,Playwright 等重型依赖一次就能拖进 2 GB,让缓存策略直接破产。本文把这条 “隐形耗时链” 拆成四段,给出可直接拷贝的精简替代方案与参数阈值。
一、耗时链拆解:四段黑洞从哪儿来
-
索引刷新
Ubuntu 镜像默认同时配置archive.ubuntu.com与azure.archive.ubuntu.com,多架构索引叠加,apt update 平均 15 s,最坏 300 s。 -
依赖求解
每次从零构建 solver 状态,重型包(qtbase5-dev、chromium)依赖图可达 800 节点,解析 5–20 s。 -
下载 & 解压
GitHub 托管 runner 千兆带宽虽大,但 deb 解压是单线程 CPU 密集,2 GB 数据仍需 25 s。 -
缓存失效
GitHub 缓存 7 天未访问即回收;feature 分支每天新建,命中率 <30 %,且并发 job 写同一 key 会直接冲突,回退到 “零缓存”。
二、官方 setup-* 为何救不了系统包
actions/setup-node、setup-python 等只缓存语言级包(npm、pip),对 /usr 目录完全无视。实测一个含 LaTeX 的文档项目,系统包占总耗时 42 %,语言缓存再快也救不回来。想把 /usr 整体打包?缓存体积轻松突破 2 GB,接近仓库级 10 GB 上限,且解压 CPU 时间可能抵消收益;更麻烦的是,Ubuntu 22.04 → 24.04 的 glibc 版本差会导致 ABI 不兼容,运行时直接段错误。
三、精简替代方案:三层递减策略
| 方案 | 适用场景 | 实施成本 | 平均提速 |
|---|---|---|---|
| A. 预构建 Docker 层 | 固定系统依赖 | 中 | 60–80 % |
| B. apt-offline 快照 | 源不稳定 | 低 | 40–60 % |
| C. 缓存分片 + 并发锁 | 必须裸机运行 | 低 | 30–50 % |
A. 预构建 Docker 层(最彻底)
把系统依赖固化到镜像,而非在 runner 里安装。CI 里只拉镜像,网络流量从 2 GB 降到 200 MB 层差。
# Dockerfile.ci
FROM mcr.microsoft.com/playwright:v1.49.0-noble
RUN apt-get update && apt-get install -y \
valgrind texlive-latex-base \
&& rm -rf /var/lib/apt/lists/*
workflow 中一句即可:
container:
image: ghcr.io/your-org/app-ci:sha-7d3e4f9
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
关键点
- 用
--squash把多层压成一层,减少拉取往返。 - 在 PR 合并前用
docker buildx imagetools create做多架构同步,避免跨架构拉取失败。
B. apt-offline 快照(网络差时救急)
把 apt-get update 生成的索引与 deb 打包成快照,上传到 GitHub Packages 或 S3,CI 内直接 apt-offline install 绕过公网。
生成快照(每周一次 cron):
sudo apt-offline set ci.sig --update --upgrade \
--install-packages valgrind libqt5core5a
sudo apt-offline get ci.sig --bundle ci.zip
CI 内恢复:
- name: 下载离线快照
run: |
curl -L -o ci.zip https://github.com/your-org/apt-snapshot/releases/download/latest/ci.zip
sudo apt-offline install ci.zip
体积仅 180 MB,缓存 30 天即可;索引与 deb 同源,无 ABI 撕裂风险。
C. 缓存分片 + 并发写锁(无法改 Docker 时)
把系统包拆成 “稳定层” 与 “变动层”,用不同 key 缓存,避免并行 job 冲突。
- name: 缓存 stable 层
uses: actions/cache@v4
with:
path: /tmp/debs/stable
key: stable-${{ runner.os }}-${{ hashFiles('.github/stable-packages.txt') }}
restore-keys: stable-${{ runner.os }}-
- name: 缓存 dynamic 层
uses: actions/cache@v4
with:
path: /tmp/debs/dynamic
key: dynamic-${{ runner.os }}-${{ github.run_id }} # 每次新 key,只读
lookup-only: true # 禁止写,避免冲突
安装脚本优先用本地 deb,再回退到 apt-get:
sudo dpkg -iR /tmp/debs || sudo apt-get install -y valgrind
并发写锁通过 lookup-only: true 实现 “只读镜像”,解决多 job 同时写同一 key 失败问题。
四、可落地参数清单
| 参数 | 推荐值 | 说明 |
|---|---|---|
| retention-days | 3 | 稳定层保留 3 天,防止仓库级 10 GB 爆仓 |
| cache-hit-continue | false | 一旦命中立即跳过 apt-get,减少 5–10 s |
| restore-keys 降级深度 | ≤2 层 | 过深会拉回旧版本,出现 “幽灵依赖” |
| apt-get 超时 | 120 s | 网络抖动时快速失败,触发回滚镜像 |
| 并发 job 数 | ≤16 | 超过后缓存写冲突概率 >10 % |
五、回滚与监控
-
命中率面板
在 workflow 末输出缓存指标:- name: 上报缓存命中率 run: | echo "cache-hit=${{ steps.cache.outputs.cache-hit }}" >> $GITHUB_STEP_SUMMARY接入 GitHub API 做折线图,命中率 <50 % 时自动告警。
-
异常回退
检测到 apt-get 卡死超过 120 s,直接sudo pkill apt-get && sudo apt-get clean,并回退到容器镜像或离线快照,保证 CI 不红。
六、结论
系统包管理器是 GitHub Actions 里最容易被忽视的性能黑洞。把 “安装” 改 “拉层”、把 “在线” 改 “离线”、把 “单缓存” 改 “分片锁”,就能在零硬件成本下把 CI 时间砍半。下次写 workflow,先问自己一句:这 2 GB 的 deb,真的值得每次都从地球另一边重新下载吗?
参考资料
- GitHub Docs: 缓存依赖项以加快工作流程
- microsoft/playwright#23388:GitHub Actions 安装浏览器超时问题