GitLab CI/CD 中的缓存
- Tier: Free, Premium, Ultimate
- Offering: GitLab.com, GitLab Self-Managed, GitLab Dedicated
缓存是指作业下载并保存的一个或多个文件。使用相同缓存的后续作业无需再次下载这些文件,因此执行速度更快。
要了解如何在 .gitlab-ci.yml 文件中定义缓存,
请参阅 cache 参考。
缓存与制品的区别
使用缓存来存储依赖项,例如从互联网下载的包。 缓存存储在安装了 GitLab Runner 的位置,如果启用了分布式缓存,则会上传到 S3。
使用制品在阶段之间传递构建中间结果。 制品由作业生成,存储在 GitLab 中,并且可以下载。
制品和缓存都定义相对于项目目录的路径,并且不能链接到项目外部的文件。
缓存
- 使用
cache关键字按作业定义缓存。否则将被禁用。 - 后续流水线可以使用缓存。
- 如果依赖项相同,同一流水线中的后续作业可以使用缓存。
- 不同项目无法共享缓存。
- 默认情况下,受保护和不受保护的分支不共享缓存。但是,你可以更改此行为。
制品
- 按作业定义制品。
- 同一流水线后续阶段的作业可以使用制品。
- 制品默认在 30 天后过期。你可以定义自定义的过期时间。
- 如果启用了保留最新制品,最新制品不会过期。
- 使用dependencies来控制哪些作业获取制品。
良好的缓存实践
为确保缓存的最高可用性,请执行以下一项或多项操作:
- 标记你的 runners,并在共享缓存的作业上使用该标签。
- 使用仅对特定项目可用的 runners。
- 使用适合你工作流的
key。例如, 你可以为每个分支配置不同的缓存。
为了使 runners 能够高效地处理缓存,你必须执行以下操作之一:
- 为所有作业使用单个 runner。
- 使用多个启用了分布式缓存的 runners, 缓存存储在 S3 存储桶中。GitLab.com 上的实例 runners 就是这样的工作方式。这些 runners 可以处于自动扩展模式, 但不是必须的。要管理缓存对象, 请应用生命周期规则在一段时间后删除缓存对象。 生命周期规则在对象存储服务器上可用。
- 使用具有相同架构的多个 runners,并让这些 runners 共享一个通过网络挂载的目录来存储缓存。此目录应使用 NFS 或类似技术。 这些 runners 必须处于自动扩展模式。
使用多个缓存
最多可以有四个缓存:
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...如果多个缓存与备用缓存键结合使用, 则每次找不到缓存时都会获取全局备用缓存。
使用备用缓存键
每个缓存的备用键
每个缓存条目最多支持五个备用键,使用 fallback_keys 关键字。
当作业找不到缓存键时,作业会尝试获取备用缓存。
备用键按顺序搜索,直到找到缓存。如果找不到缓存,
作业将不使用缓存运行。例如:
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
- echo Run tests...在此示例中:
- 作业查找
cache-$CI_COMMIT_REF_SLUG缓存。 - 如果找不到
cache-$CI_COMMIT_REF_SLUG,作业将查找cache-$CI_DEFAULT_BRANCH作为备用选项。 - 如果也找不到
cache-$CI_DEFAULT_BRANCH,作业将查找cache-default作为第二个备用选项。 - 如果都找不到,作业将不使用缓存下载所有 Ruby 依赖项,
但在作业完成时为
cache-$CI_COMMIT_REF_SLUG创建新缓存。
备用键遵循与 cache:key 相同的处理逻辑:
- 如果你手动清除缓存,每个缓存的备用键会像其他缓存键一样附加索引。
- 如果启用了为受保护分支使用单独的缓存设置,
每个缓存的备用键会附加
-protected或-non_protected。
全局备用键
你可以使用 $CI_COMMIT_REF_SLUG 预定义变量
来指定你的 cache:key。例如,如果你的
$CI_COMMIT_REF_SLUG 是 test,你可以设置一个作业来下载标记为 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/缓存提取的顺序是:
- 尝试获取
cache:key - 按顺序尝试获取
fallback_keys中的每个条目 - 尝试获取
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:
# 继承所有全局缓存设置
<<: *global_cache
# 覆盖 policy
policy: pull有关更多信息,请参阅 cache: policy。
缓存的常见用例
通常你使用缓存来避免每次运行作业时下载内容,例如依赖项 或库。Node.js 包、PHP 包、Ruby gem、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使用变量控制作业的缓存策略
为了减少仅在拉取策略方面不同的作业的重复,你可以使用 CI/CD 变量。
例如:
conditional-policy:
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
variables:
POLICY: pull-push
- if: $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH
variables:
POLICY: pull
stage: build
cache:
key: gems
policy: $POLICY
paths:
- vendor/bundle
script:
- echo "This job pulls and pushes the cache depending on the branch"
- echo "Downloading dependencies..."在此示例中,作业的缓存策略是:
- 对默认分支的更改使用
pull-push。 - 对其他分支的更改使用
pull。
缓存 Node.js 依赖项
如果你的项目使用 npm 来安装 Node.js
依赖项,以下示例定义了一个默认的 cache,以便所有作业继承它。
默认情况下,npm 将缓存数据存储在主文件夹(~/.npm)。但是,你
不能缓存项目目录外的内容。
相反,告诉 npm 使用 ./.npm,并按分支缓存它:
default:
image: node:latest
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.json 或 yarn.lock)计算缓存
键,并在许多作业中重用它。
default:
cache: # 使用锁文件缓存模块
key:
files:
- package-lock.json
paths:
- .npm/如果你使用 Yarn,你可以使用 yarn-offline-mirror
来缓存压缩的 node_modules tarball。缓存生成得更快,因为
需要压缩的文件更少:
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/使用 Ccache 缓存 C/C++ 编译
如果你正在编译 C/C++ 项目,可以使用 Ccache 来 加快构建时间。Ccache 通过缓存之前的编译并检测何时再次执行相同的编译来加速重新编译。在构建像 Linux 内核这样的大型项目时, 你可以期望编译速度显著提高。
使用 cache 在作业之间重用创建的缓存,例如:
job:
cache:
paths:
- ccache
before_script:
- export PATH="/usr/lib/ccache:$PATH" # 使用 ccache 覆盖编译器路径(此示例适用于 Debian)
- export CCACHE_DIR="${CI_PROJECT_DIR}/ccache"
- export CCACHE_BASEDIR="${CI_PROJECT_DIR}"
- export CCACHE_COMPILERCHECK=content # 容器中的编译器 mtime 可能会改变,改用校验和
script:
- ccache --zero-stats || true
- time make # 实际构建你的代码,同时测量时间和缓存效率。
- ccache --show-stats || true如果你在单个仓库中有多个项目,不需要为每个项目单独设置 CCACHE_BASEDIR。
缓存 PHP 依赖项
如果你的项目使用 Composer 来安装
PHP 依赖项,以下示例定义了一个默认的 cache,以便
所有作业继承它。PHP 库模块安装在 vendor/ 中,
并按分支缓存:
default:
image: php:latest
cache: # 在作业之间缓存库
key: $CI_COMMIT_REF_SLUG
paths:
- vendor/
before_script:
# 安装并运行 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/ 下,
并按分支缓存:
default:
image: python:latest
cache: # pip 的缓存不存储 python 包
paths: # https://pip.pypa.io/en/stable/topics/caching/
- .cache/pip
before_script:
- python -V # 打印 python 版本用于调试
- pip install virtualenv
- virtualenv venv
- source venv/bin/activate
variables: # 将 pip 的缓存目录更改为项目目录内,因为我们只能缓存本地项目。
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
test:
script:
- python setup.py test
- pip install ruff
- ruff --format=gitlab .缓存 Ruby 依赖项
如果你的项目使用 Bundler 来安装
gem 依赖项,以下示例定义了一个默认的 cache,以便所有
作业继承它。Gems 安装在 vendor/ruby/ 中,并按分支缓存:
default:
image: ruby:latest
cache: # 在构建之间缓存 gems
key: $CI_COMMIT_REF_SLUG
paths:
- vendor/ruby
before_script:
- ruby -v # 打印 ruby 版本用于调试
- bundle config set --local path 'vendor/ruby' # 安装指定 gems 的位置
- bundle install -j $(nproc) # 将依赖项安装到 ./vendor/ruby
rspec:
script:
- rspec spec如果你的作业需要不同的 gems,请在全局 cache 定义中使用 prefix
关键字。此配置为每个作业生成不同的缓存。
例如,测试作业可能不需要与部署到生产环境的作业相同的 gems:
default:
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' # 安装指定 gems 的位置
- 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:latest
extends: .go-cache
script:
- go test ./... -v -short缓存 curl 下载
如果你的项目使用 cURL 来下载依赖项或文件, 你可以缓存下载的内容。当有新的下载可用时, 文件会自动更新。
job:
script:
- curl --remote-time --time-cond .curl-cache/caching.md --output .curl-cache/caching.md "https://docs.gitlab.com/ci/caching/"
cache:
paths:
- .curl-cache/在此示例中,cURL 从 Web 服务器下载文件并将其保存到 .curl-cache/ 中的本地文件。
--remote-time 标志保存服务器报告的最后修改时间,
cURL 使用 --time-cond 将其与缓存文件的时间戳进行比较。如果远程文件具有
更新的时间戳,本地缓存会自动更新。
缓存的可用性
缓存是一种优化,但不能保证总是有效。你可能需要 在需要它们的每个作业中重新生成缓存文件。
在 .gitlab-ci.yml 中定义缓存 后,
缓存的可用性取决于:
- runner 的执行器类型。
- 是否使用不同的 runners 在作业之间传递缓存。
缓存存储位置
为作业定义的所有缓存都归档在一个单独的 cache.zip 文件中。
runner 配置定义了文件的存储位置。默认情况下,缓存
存储在安装了 GitLab Runner 的机器上。位置还取决于执行器类型。
| Runner 执行器 | 缓存的默认路径 |
|---|---|
| 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 (自动扩展 runners) | 与 Docker 执行器相同。 |
如果你在作业中使用缓存和制品来存储相同的路径,缓存可能会 被覆盖,因为缓存在制品之前被恢复。
缓存键名称
除了全局备用缓存键外,都会在缓存键后添加后缀。
例如,假设 cache.key 设置为 $CI_COMMIT_REF_SLUG,并且我们有两个分支 main
和 feature,那么下表表示生成的缓存键:
| 分支名称 | 缓存键 |
|---|---|
main |
main-protected |
feature |
feature-non_protected |
为所有分支使用相同的缓存
如果你不想使用缓存键名称, 可以让所有分支(受保护和不受保护的)使用相同的缓存。
使用缓存键名称的缓存分离是一个安全功能, 并且应该仅在所有具有 Developer 角色的用户高度可信的环境中禁用。
要为所有分支使用相同的缓存:
- 在左侧边栏,选择 Search or go to 并找到你的项目。
- 选择 Settings > CI/CD。
- 展开 General pipelines。
- 清除 Use separate caches for protected branches 复选框。
- 选择 Save changes。
归档和提取的工作原理
此示例显示两个连续阶段中的两个作业:
stages:
- build
- test
default:
cache:
key: build-cache
paths:
- vendor/
before_script:
- echo "Hello"
job A:
stage: build
script:
- mkdir vendor/
- echo "build" > vendor/hello.txt
after_script:
- echo "World"
job B:
stage: test
script:
- cat vendor/hello.txt如果一台机器上安装了一个 runner,那么你项目的所有作业 都在同一主机上运行:
- 流水线开始。
job A运行。- 缓存被提取(如果找到)。
- 执行
before_script。 - 执行
script。 - 执行
after_script。 - 运行
cache,vendor/目录被压缩到cache.zip。 然后根据 runner 的设置 和cache: key将此文件保存在目录中。 job B运行。- 缓存被提取(如果找到)。
- 执行
before_script。 - 执行
script。 - 流水线结束。
通过在单台机器上使用单个 runner,你不会遇到
job B 可能在与 job A 不同的 runner 上执行的问题。此设置确保了
缓存可以在阶段之间重用。它仅在执行从 build 阶段
到 test 阶段在同一 runner/机器上时才有效。否则,缓存可能不可用。
在缓存过程中,还需要考虑以下几点:
- 如果其他具有不同缓存配置的作业将 其缓存保存在同一个 zip 文件中,它将被覆盖。如果使用基于 S3 的共享缓存, 文件会额外上传到基于缓存键的 S3 对象中。因此,两个具有不同路径但相同缓存键的作业会 覆盖彼此的缓存。
- 当从
cache.zip提取缓存时,zip 文件中的所有内容 都会在作业的工作目录(通常是拉取下来的仓库)中提取, runner 不介意job A的归档是否覆盖了job B归档中的内容。
它之所以这样工作,是因为为一个 runner 创建的缓存 通常在由另一个 runner 使用时无效。不同的 runner 可能在不同的架构上运行(例如,当缓存包含二进制文件时)。 此外,因为不同的步骤可能由在不同机器上运行的 runners 执行,所以这是一个安全的默认设置。
清除缓存
Runners 使用缓存通过重用现有数据来加速 你的作业执行。这有时会导致 不一致的行为。
有两种方式可以从缓存的全新副本开始。
通过更改 cache:key 清除缓存
更改 .gitlab-ci.yml 文件中 cache: key 的值。
下次运行流水线时,缓存将存储在不同的位置。
手动清除缓存
你可以在 GitLab UI 中清除缓存:
- 在左侧边栏,选择 Search or go to 并找到你的项目。
- 选择 Build > Pipelines。
- 在右上角,选择 Clear runner caches。
在下一次提交时,你的 CI/CD 作业将使用新缓存。
每次手动清除缓存时,内部缓存名称都会更新。名称使用 cache-<index> 格式,索引递增一。旧缓存不会被删除。你可以手动从 runner 存储中删除这些文件。
故障排除
缓存不匹配
如果你遇到缓存不匹配问题,请按照以下步骤进行故障排除。
| 缓存不匹配的原因 | 如何修复 |
|---|---|
| 你使用多个独立的 runners(不在自动扩展模式)附加到一个项目但没有共享缓存。 | 为你的项目只使用一个 runner,或使用启用了分布式缓存的多个 runners。 |
| 你使用的是没有启用分布式缓存的自动扩展模式 runners。 | 配置自动扩展 runner 以使用分布式缓存。 |
| runner 安装在的机器磁盘空间不足,或者如果你设置了分布式缓存,存储缓存的 S3 存储桶空间不足。 | 确保清除一些空间以允许存储新缓存。没有自动方式可以做到这一点。 |
你对缓存不同路径的作业使用了相同的 key。 |
使用不同的缓存键,以便缓存存档存储在不同的位置,并且不会覆盖错误的缓存。 |
| 你没有在你的 runners 上启用分布式 runner 缓存。 | 设置 Shared = false 并重新配置你的 runners。 |
缓存不匹配示例 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/job A运行。public/被缓存为cache.zip。job B运行。- 之前的任何缓存都会被解压。
vendor/被缓存为cache.zip并覆盖了之前的缓存。- 下次
job A运行时,它使用job B的缓存,这是不同的 因此无效。
要解决此问题,为每个作业使用不同的 keys。
缓存不匹配示例 2
在此示例中,你的项目分配了多个 runners, 并且未启用分布式缓存。
第二次运行流水线时,你希望 job A 和 job 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 不同,缓存的文件也可能在每次
阶段之前被"清理",如果作业在不同的 runners 上运行,在后续的流水线中。
并发 runners 缺少本地缓存
如果你配置了多个具有 Docker 执行器的并发 runners,本地缓存的文件可能 不会像你期望的那样存在于并发运行的作业中。缓存卷的名称为每个 runner 实例构造唯一, 因此一个 runner 实例缓存的文件在另一个 runner 实例的缓存中找不到。
要在并发 runners 之间共享缓存,你可以:
- 使用 runners 的
config.toml中的[runners.docker]部分配置主机上的单个挂载点, 映射到每个容器中的/cache,防止 runner 创建唯一的卷名。 - 使用分布式缓存。