
阿里妹導讀
一、概述
最近在開發python應用程式,在部署應用的時候發現構建映象過程十分緩慢,極大影響開發效率。既然遇到了問題就不要逃避,而應該嘗試解決一下。本文主要記錄了自己透過查閱相關資料,一步步排查問題,最後透過最佳化Docerfile檔案將docker映象構建從十幾分鍾降低到1分鐘左右,效率提高了10倍左右。
本文透過如下幾個部分進行介紹:
-
現狀:簡單介紹一下未最佳化前的情況; -
最佳化效果:簡單介紹最佳化後的情況; -
分析過程:介紹如何分析映象構建存在的問題; -
最佳化過程:介紹如何透過最佳化Dockerfile提高映象構建效率; -
最佳化總結:最後總結映象構建的幾個最佳化方法;
透過本文的學習,你將有如下收穫:
1.瞭解映象構建最佳化的過程。
2.瞭解一些常用的映象構建最佳化的技巧。
二、最佳化前效果

未最佳化前可以看到映象構建耗時16分鐘,構建完成後映象大小約8G,使用的Dockerfile檔案如下:
FROM reg.docker.alibaba-inc.com/aci-images/python-service:3.8.0-63928922
# init folder
RUN mkdir -p /home/admin/logs && mkdir -p /home/admin/bin && mkdir -p /home/admin/conf && mkdir -p /home/admin/nginx && mkdir -p /home/admin/.maxhub/env_helper_util/zeta-local-env
# install zeta
RUN pushd /home/admin/.maxhub/env_helper_util/zeta-local-env && \
wget https://artifacts.antgroup-inc.cn/artifact/repositories/softwares-common/antcode/zeta/0.7.9/zeta-linux-amd64-0.7.9.sh -O zeta-release.sh && \
chmod +x zeta-release.sh && \
./zeta-release.sh --prefix=/usr/local && \
popd
# init env and install software
COPY conf/docker/build.yaml /root/
RUN python3.10 -m pip install -U antimgbuilder -i https://pypi.antfin-inc.com/simple && \
python3.10 -m antimgbuilder --config-file /root/build.yaml
# copy source file
# COPY --chown=admin:admin mydemo /home/admin/release/mydemo
COPY --chown=admin:admin aml_core /home/admin/release/aml_core
COPY --chown=admin:admin backend /home/admin/release/backend
# install requirements.txt
COPY --chown=admin:admin requirements.txt /home/admin/release/
RUN python3.10 -m venv /home/admin/run && \
. /home/admin/run/bin/activate && \
python3.10 -m pip install -i https://pypi.antfin-inc.com/simple-remote --upgrade pip &&\
python3.10 -m pip install -i https://pypi.antfin-inc.com/simple -r /home/admin/release/requirements.txt
# copy scripts
COPY --chown=admin:admin conf/docker/scripts/admin /home/admin
COPY --chown=admin:admin conf/nginx /home/admin/nginx
# 最後確保admin目錄下檔案許可權
RUN chown admin:admin -R /home/admin
RUN chmod a+xw /home/admin/bin/fetch_ollama.sh /tmp
三、最佳化後效果

最佳化後可以看到映象構建時間為1分鐘左右,映象大小約5G,使用的Dockerfile如下:
# 第一階段:下載依賴
FROM reg.docker.alibaba-inc.com/antfin-sqa/amlregservermodel-dev:20241016125401_b0296dab as builder
# install requirements.txt
COPY --chown=admin:admin requirements.txt /home/admin/release/
RUN python3.10 -m venv /home/admin/run && \
. /home/admin/run/bin/activate && \
python3.10 -m pip install -i https://pypi.antfin-inc.com/simple-remote --upgrade pip &&\
python3.10 -m pip install -i https://pypi.antfin-inc.com/simple -r /home/admin/release/requirements.txt --no-cache-dir
# 第二階段:構建應用程式映象
FROM reg.docker.alibaba-inc.com/aci-images/python-service:3.8.0-63928922
# init folder
RUN mkdir -p /home/admin/logs && mkdir -p /home/admin/bin && mkdir -p /home/admin/conf && mkdir -p /home/admin/nginx && mkdir -p /home/admin/.maxhub/env_helper_util/zeta-local-env
# install zeta
RUN pushd /home/admin/.maxhub/env_helper_util/zeta-local-env && \
wget https://artifacts.antgroup-inc.cn/artifact/repositories/softwares-common/antcode/zeta/0.7.9/zeta-linux-amd64-0.7.9.sh -O zeta-release.sh && \
chmod +x zeta-release.sh && \
./zeta-release.sh --prefix=/usr/local && \
rm -f zeta-release.sh && \
popd
# install virtualenv and uvicorn
RUN python3.10 -m venv /home/admin/run && \
. /home/admin/run/bin/activate && \
python3.10 -m pip install -i https://pypi.antfin-inc.com/simple-remote --upgrade pip &&\
python3.10 -m pip install -i https://pypi.antfin-inc.com/simple uvicorn --no-cache-dir
# init env and install software
COPY conf/docker/build.yaml /root/
RUN python3.10 -m pip install -U antimgbuilder -i https://pypi.antfin-inc.com/simple && \
python3.10 -m antimgbuilder --config-file /root/build.yaml
# copy scripts
COPY --chown=admin:admin conf/docker/scripts/admin /home/admin
COPY --chown=admin:admin conf/nginx /home/admin/nginx
COPY --chown=admin:admin aml_core /home/admin/release/aml_core
RUN chmod a+xw /home/admin/bin/fetch_ollama.sh /tmp
# 從第一階段複製下載的依賴到第二階段
COPY --from=builder /home/admin/run/lib/python3.10/site-packages/ /home/admin/run/lib/python3.10/site-packages/
# copy source file
COPY --chown=admin:admin backend /home/admin/release/backend
四、分析過程
4.1. 映象構建耗時分析
分析最佳化前的映象構建構成,找到最耗時的階段,進入映象構建任務詳情頁:

