Gitlab CI/CD 之 Gitlab Runner Docker Executor 缓存问题
定义一个流水线
在我们使用Gitlab的CICD的时候会定义一个Pipeline,Pipeline会由多个stage组成,stage整体是串行的,中间会存在并行任务。
如下是一个前端vue、后端.net的项目的自动化打包流水线
image: docker:20.10.5-dind
stages:
- prebuild
- build
- test
- publish-ui
- publish-api
- image
prebuild:
image: node:15
stage: prebuild
tags:
- builder
only:
changes:
- app/package.json
cache:
key:
files:
- app/package.json
paths:
- app/node_modules/
script:
- cd app
- npm install
build-ui:
image: node:15
stage: build
tags:
- builder
only:
changes:
- app/**/*
cache:
key:
files:
- app/package.json
policy: pull
paths:
- app/node_modules/
script:
- cd app
- npm run build
build-api:
image: dotnet/sdk:5.0
stage: build
only:
changes:
- api/**/*
tags:
- builder
script:
- cd api
- dotnet build
test:
image: dotnet/sdk:5.0
stage: test
only:
changes:
- api/**/*
tags:
- builder
script:
- cd api
- dotnet test
publish-ui:
image: node:15
stage: publish-ui
tags:
- builder
only:
refs:
- main
cache:
- key: "$CI_COMMIT_REF_SLUG-ui"
policy: push
paths:
- app/dist/
- key:
files:
- app/package.json
policy: pull
paths:
- app/node_modules/
script:
- cd app
- npm run build
publish-api:
image: dotnet/sdk:5.0
stage: publish-api
tags:
- builder
only:
- main
cache:
key: "$CI_COMMIT_REF_SLUG-api"
policy: push
paths:
- api/publish/
script:
- dotnet publish -c Release -o api/publish
image:
stage: image
tags:
- builder
only:
- main
cache:
- key: "$CI_COMMIT_REF_SLUG-ui"
policy: pull
paths:
- app/dist/
- key: "$CI_COMMIT_REF_SLUG-api"
policy: pull
paths:
- api/publish/
before_script:
- docker login -u "$LOCAL_REGISTRY_USER" -p "$LOCAL_REGISTRY_PASSWORD" $LOCAL_REGISTRY
script:
- docker build -f .gitlab/Dockerfile -t $LOCAL_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:latest .
- docker push $LOCAL_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME:latest
after_script:
- docker logout $LOCAL_REGISTRY
流水线中一共6个环节:prebuild、build、test、publish-ui、publish-api、image;其中build存在一个并行任务、其余都是串行
- prebuild
prebuild只针对vue项目,用于安装npm的包,只有当package.json存在修改的时候才会执行,否则就使用缓存 - build
每当有人push代码到服务器就会执行,检查代码是否能编译通过,如果不通过流水线失败,不能提mr - test
针对.net项目的单元测试,如果不通过流水线失败,不能提mr - publish-ui
发布前端项目,只有当main分支上有更改的时候发生 - publish-api
发布后端项目,并把前端项目放在wwwroot下,只有当main分支上有更改的时候发生 - image
打包镜像,只有当main分支上有更改的时候发生
引入缓存
我们知道Pipeline的每个Stage都是无状态的,运行完成后,产生的中间文件就会被丢弃掉,为了得到上一个Stage产生的文件,就需要将文件保存到缓存中,以便下一个Stage可以直接哪来使用。
缓存的几个属性:
- paths
指定要缓存的文件或者文件夹,只能是本仓库文件夹下的相对路径,所以生成的中间文件也只能放在当前仓库路径下的相对路径中,不能以放在/
开头的路径中(如:/app等); - key
每个缓存的键值,如果不指定就是default
,那么整个仓库就只有一份儿缓存(多个key就会有多个文件夹用来存放缓存文件),如果两个Stage中都有使用不同的缓存,那么下一个Stage会覆盖上一个Stage的缓存(一般情况下这样也没有任何问题,下一次Pipeline会先执行上一个Stage)。 - policy
缓存策略,分为pull、push、pull-push,
- pull表示当前Stage只会拉取缓存下来使用而不会对其进行改变;
- push表示当前Stage只会对缓存进行上传
- pull-push表示当前Stage会先拉下缓存,结束后会再次上传缓存
默认策略是pull-push
缓存还可以全局定义(全局定义缓存与stages同一级即可),具有继承特性,也可以禁用缓存。
// 此任务禁用缓存
job_name:
cache: {}
带来的问题
缓存解决了文件在不同Stage中的共享问题,同时也引入了一个并行任务问题。
问题描述
当一个仓库中同时有两个流水线、或者有并行Stage需要用到Cache的时候,Cache会有问题:要么找不到Cache、要么用的老的Cache。
出现问题的原因
通过研究发现runner的缓存文件存放在:/var/lib/docker/volumes/下以runner-{runnerid}-开头的文件夹中,
每个项目的缓存存放方式:runner-{runnerid}-projects-{projectid}-concurrent-{num}-cache-3c3f060a0374fc8bc39395164f415a70|c33bcaa1fd2c77edfc3893b41966cea8
以3c3f060a0374fc8bc39395164f415a70结尾的文件夹中存放的就是缓存文件,以c33bcaa1fd2c77edfc3893b41966cea8结尾的文件夹中存放的是代码源文件。
当任务出现并行的时候runner会创建多个Pipeline实例文件夹concurrent-0、concurrent-1...每个文件夹中保存当前并行实例的缓存数据,且每个job的并行id是不固定的;
如下两个并行Pipeline A、B,有5个Stage,并行执行会产生10个job:
A:1-2-3-4-5
B:1-2-3-4-5
- 第一种情况
假如3、4Stage需要用到缓存,那么可能会出现什么情况?
当A3在执行的时候缓存文件夹是concurrent-0、B3是concurrent-1,两个任务同时完成;
当A4在执行的时候缓存文件夹是concurrent-1、B4是concurrent-0,这样两个缓存就出现了交叉,出现严重问题。 - 第二种情况
假如3、4Stage需要用到缓存,且3是一个并行任务(pub-ui、pub-api)
那么就可能会同时出现4个并行实例,concurrent-0、concurrent-1、concurrent-2、concurrent-3;
假如4需要3的两个缓存,那么4要么永远都拿不到3中的其中一个缓存,要么拿到老的缓存。
这就造成了很验证的缓存错乱的问题。
如何解决此问题
- 从根本上解决
上分布式缓存(s3, gcs, azure.),这个没实践过官方反正这么说的; - 治标不治本
同一个项目中并行的job不要用缓存、用到缓存的Stage走串行、不要同时发生多个使用到缓存的Pipeline实例。
环境说明:
以上问题的对应的gitlab-runner版本为v13.10.0
参考链接:
官方cache文档地址
官方问题issue地址