容器環境持續整合優化,Drone CI 提速 500%

Allo發表於2019-05-06

前文介紹了容器環境下 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 CI 提速 500%

這個過程中到底慢在哪裡呢, 在 Drone 的構建過程中看到,容器構建的耗時佔了 90%以上,一方面 npm 需要下載安裝 3000 多個依賴,另一方面 webpack 的編譯也需要 30s 左右,如果網路再有不穩定,等待時間無疑會更長。

容器環境持續整合優化,Drone CI 提速 500%

另一個耗時的元凶也很明顯,由於引入了 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%以上。

容器環境持續整合優化,Drone CI 提速 500%

通過 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 等環節都可以通過條件省略,結果如下。

容器環境持續整合優化,Drone CI 提速 500%

如此優化後 release 環節的耗時縮短到 1 分鐘以內,看看最終成果,從程式碼提交到釋出完成,總耗時不到 5 分鐘,是比較友好的。

容器環境持續整合優化,Drone CI 提速 500%

相關文章