點選 image-build-3 找到耗時最長的指令:

可以看到在指令:
COPY –chown=admin:admin conf/docker/scripts/admin /home/admin
的前一步耗時達到了 10 分鐘左右,對照著 Dockerfile 檔案可以看到,是下面下載依賴比較耗時。

由於構建出來的映象比較大,導致推送映象耗時約:4分鐘

映象構建耗時分析總結:
1.從構建的日誌中可以看到是下載依賴比較耗時約:10 分鐘。
2.並且前面的指令快取失效, 則隨後指令構建的映象都不再使用快取導致耗時增加。
3.構建出來的映象比較大,導致推送映象耗時約:4分鐘。
4.2. 映象構建體積較大分析
從前面的Dockfile檔案中可以看到,使用的基礎映象是:
reg.docker.alibaba-inc.com/aci-images/python-service:3.8.0-63928922,拉取該映象,檢視基礎映象的體積:

可以看到該映象的大小是:2.33G
我們進入Docker容器,檢視下載依賴的大小以及快取的大小,下載依賴的快取目錄一般是 /root/.cache/pip:

映象構建體積較大分析總結:
1.基礎映象體積較大:2.33G。
2.安裝的依賴較大,並且下載依賴時預設開啟了快取,導致佔用更多的記憶體空間約:3.1G(包括下載的依賴和快取佔用:2.6G + 729M )。
為什麼使用 pip install 安裝依賴時沒有新增 –no-cache-dir 引數會導致佔用的記憶體更多?如果在使用 pip install 安裝依賴時沒有新增 –no-cache-dir 引數,會導致快取目錄中的檔案不斷增加,佔用更多的記憶體空間。每次使用 pip install 安裝依賴時,pip 會預設將下載的依賴包儲存在快取目錄 /root/.cache/pip 中,如果沒有新增 –no-cache-dir 引數,pip 會在安裝依賴時從快取目錄中檢查已有的依賴包,如果有相同的包就會直接使用快取中的包,而不是重新下載。因此,隨著時間的推移,快取目錄中會存放越來越多的依賴包,佔用更多的記憶體空間。為了避免佔用更多的記憶體空間,可以在使用 pip install 安裝依賴時新增 –no-cache-dir 引數,這樣將停用快取,使得每次安裝依賴都會重新下載依賴包,從而避免佔用更多的記憶體空間。
4.3. 使用 docker history 分析
接下來我們使用 docker history 進行分析。
docker history :用於檢視 Docker 映象的構建歷史,顯示每一層的提交資訊,包括映象 ID、建立人、建立時間和指令。這個命令可以幫助使用者理解映象是如何構建的,瞭解每個操作對映象大小的影響,以及對映象進行最佳化和精簡。透過檢視映象的構建歷史,使用者可以更好地理解和管理映象,提高映象的效能和安全性。
下載映象到本地或者在本地構建未最佳化的Dockerfile映象,使用下面的命令構建映象:
docker build -f conf/docker/Dockerfile -t amlservermodel:latest .
使用下面的命令分析映象,可以看到各個操作對映象大小的影響如下:
docker history amlservermodel:latest

