定義一個流水線
在我們使用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地址