GitLab CI/CD 缓存

缓存是作业下载和保存的一个或多个文件。使用相同缓存的后续作业不必再次下载文件,因此它们执行得更快。

要了解如何在您的 .gitlab-ci.yml 文件中定义缓存,请参阅 cache 参考

缓存与产物的不同之处

对依赖项使用缓存,例如您从 Internet 下载的包。 如果启用分布式缓存,则缓存存储在安装了 GitLab Runner 并上传到 S3 的位置。

使用产物在阶段之间传递中间构建结果。 产物由作业生成,存储在系统中,可以下载。

产物和缓存都定义了它们相对于项目目录的路径,并且不能链接到它之外的文件。

缓存

  • 使用 cache 关键字定义每个作业的缓存。否则它被禁用。
  • 后续流水线可以使用缓存。
  • 如果依赖项相同,同一流水线中的后续作业可以使用缓存。
  • 不同的项目不能共享缓存。

产物

  • 定义每个作业的产物。
  • 同一流水线后期的后续作业可以使用产物。
  • 不同的项目不能共享产物。
  • 默认情况下,产物会在 30 天后过期。您可以自定义到期时间
  • 如果启用了保留最新产物,则最新的产物不会过期。
  • 使用依赖来控制哪些作业获取工件。

良好的缓存实践

要确保缓存的最大可用性,请执行以下一项或多项操作:

  • 标记您的 runner 并在共享缓存的作业上使用标签。
  • 使用仅适用于特定项目的 runner。

  • 使用适合您工作流程的 key。例如,您可以为每个分支配置不同的缓存。

为了让 runner 有效地使用缓存,您必须执行以下操作之一:

  • 为您的所有工作使用一个 runner。
  • 使用具有分布式缓存的多个 runner,其中缓存存储在 S3 存储桶中。这些 runner 可以处于自动缩放模式,但并非必须如此。
  • 使用多个具有相同架构的 runner,并让这些 runner 共享一个公共的网络安装目录来存储缓存。这个目录应该使用 NFS 或类似的东西。这些 runner 必须处于自动缩放模式。

使用多个缓存

  • 引入于 13.10 版本。
  • 功能标志移除于 13.12 版本。

您最多可以有四个缓存:

test-job:
  stage: build
  cache:
    - key:
        files:
          - Gemfile.lock
      paths:
        - vendor/ruby
    - key:
        files:
          - yarn.lock
      paths:
        - .yarn-cache/
  script:
    - bundle config set --local path 'vendor/ruby'
    - bundle install
    - yarn install --cache-folder .yarn-cache
    - echo Run tests...

如果多个缓存与回退缓存键组合,则每次未找到缓存时都会获取全局回退缓存。

使用回退缓存键

每个缓存的回退键

引入于 16.0 版本。

每个缓存条目最多支持 5 个回退键:

test-job:
  stage: build
  cache:
    - key: cache-$CI_COMMIT_REF_SLUG
      fallback_keys:
        - cache-$CI_DEFAULT_BRANCH
        - cache-default
      paths:
        - vendor/ruby
  script:
    - bundle config set --local path 'vendor/ruby'
    - bundle install
    - yarn install --cache-folder .yarn-cache
    - echo Run tests...

回退键遵循与 cache:key 相同的处理逻辑,这意味着全名可能包括一个 -$index(基于缓存清除)和 -protected/-non_protected(如果在受保护的分支上启用缓存分离)后缀。

全局回退键

引入于 GitLab Runner 13.4。

您可以使用 $CI_COMMIT_REF_SLUG 预定义变量来指定您的 cache:key。例如,您的 $CI_COMMIT_REF_SLUGtest,您可以设置一个作业来下载带有 test 标签的缓存。

如果没有找到带有这个标签的缓存,你可以使用 CACHE_FALLBACK_KEY 来指定一个缓存,当不存在缓存时使用。