使用 docker history 分析映象總結,佔用映象體積較大的兩個層是:
1.下載依賴佔用約:3.18G(包括下載的依賴和快取)。
2.給目錄設定許可權:
在構建docker映象時,Dockerfile檔案中使用指令:RUN chown admin:admin -R /home/admin,為什麼會導致映象體積變大?這條指令會導致映象體積變大的原因是,每一條指令在Dockerfile中都會建立一個新的映象層。當在Dockerfile中使用RUN chown命令時,會建立一個新的映象層,其中包含了檔案許可權的更改。這意味著原本的檔案和目錄仍然存在於之前的映象層中,而新的映象層只是在其基礎上進行了更改。因此,即使在新的映象層中刪除了一些檔案或更改了檔案許可權,但之前的映象層仍然包含了這些檔案,導致映象體積變大。為了避免映象體積變大,可以在Dockerfile中儘量減少使用RUN指令,或者在同一條RUN指令中一次性執行多個操作,以減少建立的映象層數。也可以在構建映象的過程中清理不必要的檔案和快取,以減小映象的體積。
五、最佳化過程
5.1. 最佳化方案
在進行最佳化之前,我們需要了解一些docker映象的構建原則:
5.1.1. 動靜分離原則
我們應該把變化最少的部分放在 Dockerfile 的前面,這樣可以充分利用映象快取。
1.每條指令只要前面的指令快取失效, 則隨後指令構建的映象都不再使用快取。
2.對應COPY和ADD檔案會檢驗檔案的校驗和, 如果發現改變則快取失效。
5.1.2. 多階段構建
Docker多階段構建映象的原理是利用多個Docker容器來處理不同的構建階段,並將最終構建產物傳遞給下一個容器。每個階段可以定義自己的基礎映象、依賴和構建執行環境,使得映象的構建過程更加靈活和高效。
多階段構建映象可以降低最終映象的體積的原因包括以下幾點:
1.最佳化構建產物:多階段構建可以在不同的階段處理不同的構建任務,比如編譯、打包、測試等,從而避免將構建產物暴露給最終映象,減小了最終映象的體積。
2.移除構建環境:多階段構建可以將構建時用到的工具、依賴等移除,只將必要的產物傳遞到最終映象中,避免了構建環境對最終映象的影響,減小了最終映象的體積。
3.最佳化基礎映象:多階段構建可以根據需要選擇不同的基礎映象,每個階段可以選擇適合自己需求的基礎映象,從而避免了不必要的依賴和工具被打包到最終映象中,減小了最終映象的體積。
綜上所述,多階段構建映象可以將構建過程分解成多個階段,根據需要進行最佳化,避免了不必要的依賴和工具被打包到最終映象中,從而降低了最終映象的體積。
5.2. 最佳化分析
透過前面的分析,我們做出如下最佳化:
5.2.1. 構建耗時最佳化
透過多階段構建的方式,可以並行的處理不同階段的構建,只將必要的產物傳遞到最終映象,為了提高下載依賴的效率,我們還可以將專案中使用的依賴提前下載好,構建在第一階段或者基礎映象中,避免每次重新下載全部依賴。我的最佳化方案如下:
使用多階段構建,第一階段下載依賴,第二階段構建應用程式映象。
對於第一階段下載依賴,我將應用程式需要的依賴構建在基礎映象中,避免重新下載全部依賴,如果依賴檔案 requirements.txt有變化,則會重新下載依賴,並且和第二階段的構建是並行進行,任然是可以提高構建效率的,我的修改如下:

最後將下載的依賴從第一階段複製到第二階段,因為應用程式會頻繁修改,所以將應用程式的程式碼放在了Dockerfile檔案的最後,將不經常變化的內容放在Dockerfile檔案前面,可以充分利用映象的快取提高效率,修改後的Dockerfile檔案如下:

我們檢視最佳化後的構建過程如下:

