Hotdry.
systems-engineering

拆解 GitHub Actions 内建包管理器为何成为 CI 性能隐形杀手与精简替代方案

从 apt 索引更新到并发缓存冲突,逐层拆解系统级包管理在 GitHub Actions 中的耗时黑洞,给出可落地的 Docker 层、离线快照与缓存分片参数。

GitHub Actions 的 “秒级启动” 承诺常常死在第一行 sudo apt-get update。官方 runner 每次从全新虚拟机开始,系统包管理器必须从零拉取索引、解析依赖、下载 deb—— 一条链轻松烧掉 30–180 秒。更隐蔽的是,当多架构源出现网络抖动,apt-get 会卡死 6 小时直到 Actions 超时,Playwright 等重型依赖一次就能拖进 2 GB,让缓存策略直接破产。本文把这条 “隐形耗时链” 拆成四段,给出可直接拷贝的精简替代方案与参数阈值。

一、耗时链拆解:四段黑洞从哪儿来

  1. 索引刷新
    Ubuntu 镜像默认同时配置 archive.ubuntu.comazure.archive.ubuntu.com,多架构索引叠加,apt update 平均 15 s,最坏 300 s。

  2. 依赖求解
    每次从零构建 solver 状态,重型包(qtbase5-dev、chromium)依赖图可达 800 节点,解析 5–20 s。

  3. 下载 & 解压
    GitHub 托管 runner 千兆带宽虽大,但 deb 解压是单线程 CPU 密集,2 GB 数据仍需 25 s。

  4. 缓存失效
    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 %

五、回滚与监控

  1. 命中率面板
    在 workflow 末输出缓存指标:

    - name: 上报缓存命中率
      run: |
        echo "cache-hit=${{ steps.cache.outputs.cache-hit }}" >> $GITHUB_STEP_SUMMARY
    

    接入 GitHub API 做折线图,命中率 <50 % 时自动告警。

  2. 异常回退
    检测到 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 安装浏览器超时问题
查看归档