在以下示例中,如果未找到 $CI_COMMIT_REF_SLUG,作业将使用由 CACHE_FALLBACK_KEY 变量定义的键:

variables:
  CACHE_FALLBACK_KEY: fallback-key

job1:
  script:
    - echo
  cache:
    key: "$CI_COMMIT_REF_SLUG"
    paths:
      - binaries/

缓存提取的顺序是:

  1. 尝试检索 cache:key
  2. fallback_keys 中按顺序尝试检索每个条目
  3. 尝试检索 CACHE_FALLBACK_KEY 中的全局回退键

在检索到第一个成功的缓存后,缓存提取过程将停止。

禁用特定作业的缓存

如果全局定义缓存,则每个作业都使用相同的定义。您可以为每个作业覆盖此行为。

要为作业完全禁用它,请使用空列表:

job:
  cache: []

继承全局配置,但覆盖每个作业的特定设置

您可以使用锚点覆盖缓存设置而不覆盖全局缓存。例如,如果您想覆盖一项作业的 policy

default:
  cache: &global_cache
    key: $CI_COMMIT_REF_SLUG
    paths:
      - node_modules/
      - public/
      - vendor/
    policy: pull-push

job:
  cache:
    # inherit all global cache settings
    <<: *global_cache
    # override the policy
    policy: pull

获取更多信息,查看 cache: policy

缓存的常见用例

通常,您每次运行作业时都使用缓存来避免下载内容,例如依赖项或库。可以缓存 Node.js 包、PHP 包、Ruby gems、Python 库等。

获取示例,查看 GitLab CI/CD 模板

在同一分支中的作业之间共享缓存

要让每个分支中的作业使用相同的缓存,请使用 key: $CI_COMMIT_REF_SLUG 定义一个缓存:

cache:
  key: $CI_COMMIT_REF_SLUG

此配置可防止您意外覆盖缓存。但是,合并请求的第一个流水线很慢。下一次提交被推送到分支时,缓存会被重用,作业运行得更快。

要启用每个作业和每个分支的缓存:

cache:
  key: "$CI_JOB_NAME-$CI_COMMIT_REF_SLUG"

要启用每个阶段和每个分支缓存:

cache:
  key: "$CI_JOB_STAGE-$CI_COMMIT_REF_SLUG"

在不同分支的作业之间共享缓存

要在所有分支和所有作业之间共享缓存,请对所有内容使用相同的键:

cache:
  key: one-key-to-rule-them-all

要在分支之间共享缓存,但为每个作业拥有唯一的缓存:

cache:
  key: $CI_JOB_NAME

缓存 Node.js 依赖项

如果您的项目使用 npm 安装 Node.js 依赖项,以下示例全局定义了 cache,以便所有作业都继承它。 默认情况下,npm 将缓存数据存储在主文件夹 (~/.npm) 中。但是,您不能缓存项目目录之外的内容。 您告诉 npm 使用 ./.npm,并为每个分支缓存它:

#
# https://gitlab.com/gitlab-org/gitlab/-/tree/master/lib/gitlab/ci/templates/Nodejs.gitlab-ci.yml
#
image: node:latest

# Cache modules in between jobs
cache:
  key: $CI_COMMIT_REF_SLUG
  paths:
    - .npm/

before_script:
  - npm ci --cache .npm --prefer-offline

test_async:
  script:
    - node ./specs/start.js ./specs/async.spec.js

从锁定文件计算缓存键

您可以使用 cache:key:files 从像 package-lock.jsonyarn.lock 之类的锁文件中计算缓存键,然后在许多作业中重用。

# Cache modules using lock file
cache:
  key:
    files:
      - package-lock.json
  paths:
    - .npm/

如果您使用 Yarn,您可以使用 yarn-offline-mirror 来缓存压缩的 node_modules 压缩包。缓存生成得更快,因为需要压缩的文件更少:

job:
  script:
    - echo 'yarn-offline-mirror ".yarn-cache/"' >> .yarnrc
    - echo 'yarn-offline-mirror-pruning true' >> .yarnrc
    - yarn install --frozen-lockfile --no-progress
  cache:
    key:
      files:
        - yarn.lock
    paths:
      - .yarn-cache/

缓存 PHP 依赖项

如果您的项目使用 Composer 安装 PHP 依赖项,则以下示例全局定义了 cache,以便所有作业都继承它。PHP 库模块安装在 vendor/ 中,并按分支缓存:

#
# https://gitlab.com/gitlab-org/gitlab/-/tree/master/lib/gitlab/ci/templates/PHP.gitlab-ci.yml
#
image: php:7.2

# Cache libraries in between jobs
cache:
  key: $CI_COMMIT_REF_SLUG
  paths:
    - vendor/

before_script:
  # Install and run Composer
  - curl --show-error --silent "https://getcomposer.org/installer" | php
  - php composer.phar install

test:
  script:
    - vendor/bin/phpunit --configuration phpunit.xml --coverage-text --colors=never

缓存 Python 依赖项

如果您的项目使用 pip 安装 Python 依赖项,则以下示例全局定义了 cache,以便所有作业都继承它。pip 的缓存定义在 .cache/pip/ 下,并且每个分支都缓存:

#
# https://gitlab.com/gitlab-org/gitlab/-/tree/master/lib/gitlab/ci/templates/Python.gitlab-ci.yml
#
image: python:latest

# Change pip's cache directory to be inside the project directory since we can
# only cache local items.
variables:
  PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"

# Pip's cache doesn't store the python packages
# https://pip.pypa.io/en/stable/reference/pip_install/#caching
cache:
  paths:
    - .cache/pip

before_script:
  - python -V               # Print out python version for debugging
  - pip install virtualenv
  - virtualenv venv
  - source venv/bin/activate

test:
  script:
    - python setup.py test
    - pip install ruff
    - ruff --format=gitlab .

缓存 Ruby 依赖项

如果您的项目使用 Bundler 安装 gem 依赖项,以下示例全局定义了 cache,以便所有作业都继承它。 Gems 安装在 vendor/ruby/ 中,并按分支缓存:

#
# https://gitlab.com/gitlab-org/gitlab/-/tree/master/lib/gitlab/ci/templates/Ruby.gitlab-ci.yml
#
image: ruby:2.6

# Cache gems in between builds
cache:
  key: $CI_COMMIT_REF_SLUG
  paths:
    - vendor/ruby

before_script:
  - ruby -v                                        # Print out ruby version for debugging
  - bundle config set --local path 'vendor/ruby'   # The location to install the specified gems to
  - bundle install -j $(nproc)                     # Install dependencies into ./vendor/ruby

rspec:
  script:
    - rspec spec

如果您的作业需要不同的 gem,请在全局 cache 定义中使用 prefix 关键字。 此配置为每个作业生成不同的缓存。

例如,测试作业可能不需要与部署到生产的作业相同的 gem:

cache:
  key:
    files:
      - Gemfile.lock
    prefix: $CI_JOB_NAME
  paths:
    - vendor/ruby

test_job:
  stage: test
  before_script:
    - bundle config set --local path 'vendor/ruby'
    - bundle install --without production
  script:
    - bundle exec rspec

deploy_job:
  stage: production
  before_script:
    - bundle config set --local path 'vendor/ruby'   # The location to install the specified gems to
    - bundle install --without test
  script:
    - bundle exec deploy

缓存 Go 依赖项

如果您的项目使用 Go Modules 安装 Go 依赖项,以下示例在 go-cache 模板中定义了 cache,任何作业都可以扩展该模板。Go 模块安装在 ${GOPATH}/pkg/mod/ 中,并为所有 go 项目缓存:

