前文介紹了容器環境下 Drone + semantic release 實現的語義化持續整合 Workflow,為了方便演示,流程僅給出了工作流中最重要的幾個環節,實際用起來可能會發現不少值得優化的地方。
因此本次在這個工作流的基礎上,介紹一些容器環境下 CI 的優化及提速方法,方法本身不限定一定要使用 Drone,使用同樣的思路完全可以套用到其他的 CI 工具中。
優化前專案概況
以一個生產環境的實際專案為例,專案的主要結構如下
├── Dockerfile
├── dist/
├── node_modules/
├── package.json
└── src/
複製程式碼
這是一個比較常見的基於 React 的前端專案,用 npm list | wc -l
可以看到有 3952 個依賴,專案會通過 webpack 打包到 dist
目錄下,打包命令被封裝成 npm run build
。最終 dist
目錄通過 Dockerfile
被打包到 Nginx 的 Docker 映象內, 生產環境直接執行打包後的映象即可。
Dockerfile 是這樣編寫的
FROM node:10 as build
WORKDIR /app
COPY . /app
RUN npm install
RUN npm run build
FROM nginx:1.15-alpine
COPY --from=build /app/dist /usr/share/nginx/html
複製程式碼
使用了 Docker 的多階段構建功能,即 npm 的安裝,編譯作為第 1 個階段,編譯完成後僅將編譯的結果 dist
資料夾複製出來,其餘未複製的檔案丟棄,這樣打包後的映象僅為 23.2MB,更利於部署。
流程瓶頸分析
釋出流程直接套用前文介紹的 Gitflow + semantic release 工作流。可以看到此時的一次釋出是比較慢的,push 到 master 構建 staging 映象用時 9:18,semantic release 打上 Tag 構建 production 映象用時 6:24。
這個過程中到底慢在哪裡呢, 在 Drone 的構建過程中看到,容器構建的耗時佔了 90%以上,一方面 npm 需要下載安裝 3000 多個依賴,另一方面 webpack 的編譯也需要 30s 左右,如果網路再有不穩定,等待時間無疑會更長。
另一個耗時的元凶也很明顯,由於引入了 semantic release, push master 和 release 兩個動作會觸發 2 次 CI,每次 CI 都進行了 Docker 映象的構建,但其實如果沒有異常發生,兩個 Docker 映象對應的其實是同一份程式碼,應當是完全一致的,即 release 時的映象構建所花費的時間是浪費的。
其他當然還有應用層面的優化,比如可以用 yarn 替代 npm,使用更快的源,去除不必要的依賴等等,但這些並不在本文的討論範圍內,就略過不提。
引入快取減少重複下載
每次構建都要下載 3000 多個依賴,那麼最容易想到的當然是將這些依賴快取起來,但是在這個專案中,npm 下載/編譯都發生在容器構建環節,這是比較難引入快取的。因此首先要做的,是將下載/編譯過程從容器轉移到 CI,通過 CI 完成下載/編譯,再將結果複製到容器映象內。
在下載/編譯轉移到 CI 的基礎上,可以直接使用 Drone 提供的快取外掛,目前根據不同檔案系統,Drone 可選的快取外掛有
這裡以 Volume Cache 為例,.drone.yml
如下。這裡的語法對應 Drone-v1.0 以上版本,可能與官方部分舊文件有出入。
steps:
- name: restore-cache
image: drillster/drone-volume-cache
settings:
restore: true
mount:
- ./.npm-cache
- ./node_modules
volumes:
- name: cache
path: /cache
- name: npm-install
image: node:10
commands:
- npm config set cache ./.npm-cache --global
- npm install
- name: build-dist
image: node:10
commands:
- npm run build
- name: rebuild-cache
image: drillster/drone-volume-cache
settings:
rebuild: true
mount:
- ./.npm-cache
- ./node_modules
volumes:
- name: cache
path: /cache
volumes:
- name: cache
host:
path: /tmp/cache
複製程式碼
Volume Cache 外掛使用很簡單,首先需要宣告一個 Volume,對應主機的一個資料夾,這裡使用的是/tmp/cache
。Volume Cache 外掛的引數中,mount
列出需要快取的資料夾,restore: true
會將檔案從主機複製到容器,因此放在 pipeline 的開頭,rebuild: true
則反之,放在 pipeline 最後。
另外注意使用 Volume 需要在 Drone 中將 Repo 設定為 Trusted。
而此時的 Dockerfile 就只剩下檔案複製的部分了
FROM nginx:1.15-alpine
COPY ./dist /usr/share/nginx/html
複製程式碼
在增加了快取後,構建的時長大幅下降到 2:38,整體耗時下降了 50%以上。
通過 Docker Tag 省略重複的構建
在上文的基礎上,不難想到 push master 和 release 造成的重複構建,是否也可以同樣通過快取去除。這當然在理論上也是可行的,但是由於快取並不穩定,因此需要更為通用的方法。
在 semantic release 的流程中,push master 和 release 的唯一區別就是 release 增加了一個 git tag。而 git tag 本質上只是對一個特定 commit 的引用,並不會改變 commit 記錄,因此 push master 和 release 兩次觸發的 CI 中,最後一次 commit 是相同的,即 DRONE_COMMIT_SHA
不會改變。
基於這一點,我們可以在 push master 的構建中,將DRONE_COMMIT_SHA
作為 Docker 映象額外的 Tag,在 release 環節,只要給有 DRONE_COMMIT_SHA
Tag 的映象再打上最終的版本號 Tag 即可,並不需要在 release 環節從頭構建映象。
這個過程對應 .drone.yml
如下
- name: push-docker-staging
image: plugins/docker
settings:
repo: allovince/xxx
username: allovince
password:
from_secret: DOCKER_PASSWORD
tag:
- staging
- sha_${DRONE_COMMIT_SHA}
when:
branch: master
event: push
- name: semantic-release
image: gtramontina/semantic-release:15.13.3
environment:
GITHUB_TOKEN:
from_secret: GITHUB_TOKEN
entrypoint:
- semantic-release
when:
branch: master
event: push
- name: push-docker-production
image: plugins/docker
environment:
DOCKER_PASSWORD:
from_secret: DOCKER_PASSWORD
commands:
- docker -v
- nohup dockerd &
- docker login -u allovince -p $${DOCKER_PASSWORD}
- docker pull allovince/xxx:sha_$${DRONE_COMMIT_SHA}
- docker tag allovince/xxx:sha_$${DRONE_COMMIT_SHA} allovince/xxx:$${DRONE_TAG}
- docker push allovince/xxx:$${DRONE_TAG}
when:
event: tag
privileged: true
複製程式碼
假設最後一次 commit 的 hash 是 c0558777
, release 版本是 v1.0.9, push master 後, 映象將打上
- staging
- sha_c0558777
兩個 Tag,在 release 後,映象將再增加一個 v1.0.9
的 Tag。
需要注意的是為映象打 Tag 使用到了 Docker-in-Docker,需要 privileged flag,即privileged: true
同時 docker tag 等命令依賴 docker daemon 的啟動,否則會報錯
Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?
一種方式是掛載主機的 daemon /var/run/docker.sock
,另一種方式是在容器內啟動 docker daemon,我這裡使用的是後者,對應 nohup dockerd &
,而在 release 階段,由於任務僅僅是為 docker 映象額外增加一個 tag,以及通知生產環境釋出,因此上文中的 cache 等環節都可以通過條件省略,結果如下。
如此優化後 release 環節的耗時縮短到 1 分鐘以內,看看最終成果,從程式碼提交到釋出完成,總耗時不到 5 分鐘,是比較友好的。