5.2.2. 映象體積最佳化
針對前面的分析,當前案例中映象體積較大的原因有如下幾點:
1.基礎映象較大;
2.安裝的依賴較大,並且開啟了快取;
3.使用RUN chown 指令導致映象較大;
4.由於映象構建中發現有很多指令,構建了很多層,導致映象體積變大;
針對的最佳化方案
1.基礎映象較大我們可以選擇較小的基礎映象,可以在 螞蟻的基礎映象中查詢對應的基礎映象;
2.安裝依賴時使用 pip install –no-cache-dir 關閉快取;
3.移除 RUN chown指令,因為在這裡可以針對特定的檔案或者資料夾指定就行,不需要對所有的目錄修改許可權;
4.合併多個RUN指令,減少映象的層數,進而減少映象的體積;
最後透過針對性的最佳化,映象體積減小到原來的一半,本來想找一個體積更小的基礎映象,但是在基礎映象庫中沒有找到合適的版本,並且透過前面的一系列最佳化,映象的構建時間以及可以達到秒級了,所以後續有需要再自定義一個合適的基礎映象。
六、構建快取失效
構建映象時,Docker 會逐步執行 Dockerfile 中的指令,並按指定的順序執行每條指令。對於每條指令, 構建器都會檢查是否可以重用構建快取中的指令。
6.1. 一般規則
構建快取失效的基本規則如下:
-
構建器首先檢查基礎映象是否已快取。隨後的每個指令都會與快取的層進行比較,如果沒有快取的層與指令完全匹配,則快取將失效。 -
在大多數情況下,將 Dockerfile 指令與相應的快取層進行比較就足夠了,但是有些指令需要額外的檢查和解釋。 -
對於ADD和COPY指令以及RUN帶有繫結掛載的指令(RUN –mount=type=bind),構建器會根據檔案元資料計算快取校驗和,以確定快取是否有效。在快取查詢期間,如果涉及的任何檔案的檔案元資料發生更改,則快取將失效。計算快取校驗和時不考慮檔案的修改時間(mtime),如果只有複製的檔案的 mtime發生了更改,則快取不會失效。 -
除了ADD和COPY命令之外,快取檢查不會檢視容器中的檔案來確定快取匹配。例如,在處理命令時,RUN apt-get -y update不會檢查容器中更新的檔案來確定是否存在快取命中。在這種情況下,只使用命令字串本身來查詢匹配項。
一旦快取失效,所有後續的 Dockerfile 命令都會生成新的影像,並且不會使用快取。
如果構建的映象包含多個層,並且想要確保構建快取可重複使用,請儘可能按從更改頻率較低的順序排列指令。
6.2. RUN 指令
指令快取RUN不會在構建之間自動失效。假設您的 Dockerfile 中有一步要安裝curl:
FROMalpine:3.20 AS install
RUNapk add curl
這並不意味著curl在映象中的版本始終是最新的,一週後重建映象仍將獲得與之前相同的軟體包,如果要強制重新執行該RUN指令,可以:
-
確保之前的一個層已經改變; -
使用以下方法在構建之前清除構建快取 docker builder prune; -
使用–no-cache或–no-cache-filter選項;
該–no-cache-filter選項允許您指定特定的構建階段以使快取無效:
docker build --no-cache-filter install .
如果要使RUN指令的快取失效,可以傳遞一個構建引數,該引數帶有變化的值,構建引數確實會導致快取失效,因為RUN指令是使用命令字串本身來查詢匹配快取的。
七、最佳化總結
要對映象進行最佳化和精簡,你可以採取以下步驟:
1.使用多階段構建:使用多階段構建可以減少映象的大小,因為你可以在不同的映象中執行不同的構建步驟,並在最終映象中只保留必要的檔案和依賴。
2.清理不需要的檔案和依賴:在Dockerfile中,你可以使用一系列命令來清理不需要的檔案和依賴,例如使用rm命令刪除不需要的檔案,使用–no-cache選項來清理快取等。
3.使用輕量的基礎映象:選擇一個輕量的基礎映象作為你的映象的基礎,這樣可以減少映象的大小。
4.合併映象層:在Dockerfile中,你可以使用多個命令來合併多個操作,這樣可以減少映象的層數和大小。
5.我們應該把變化最少的部分放在 Dockerfile 的前面,將經常變化的內容放在最後面,這樣可以充分利用映象快取。
透過以上步驟,你可以對映象進行最佳化和精簡,減少其大小並提高效能。
Dockerfile 編碼規約:
規約項
|
Level
|
說明
|
Dockerfile指令不應超過20條
|
WARN
|
層數過多
|
不應該超過3條連續RUN命令
|
WARN
|
層數過多
|
CMD/ENTRYPOINT/EXPOSE/LABEL指令位置應在COPY/RUN之前
|
INFO
|
動靜分離原則
|
RUN 指令應在COPY主包指令之前
|
ERROR
|
動靜分離原則
|
RUN yum指令後應以yum clean all收尾
|
WARN
|
最小原則
|
RUN pip install應該加–no-cache-dir引數
|
ERROR
|
最小原則
|
RUN npm install指令應加–no-cache引數
|
ERROR
|
最小原則
|
單層映象最大的編譯時間不應超過80秒
|
WARN
|
構建效率過低
|
單層映象體積不應超過500M
|
WARN
|
最小原則
|
構建時發生變化的層不應該超過3層
|
INFO
|
動靜分離
|
base映象體積不應超過2G
|
WARN
|
最小原則
|
最後在網上找到一些其他的最佳化手段,在這裡彙總一下:
-
編寫.dockerignore 檔案
-
容器只執行單個應用
-
將多個 RUN 指令合併為一個
-
基礎映象的標籤不要用 latest
-
每個 RUN 指令後刪除多餘檔案
-
選擇合適的基礎映象(alpine 版本最好)
-
設定 WORKDIR 和 CMD
-
使用 ENTRYPOINT (可選)
-
在 entrypoint 指令碼中使用 exec
-
COPY 與 ADD 優先使用前者
-
合理調整 COPY 與 RUN 的順序
-
設定預設的環境變數,對映埠和資料卷
-
使用 LABEL 設定映象元資料
-
新增 HEALTHCHECK
參考文件
Building best practices:https://docs.docker.com/build/building/best-practices/