.go-cache:
  variables:
    GOPATH: $CI_PROJECT_DIR/.go
  before_script:
    - mkdir -p .go
  cache:
    paths:
      - .go/pkg/mod/

test:
  image: golang:1.13
  extends: .go-cache
  script:
    - go test ./... -v -short

缓存的可用性

缓存是一种优化,但不能保证始终有效。您可能要在需要它们的每个作业中重新生成缓存文件。

在你定义了一个 cache in .gitlab-ci.yml 之后,缓存的可用性取决于:

  • runner 的 executor 类型。
  • 是否使用不同的 runner 在作业之间传递缓存。

缓存的存储位置

为作业定义的所有缓存都存档在单个 cache.zip 文件中。 runner 配置定义了文件的存储位置。默认情况下,缓存存储在安装了 GitLab Runner 的机器上。位置还取决于 executor 的类型。

Runner executor 缓存的默认路径
Shell 在本地,在 gitlab-runner 用户的主目录下:/home/gitlab-runner/cache/<user>/<project>/<cache-key>/cache.zip
Docker 在本地,在 Docker 卷下:/var/lib/docker/volumes/<volume-id>/_data/<user>/<project>/<cache-key>/cache.zip
Docker Machine(自动缩放 runner) 与 Docker 执行器相同。

如果您使用缓存和产物在作业中存储相同的路径,缓存可能会被覆盖,因为缓存在产物之前恢复。

缓存键名称

引入于 15.0 版本。

除了回退缓存键之外,后缀被添加到缓存键中。

例如,假设 cache.key 设置为 $CI_COMMIT_REF_SLUG,并且我们有两个分支 mainfeature,那么生成的缓存键如下表所述:

分支名称 缓存键
main main-protected
feature feature-non_protected
对所有分支使用相同的缓存

引入于 15.0 版本。

如果您不想使用缓存键名称,您可以让所有分支(受保护和不受保护)使用相同的缓存。

根据缓存键名称的缓存分离是一项安全功能,仅应在所有具有开发者角色的用户都受高度信任的环境中禁用。

为所有分支使用相同的缓存:

  1. 在顶部栏上,选择 主菜单 > 项目 并找到您的项目。
  2. 在左侧边栏,选择 设置 > CI/CD
  3. 展开 流水线通用设置
  4. 清除 为受保护的分支使用单独的缓存 复选框。
  5. 选择 保存修改

归档和提取的工作原理

此示例显示了两个连续阶段中的两个作业:

stages:
  - build
  - test

before_script:
  - echo "Hello"

job A:
  stage: build
  script:
    - mkdir vendor/
    - echo "build" > vendor/hello.txt
  cache:
    key: build-cache
    paths:
      - vendor/
  after_script:
    - echo "World"

job B:
  stage: test
  script:
    - cat vendor/hello.txt
  cache:
    key: build-cache
    paths:
      - vendor/

如果一台机器安装了一个 runner,那么您项目的所有作业都在同一台主机上运行:

  1. 流水线启动。
  2. job A 运行。
  3. before_script 被执行。
  4. script 被执行。
  5. after_script 被执行。
  6. cache 运行,vendor/ 目录被压缩到 cache.zip 中。这个文件然后被保存在基于 runner 设置和 cache: key 的目录中。
  7. job B 运行。
  8. 提取缓存(如果找到)。
  9. before_script 被执行。
  10. script 被执行。
  11. 流水线完成。

通过在单台机器上使用单个 runner,您不会遇到 job B 可能在不同于 job A 的 runner 上执行的问题。这种设置保证缓存可以在阶段之间重用。只有在同一 runner/机器中执行从 build 阶段到 test 阶段时,它才有效。否则,缓存可能不可用

