本文將以 Node 程式展示如何最佳化 Docker 映象(最佳化思想是通用的,不分程式),主要解決映象大小過大、CI/CD 構建映象速度,本文演示如何一步步最佳化 Dockerfile 檔案,絕對的乾貨,建議先贊再看,看完不是乾貨再取消贊也不是不行。最佳化的結果如下:
文章轉載:樂位元組
大小從 1.06G 到 73.4M
構建速度從 29.6 秒到 1.3 秒
(對比的是第二次構建的速度)
Node 專案
簡單寫了一個自己用的 wechat-bot ,接下來就以這個專案演示怎麼去最佳化 Docker 映象
以下是我沒有仔細研究 Docker 剛開始寫的 Dockerfile 檔案
FROM node:14。17。3# 設定環境變數ENV NODE_ENV=productionENV APP_PATH=/node/app# 設定工作目錄WORKDIR $APP_PATH# 把當前目錄下的所有檔案複製到映象的工作目錄下 。dockerignore 指定的檔案不會複製COPY 。 $APP_PATH# 安裝依賴RUN yarn# 暴露埠EXPOSE 4300CMD yarn start複製程式碼
build 之後,如下圖,我這個簡單的 Node 程式映象竟然有
1G
多,接下來我們將逐步去最佳化減少這個大小
最佳化前言
在最佳化之前,有些東西我們必須瞭解,解決問題的第一步就是先找出導致問題的原因
Dockerfile 檔案,其內包含了一條條的指令,每一條指令構建一層,因此每一條指令的內容,就是描述該層如何構建
Docker 映象並非只是一個檔案,而是由一堆檔案組成,最主要的檔案是
層
(Layers) 映象構建時,會一層層構建,前一層是後一層的基礎 每一層構建完就不會再發生改變,後一層上的任何改變只發生在自己這一層。比如,刪除前一層檔案的操作,實際不是真的刪除前一層的檔案,而是僅在當前層標記為該檔案已刪除。在最終容器執行的時候,雖然不會看到這個檔案,但是實際上該檔案會一直跟隨映象 映象層將會被快取和複用(這也是從第二次開始構建映象時,速度會快的原因,最佳化映象構建速度的原理也是利用快取原理來做) 當 Dockerfile 的指令修改了,操作的檔案變化了,或者構建映象時指定的變數不同了,對應的映象層快取就會失效
docker build 的快取機制,docker 是怎麼知道檔案變化的呢?
Docker 採取的策略是:獲取 Dockerfile 下內容(包括檔案的部分 inode 資訊),計算出一個唯一的 hash 值,若 hash 值未發生變化,則可以認為檔案內容沒有發生變化,可以使用快取機制,反之亦然。 某一層的映象快取失效之後,它之後的映象層快取都會失效 映象的每一層只記錄檔案變更,在容器啟動時,Docker 會將映象的各個層進行計算,最後生成一個檔案系統 當我知道這點時,我恍然大悟,我們使用的作業系統,比如安卓、ios、win、mac 等,其實就是一個檔案系統,我們的軟體介面互動等,其實就是在讀寫檔案,我們網頁寫個彈框,操作 dom,就是在讀寫本地檔案或者是讀寫記憶體裡的資料,個人的一些見解不知道對不對,本人非科班出身的前端 coder 參考資料:docker 映象分層原理
ok,我們已經知道映象是由多層檔案系統組成,想要最佳化它的大小,就需要去減少層數、每一層儘量只包含該層需要的東西,任何額外的東西應該在該層構建結束前清理掉,下面開始正文
最佳化 Dockerfile
最佳化第一層 FROM node:14。17。3
方案一:使用 node 的 Alpine 版本
這也是絕多數人知道的最佳化映象手段,Alpine 是一個很小的 Linux 發行版,只要選擇 Node 的 Alpine 版本,就會有很大改進,我們把這一句改成指令改成 FROM node:14。17。4-alpine (可以去 dockerhub 檢視 node 有哪些版本標籤),build 後鏡像大小如下圖,瞬間
從 1.06G 降到 238M
,可以說是效果顯著
還可以使用其它的基礎小映象,比如 mhart/alpine-node,這個還能再小,改成 FROM mhart/alpine-node:14。17。3 再試試,可以看到又小了
5M
,雖然不多,但是秉著能壓榨一點是一點的“老闆原則”,積少成多,極致壓榨
方案二:使用純淨 Alpine 映象手動裝 Node
既然 Alpine 是最小的 Linux,那我們試下用純淨的 Alpine 映象,自己再裝 Node 試試
FROM alpine:latest# 使用 apk 命令安裝 nodejs 和 yarn,如果使用 npm 啟動,就不需要裝 yarnRUN apk add ——no-cache ——update nodejs=14。17。4-r0 yarn=1。22。10-r0# 。。。 後面的步驟不變複製程式碼
build 後看下圖,只有
174M
了,又小了不少
結論就是不嫌麻煩追求極致就用方案二,
從 1.06G 減少到 174M
減少層數、不經常變動的層提到前面去
ENV 指令是可以一次性設定多個環境變數,能一次指令執行完,就不用兩次,多一個指令就多一層
EXPOSE 指令是暴露埠,其實也可以不用寫這個指令,在啟動容器的時候自己對映埠,如果寫了這個指令的話,因為埠不經常變,所以把這個指令提前,寫上這個指令有兩個好處: 幫助映象使用者理解這個映象服務的守護埠,以方便配置對映 在執行時使用隨機埠對映時,也就是 docker run -P 時,會自動隨機對映 EXPOSE 的埠 至於寫還是不寫,看個人吧,我個人一般不寫,因為我在專案啟動命令會指定專案埠,啟動容器的時候映射出來就好,這樣我就要維護一個地方,Dockerfile 也寫了的話,專案埠變了,這裡也要修改,多了點維護成本,當然也有辦法讓兩邊埠變數取自配置檔案,只要改配置檔案即可
下面是改寫後的 Dockerfile
FROM alpine:latest# 使用 apk 命令安裝 nodejs 和 yarn,如果使用 npm 啟動,就不需要裝 yarnRUN apk add ——no-cache ——update nodejs=14。17。4-r0 yarn=1。22。10-r0# 暴露埠EXPOSE 4300# 設定環境變數ENV NODE_ENV=production \ APP_PATH=/node/app# 設定工作目錄WORKDIR $APP_PATH# 把當前目錄下的所有檔案複製到映象的工作目錄下 。dockerignore 指定的檔案不會複製COPY 。 $APP_PATH# 安裝依賴RUN yarn# 啟動命令CMD yarn start複製程式碼
這一步的最佳化,無論從映象大小還是構建映象速度都看不到明顯的差別,因為改動的層內容少(體現不出來),但是可以檢視到映象的層是變少了的,可以自行試試檢視映象的層試試
減少映象層數是“好老闆”的傳統優良習慣,不讓“員工”浪費資源
package。json 提前提高編譯速度
從下圖可以看到每次我們 build 的時候最耗時的就是在執行 yarn 命令裝依賴的時候,大部分時候我們只是改程式碼,依賴不變,這時候如果可以讓這一步快取起來,依賴沒有變化的時候,就不需要重新裝依賴,就可以大大改進編譯速度
前面我們說了映象構建時,是一層層構建,前一層是後一層的基礎,既然是這樣的話,我們就把 package。json 檔案單獨提前複製到映象,然後下一步裝依賴,執行命令裝依賴這層的前一層是複製 package。json 檔案,因為安裝依賴命令不會變化,所以只要 package。json 檔案沒變化,就不會重新執行 yarn 安裝依賴,它會複用之前安裝好的依賴,原理講清楚了,下面我們看效果
改變後的 Dockerfile 檔案
FROM alpine:latest# 使用 apk 命令安裝 nodejs 和 yarn,如果使用 npm 啟動,就不需要裝 yarnRUN apk add ——no-cache ——update nodejs=14。17。4-r0 yarn=1。22。10-r0# 暴露埠EXPOSE 4300# 設定環境變數ENV NODE_ENV=production \ APP_PATH=/node/app# 設定工作目錄WORKDIR $APP_PATH# 複製 package。json 到工作跟目錄下COPY package。json 。# 安裝依賴RUN yarn# 把當前目錄下的所有檔案複製到映象的工作目錄下 。dockerignore 指定的檔案不會複製COPY 。 。# 啟動命令CMD yarn start複製程式碼
build 看下圖,編譯時間從 29。6s 到 1。3s,使用了快取的層前面會有個 CACHED 字眼,仔細看下圖可以看到
充分利用 docker 快取特性是最佳化構建速度的利器
使用多階段構建再次壓榨映象大小
多階段構建這裡不多說了,不瞭解的可以先搜相關資料瞭解
因為我們執行 node 程式是隻需要生產的依賴和最終 node 可以執行的檔案,就是說我們執行專案只需要 package。js 檔案裡 dependencies 裡的依賴,devDependencies 依賴只是編譯階段用的,比如 eslint 等這些工具在專案執行時是用不到的,再比如我們專案是用 typescript 寫的,node 是不能直接執行 ts 檔案,ts 檔案需要編譯成 js 檔案,執行專案我們只需要編譯後的檔案和 dependencies 裡的依賴就可以執行,也就是說最終映象只需要我們需要的東西,任何其他東西都可以刪掉,下面我們使用多階段改寫 Dockerfile
# 構建基礎映象 FROM alpine:3。14 AS base # 設定環境變數 ENV NODE_ENV=production \ APP_PATH=/node/app # 設定工作目錄 WORKDIR $APP_PATH # 安裝 nodejs 和 yarn RUN apk add ——no-cache ——update nodejs=14。17。4-r0 yarn=1。22。10-r0# 使用基礎映象 裝依賴階段 FROM base AS install # 複製 package。json 到工作跟目錄下 COPY package。json 。/ # 安裝依賴 RUN yarn# 最終階段,也就是輸出的映象是這個階段構建的,前面的階段都是為這個階段做鋪墊 FROM base # 複製 裝依賴階段 生成的 node_modules 資料夾到工作目錄下 COPY ——from=install $APP_PATH/node_modules 。/node_modules # 將當前目錄下的所有檔案(除了。dockerignore排除的路徑),都複製進入映象的工作目錄下 COPY 。 。 # 啟動 CMD yarn start複製程式碼
細心的朋友會發現我這裡有指定 alpine 版本,而上面都是用的 latest 版本,因為就在剛剛發現有個坑需要注意下,就是我們選擇 alpine 版本的時候,最好不要選擇 latest 版本,因為後面要裝的軟體版本可能會在 alpine 的 latest 版本沒有對應軟體的版本號,就會安裝錯誤,我剛剛就翻車了,點選檢視 alpine 版本下的包資訊
build 後,我們看看映象大小,上次的是 174M 再次降到 73。4M,極致壓榨。映象:”放過我把,我真的沒有了“
講解:
我把這個構建分成了三個階段:
第一階段:構建基礎映象 安裝依賴、編譯、執行等等階段,就是所有階段共用的東西都在第一階段封到一個基礎映象裡供其它階段使用,比如設定環境變數、設定工作目錄、安裝 nodejs、yarn 等等
第二階段:裝依賴階段 在這個階段,裝依賴,如果專案需要編譯,可以在這個階段裝依賴編譯好 這裡在說下裝依賴的小細節,就是執行 yarn ——production 加個 production 引數或者環境變數 NODE_ENV 為 production,yarn 將不會安裝 devDependencies 中列出的任何軟體包,點我檢視官方文件說明,因為我設定了環境變數所以就沒加這個引數
第三階段:最終使用映象 複製第二階段安裝的好的依賴資料夾,然後在複製程式碼檔案到工作目錄,執行啟動命令,第二階段裝依賴多出的一些垃圾我們不需要,我們就只複製我們要用的東西,大大減少映象的大小 如果專案需要編譯,在複製編譯後的資料夾,不需要複製編譯前的程式碼,有編譯後的程式碼和依賴就可以跑起專案
多階段構建,最後生成的映象只能是最後一個階段的結果,但是,能夠將前置階段中的檔案複製到後邊的階段中,這就是多階段構建的最大意義。
最終最佳化成果:
大小從 1.06G 到 73.4M
構建速度從 29.6 秒到 1.3 秒
(對比的是第二次構建的速度)
至此,壓榨映象手段就完了,如果各位老闆還有壓榨手段可以分享分享
映象內心獨白:”你禮貌嗎?還來“
github 的 actions 構建映象問題
github 提供的 actions,每次都是一個乾淨的例項,什麼意思,就是每次執行,都是乾淨的機器,這會導致一個問題,會導致 docker 沒法使用快取,那有沒有解決辦法呢,我想到了兩種解決辦法:
docker 官方提供的 action 快取方案 我用的是 Github cache 方案
自託管 actions 執行機器 相當於 gitlab 的 runner 一樣,自己提供執行器,自己提供的就不會每次都是乾淨的機器,詳情看 actions 官方文件
先構建一個已經安裝好依賴包的映象,然後基於此映象再次構建,相當於多階段構建,把前兩個階段構建的映象產物推送到映象倉庫,再以這個映象為基礎去構建後續部分。藉助映象倉庫儲存基礎映象從而達到快取的效果(此方案來源於評論裡的大佬) # 以這個映象為基礎去構建,這個映象是已經裝好專案依賴的映象並推送到映象倉庫裡,這裡從映象倉庫拉下來 FROM project-base-image:latest COPY 。 。 CMD yarn start 複製程式碼
參考資料:
在 GitHub Actions 上使用 Docker 層快取構建映象
最後
專案倉庫地址 wechat-bot
文章轉載:樂位元組
文章有錯誤的地方歡迎指正,避免誤人子弟