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,
  1. pull表示当前Stage只会拉取缓存下来使用而不会对其进行改变;
  2. push表示当前Stage只会对缓存进行上传
  3. 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
  1. 第一种情况
    假如3、4Stage需要用到缓存,那么可能会出现什么情况?
    当A3在执行的时候缓存文件夹是concurrent-0、B3是concurrent-1,两个任务同时完成;
    当A4在执行的时候缓存文件夹是concurrent-1、B4是concurrent-0,这样两个缓存就出现了交叉,出现严重问题。
  2. 第二种情况
    假如3、4Stage需要用到缓存,且3是一个并行任务(pub-ui、pub-api)
    那么就可能会同时出现4个并行实例,concurrent-0、concurrent-1、concurrent-2、concurrent-3;
    假如4需要3的两个缓存,那么4要么永远都拿不到3中的其中一个缓存,要么拿到老的缓存。
    这就造成了很验证的缓存错乱的问题。

如何解决此问题

  1. 从根本上解决
    上分布式缓存(s3, gcs, azure.),这个没实践过官方反正这么说的;
  2. 治标不治本
    同一个项目中并行的job不要用缓存、用到缓存的Stage走串行、不要同时发生多个使用到缓存的Pipeline实例。

环境说明:
以上问题的对应的gitlab-runner版本为v13.10.0
参考链接:
官方cache文档地址
官方问题issue地址

posted @ 2021-06-09 01:00  whyfate  阅读(1472)  评论(0编辑  收藏  举报