极狐GitLab

流水线每次都从零装依赖,缓存该怎么配才不白跑

极狐GitLab
2026年10月13日
15530
分享:

流水线每次都从零装依赖,缓存该怎么配才不白跑

定义框:CI/CD 缓存(cache)是指作业下载并保存的一个或多个文件,后续使用同一缓存的作业无需再次下载,从而缩短执行时间。它在极狐GitLab 中由 .gitlab-ci.yml 的 cache 关键字定义,默认处于禁用状态,必须逐作业显式声明才会生效。

一条流水线的耗时里,真正花在"编译和测试"上的往往不是大头。前端项目跑一次 npm ci、后端项目拉一遍 Maven 依赖、Python 项目装一轮虚拟环境——这些动作每次都从头来一遍,几分钟就这么耗掉了。更要命的是它是隐性的:没人会觉得"装依赖花了三分钟"是个问题,直到某天有人把三条流水线的日志摊开对比,才发现同一份依赖在一小时内被下载了十几次。

本文从缓存与产物的分工讲起,给出一套可以直接抄进 .gitlab-ci.yml 的配置,并列出几个让缓存"配了却没命中"的高频原因。

为什么流水线总在重复下载依赖

根因不在缓存没配,而在缓存的生效条件比想象中苛刻。缓存不是"配了就有",它的可用性取决于三件事:Runner 的执行器类型、作业之间是否由同一个 Runner 承接、以及缓存键是否匹配。

默认配置下,缓存存储在安装 Runner 的那台机器上,归档为一个 cache.zip。Shell 执行器落在 /home/gitlab-runner/cache////cache.zip,Docker 执行器落在 Docker 卷下。一旦你的 Runner 是自动扩缩的——作业每次落到一台新机器上——本地缓存就找不到了。这时缓存看起来"配了但完全没生效",日志里依然每次全量下载。

第二个常见原因是缓存键漂移。用 $CI_COMMIT_REF_SLUG 做键时,每个分支各有一份缓存,特性分支永远享受不到主干的缓存红利,第一次跑依然是全量。

缓存与产物该怎么分工

很多人把两者混用,结果既没提速又占了两份存储。官方给出的边界很清晰:缓存用来装从互联网下载的依赖项,产物用来在阶段之间传递中间构建结果。

维度

缓存(cache)

产物(artifacts)

用途

依赖项,如 npm、Maven 包

阶段间的中间构建结果

存储位置

Runner 所在机器,或启用分布式缓存后上传到 S3

极狐GitLab 实例内

跨流水线

后续流水线可以复用

仅限同一流水线的后续阶段

跨项目

不共享

通过 dependencies 控制传递

过期

无默认过期时间

默认 30 天过期,可自定义 expire_in

一个容易忽略的细节:如果同一个路径既被缓存又被产物使用,由于缓存在产物之前恢复,缓存可能被产物覆盖。规划路径时要错开。

四步配好一套能用的缓存

第一步,用锁文件做缓存键。 让缓存键跟随依赖清单变化,而不是跟随分支名。这样依赖没变时所有分支共享同一份缓存,依赖变了才重建:

.node_cache: &node_cache
  key:
    files:
      - package-lock.json
  paths:
    - node_modules/
  policy: pull-push

install:
  stage: install
  cache:
    - <<: *node_cache
      policy: pull-push
  script:
    - npm ci --cache .npm --prefer-offline

test:
  stage: test
  cache:
    - <<: *node_cache
      policy: pull
  script:
    - npm run test

install 作业用 pull-push 读写缓存,test 作业只用 pull 读取——避免多个作业同时回写造成无谓的上传开销。

第二步,给缓存加回退键。 依赖清单变化后新缓存还没建立,此时回退到主干缓存能省掉一次全量下载。每个缓存条目最多支持五个回退键:

test-job:
  stage: build
  cache:
    - key:
        files:
          - Gemfile.lock
      fallback_keys:
        - cache-$CI_DEFAULT_BRANCH
        - cache-default
      paths:
        - vendor/ruby
  script:
    - bundle config set --local path 'vendor/ruby'
    - bundle install

检索顺序是:先找 cache:key,再按序找 fallback_keys 里的每一项,最后才是全局的 CACHE_FALLBACK_KEY。命中第一个就停止。

第三步,自动扩缩的 Runner 必须配分布式缓存。 否则缓存落在本机,机器回收即失效。做法是让多个 Runner 共享一个 S3 存储桶,并在对象存储上配置生命周期规则定期清理旧缓存对象。

第四步,按标签把共享缓存的作业钉在同一个 Runner 上。 给 Runner 打标签并在共享缓存的作业上使用该标签,是官方列出的首要实践之一;项目专属 Runner 同样能提升命中率。

缓存配了却不命中的几个坑

坑一:受保护分支的缓存键后缀。 默认情况下受保护分支和非受保护分支不共享缓存。缓存键会按分支保护状态和触发者角色附加 -protected 或 -non_protected 后缀。同样是 feature 分支,开发者角色触发得到 feature-non_protected,维护者触发得到 feature-protected——两份缓存互不相通。可以在项目设置里清除「为受保护分支使用单独的缓存」,但这属于安全特性,只有在所有开发者都高度可信时才建议关闭。

坑二:多缓存与全局回退键叠加。 每个作业最多四个缓存;如果多个缓存都配了回退键,那么每次找不到缓存时都会去取一次全局回退缓存,上传下载次数翻倍。

坑三:把缓存当成可靠存储。 官方文档明确写了缓存是一种优化,不保证始终可用。任何作业都必须能在没有缓存的情况下跑通——不要把构建必需的中间产物放进缓存路径。

坑四:全量清理后索引漂移。 手动清除缓存后,缓存键会像其他键一样附加一个索引,导致旧键全部失配,这是一次性的,不必惊慌。

常见问题

缓存和产物能互相替代吗?

不能。缓存面向跨流水线的依赖项复用,产物面向同一流水线内的阶段间传递。用产物传依赖会占用实例存储并 30 天后过期,用缓存传构建结果则可能因不命中而丢失。

为什么改了依赖清单之后,缓存反而更慢了?

cache:key:files 依赖文件内容生成键,清单一变键就变,新分支首次必然全量下载。这是预期行为,用 fallback_keys 回退到主干缓存可以缓解。

多个 Runner 时缓存一定失效吗?

不一定。除了共享 S3 的分布式缓存,也可以让多个同架构 Runner 挂载同一个 NFS 目录,但这些 Runner 必须处于自动扩缩模式。


想把流水线耗时压下来? 极狐GitLab 提供从缓存策略、Runner 弹性伸缩到依赖代理的完整 CI/CD 提速能力,支持私有化部署与 SaaS 两种形态。访问 极狐GitLab 官网 了解 CI/CD 实践方案,或查阅 文档中心 中的缓存配置章节。