加3行代碼減少80%構(gòu)建時間

背景
最近接手的BI項目在Jenkins的構(gòu)建機上構(gòu)建耗時比較久,日常構(gòu)建耗時都在 20min 以上,即使改動一行代碼也要構(gòu)建這么久。構(gòu)建耗時截圖如下:



構(gòu)建耗時較長導(dǎo)致日常測試和正式發(fā)版都會浪費很多時間等待,對研發(fā)流程影響較大(主要是我忍不了)。因此需要對構(gòu)建速度進行優(yōu)化。

優(yōu)化思路分析
要優(yōu)化項目的構(gòu)建速度,得先了解構(gòu)建流程:

開發(fā)人員推送代碼到 Gitlab,觸發(fā) Gitlab 服務(wù)器的 Push Events
Push Events被觸發(fā)后,會調(diào)用提前配置好的 Jenkins webhooks
Jenkins webhooks被調(diào)用后,會執(zhí)行對應(yīng)項目的構(gòu)建任務(wù)
構(gòu)建任務(wù)開始后先拉取項目源碼到構(gòu)建機,再使用docker build構(gòu)建鏡像
docker 構(gòu)建鏡像分為兩個階段,先使用npm scripts構(gòu)建前端項目,然后把構(gòu)建產(chǎn)物拷貝到nginx基礎(chǔ)鏡像
在這個流程中,可以優(yōu)化的環(huán)節(jié)只有構(gòu)建docker鏡像這一步,其他環(huán)節(jié)的耗時基本可以忽略不計。而在不大改項目的情況下能起到明顯提速效果的方案是:緩存策略。構(gòu)建docker鏡像時可以用到的緩存包括兩類:docker層緩存和應(yīng)用層緩存。

docker層緩存是指docker build所產(chǎn)生的可重用鏡像層,只要Dockerfile中的命令及相關(guān)的源文件未改變,就能直接使用這些鏡像緩存。這種緩存策略在代碼不改變的情況下效果很好,構(gòu)建耗時甚至可以控制在 10 秒內(nèi)。而對于日常開發(fā)情況下,代碼頻繁變化,如果應(yīng)用本身構(gòu)建時間又很長,則需要使用應(yīng)用層緩存。(上一篇文章《docker build 緩存失效分析》中有 docker 層緩存相關(guān)介紹,也可以看看官方文檔、中文文檔,本文不再贅述)

應(yīng)用層緩存是指應(yīng)用構(gòu)建所產(chǎn)生的中間產(chǎn)物,這些中間產(chǎn)物主要是node_modules目錄中的物理文件,其中包括npm install下載的依賴包和npm run build產(chǎn)生的.cache目錄文件。而docker build每次都會初始化全新的環(huán)境用于構(gòu)建,新環(huán)境中不存在node_modules目錄,因此每次都是重新寫入而無法復(fù)用,得想辦法復(fù)用該目錄下的文件;另外npm run build需要開啟緩存功能,才會輸出緩存文件到node_modules/.cache目錄。

綜上,優(yōu)化思路主要是兩點:1、開啟應(yīng)用層構(gòu)建緩存(如webpack cache);2、持久化node_modules目錄,確保每次npm install和npm run build都能復(fù)用該目錄下的文件。

開啟應(yīng)用層構(gòu)建緩存
項目使用的技術(shù)是React,構(gòu)建主要依靠react-scripts@4.0.3,底層實際調(diào)用的是webpack@4.44.2,應(yīng)用構(gòu)建緩存主要來自webpack。webpack需要手工開啟緩存功能(官方文檔傳送門),配置cache屬性為true即可。

實際操作只有 1 步, 找到webpack.config.js設(shè)置cache:true,代碼如下:

module.exports = {
  //...
  cache: true
};
復(fù)制代碼
本地首次npm run build構(gòu)建,無緩存的情況下,耗時 13min 左右。



啟用緩存后在本地進行二次構(gòu)建,有緩存的情況下,無論是否修改源碼構(gòu)建耗時均為 4min 左右,比優(yōu)化前的 13min 有明顯提升。 構(gòu)建耗時截圖如下:



實際上,webpack@4的緩存只在watch和development模式下生效,在上述構(gòu)建測試中其實不起作用。 實測刪除wepack中的cache:true配置,或者配置為cache:false,二次構(gòu)建時間也是 4min 左右。

之所以構(gòu)建速度提升了那么多,是因為react-scripts的webpack配置中開啟了babel-loader和eslint-webpack-plugin的緩存功能,另外terser-webpack-plugin配置也默認開啟了緩存功能。從緩存目錄node_modules/.cache中也能看到它們的緩存文件。



所以,這一步其實啥也不用做,如果想進一步提速可以升級到webpack@5。

持久化node_modules目錄
想在docker build環(huán)境中持久化node_modules需要使用到BuildKit的mount功能,該功能有幾個前置條件:

docker 版本必須高于 18.09
BuildKit需要手工啟用,可在docker build命令前添加環(huán)境變量DOCKER_BUILDKIT=1啟用
如果前兩個條件不滿足,則需要具備Jenkins和構(gòu)建機的讀寫權(quán)限,以調(diào)整構(gòu)建環(huán)境參數(shù)
修改Dockerfile,使用RUN --mount=type=cache運行npm install和npm run build指令(--mount=type=cache說明文檔傳送門)
開啟BuildKit還有其他特性,比如輸出日志更友好,基本每一步都會輸出耗時,就這一條,值了!

實際操作分為 2 步:1、修改Jenkins配置,在docker build命令前加上環(huán)境變量。修改后鏡像構(gòu)建命令長這樣:

 DOCKER_BUILDKIT=1 docker build .
復(fù)制代碼
2、修改Dockerfile,將RUN npm install和RUN npm run build指令改為RUN --mount=type=cache npm xxx。修改后Dockerfile長這樣:

FROM node:alpine as builder

WORKDIR /app

COPY package.json /app/

RUN --mount=type=cache,target=/app/node_modules,id=my_app_npm_module,sharing=locked \
    --mount=type=cache,target=/root/.npm,id=npm_cache \
    npm i --registry=https://registry.npm.taobao.org

COPY src /app/src

RUN --mount=type=cache,target=/app/node_modules,id=my_app_npm_module,sharing=locked \
    npm run build
復(fù)制代碼
文檔說由于 BuildKit 為實驗特性,需要在 Dockerfile 文件開頭加上如下代碼:# syntax = docker/dockerfile:experimental。在Docker 20.10環(huán)境下,加了上述代碼反而構(gòu)建報錯,原因是加載外網(wǎng)資源失敗,刪除后構(gòu)建成功。這不就是玄學(xué)嗎???

優(yōu)化結(jié)果
在配置好緩存策略后,模擬日常開發(fā)修改項目代碼觸發(fā)自動構(gòu)建流程,構(gòu)建耗時從 20min+下降到 4min+,總體耗時減少 80%。整個優(yōu)化過程修改了Jenkins的一行配置,另外在Dockerfile中添加了3行代碼,改動很少但效果很不錯。





作者:Whilconn

原文:https://juejin.cn/post/7135756687134162980





作者:Whilconn


歡迎關(guān)注微信公眾號 :深圳灣碼農(nóng)