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...

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

使用回退缓存键

引入于 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/

禁用特定作业的缓存

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

要为作业完全禁用它,请使用空哈希:

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 flake8
    - flake8 .

缓存 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使用不同的缓存键,将缓存存档存储到不同的位置,并且不会覆盖错误的缓存。

缓存不匹配示例 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 上运行,则缓存文件可能会在每个阶段之前被“清理”。