在缓存过程中,还需要考虑以下几点:

  • 如果具有其它缓存配置的其它作业,将其缓存保存在同一个 zip 文件中,则会被覆盖。如果使用基于 S3 的共享缓存,文件将额外上传到 S3,到基于缓存键的对象。因此,具有不同路径但具有相同缓存键的两个作业会覆盖它们的缓存。
  • cache.zip 中提取缓存时,zip 文件中的所有内容都被提取到作业的工作目录(通常是下拉的仓库)中,并且 runner 不介意 job A 的存档是否覆盖 job B 存档中的内容。

它以这种方式工作,因为为一个 runner 创建的缓存在被另一个 runner 使用时通常无效。不同的 runner 可能会在不同的架构上运行(例如,当缓存包含二进制文件时)。此外,因为不同的步骤可能由在不同机器上运行的 runner 执行,所以这是一个安全的默认值。

清除缓存

Runners 使用 cache 通过重用现有数据来加速作业的执行。这有时会导致不一致的行为。

有两种方法可以从缓存的新副本开始。

通过 cache:key 清除缓存

更改 .gitlab-ci.yml 文件中 cache: key 的值。 下次流水线运行时,缓存将存储在不同的位置。

手动清除缓存

您可以在 UI 中清除缓存:

  1. 在顶部栏上,选择 主菜单 > 项目 并找到您的项目。
  2. 在左侧边栏上,选择 CI/CD > 流水线 页面。
  3. 在右上角,选择 清除 Runner 缓存

在下一次提交时,您的 CI/CD 作业使用新的缓存。

note每次手动清除缓存时,内部缓存名称都会更新。名称使用格式cache-<index>,索引递增 1。旧缓存不会被删除。您可以从 runner 存储中手动删除这些文件。

故障排查

缓存不匹配

如果缓存不匹配,请按照以下步骤进行故障排查。

缓存不匹配的原因 如何修复
您使用附加到一个项目的多个独立 runner(不在自动缩放模式下),而没有共享缓存。 为您的项目仅使用一个 runner,或使用多个启用了分布式缓存的 runner。
您在未启用分布式缓存的情况下,使用自动缩放模式 runner。 将自动缩放 runner 配置为使用分布式缓存。
安装 runner 的机器磁盘空间不足,或者,如果您设置了分布式缓存,则存储缓存的 S3 存储桶没有足够的空间。 确保清除一些空间以允许存储新缓存。 没有自动的方法来做到这一点。
对于缓存不同路径的作业,您可以使用相同的 key使用不同的缓存键,将缓存存档存储到不同的位置,并且不会覆盖错误的缓存。
您未启用 runner 上的分布式 runner 缓存设置 Shared = false 并重新配置您的跑步者。

缓存不匹配示例 1

如果您的项目只分配了一个 runner,则缓存默认存储在 runner 的机器上。

如果两个作业具有相同的缓存键但路径不同,则可以覆盖缓存。 例如:

stages:
  - build
  - test

job A:
  stage: build
  script: make build
  cache:
    key: same-key
    paths:
      - public/

job B:
  stage: test
  script: make test
  cache:
    key: same-key
    paths:
      - vendor/
  1. job A 运行。
  2. public/ 缓存为 cache.zip
  3. job B 运行。
  4. 解压之前的缓存,如果有的话。
  5. vendor/ 缓存为 cache.zip,覆盖上一个。
  6. 下一次 job A 运行时,它使用的是不同的 job B 缓存,因此无效。

要解决此问题,请为每个作业使用不同的 keys

缓存不匹配示例 2

在此示例中,您为项目分配了多个 runner,并且未启用分布式缓存。

流水线第二次运行时,您希望 job Ajob B 重新使用它们的缓存(在这种情况下是不同的):

stages:
  - build
  - test

job A:
  stage: build
  script: build
  cache:
    key: keyA
    paths:
      - vendor/

job B:
  stage: test
  script: test
  cache:
    key: keyB
    paths:
      - vendor/

即使 key 不同,如果作业在后续流水线中的不同 runner 上运行,则缓存文件可能会在每个阶段之前被“清理”。