每當您構建 Docker 映像時,例如,您想要將 Java/Node/Python 應用程式整合為一個,您都會遇到以下兩個問題:
- 如何使docker build命令執行得儘可能快?
- 如何確保生成的 Docker 映象儘可能小?
Docker 映象層 101
看看下面的內容Dockerfile。
FROM eclipse-temurin:17-jdk |
透過執行docker build -t myapp .此 Dockerfile,您將獲得(一個)Docker映象,該映象將基於 Java 17 (Eclipse-Temurin) 映象,幷包含並執行我們的 Java 應用程式(app.jar 檔案)。
- Docker 行中的每一行都將建立一個Docker 層
- 每個映象都由幾個這樣的層組成。
您可以透過執行來確認這一點,例如:
docker image history myapp
這將在新行上返回層:
IMAGE CREATED CREATED BY SIZE COMMENT |
我們的映象中
- 有一層ENTRYPOINT、一層用於COPY、一層用於ARG。
- 包含 app.jar 檔案(COPY)的圖層大約有 20MB,ENTRYPOINT 和 ARG 行的後設資料圖層為 0B。
現在,我們如何處理這些資訊?
你的層很容易膨脹
想象一下,您想要透過包管理器安裝一個包,為此,您想要執行apt update,它會更新包管理器的索引。
FROM eclipse-temurin:17-jdk |
讓我們看一下生成的圖層 ( docker image history myapp),並重點關注最後一行 ( RUN /bin/sh -c…):
IMAGE CREATED CREATED BY SIZE COMMENT |
哇哈! Runningapt-update為我們生成的 Docker 映象新增了一個大小高達 45.7MB 的新層。
現在,每次推送或拉取映象時,您都需要傳輸這些額外的兆位元組。
圖層是相加的
讓我們繼續上面的示例並新增更多執行命令,以安裝最新的 mysql 軟體包。
FROM eclipse-temurin:17-jdk |
此外,我們還使用該rm -rf /var/lib/apt/lists/*命令刪除 apt 索引快取(上面的 45.7MB)。讓我們看看我們的映象歷史現在是什麼樣子:
59f82a5b4c5a 6 seconds ago ENTRYPOINT ["java" "-jar" "/app.jar"] 0B buildkit.dockerfile.v0 |
哇啊,那是什麼?即使我們刪除了 apt 快取檔案,45.7MB 層仍然存在(除了 605MB MySQL 層,順便說一句)。
這是因為層是嚴格可加的/不可變的。您當然可以從當前圖層中刪除這些檔案,但較舊/之前的圖層仍將包含它們。
如何解決這個問題?
一個簡單的解決方法是RUN在一行上執行所有三個命令(==單個結果層)
FROM eclipse-temurin:17-jdk |
現在讓我們看看該影像的歷史:
IMAGE CREATED CREATED BY SIZE COMMENT |
哈!我們現在至少節省了 45.7MB。但這還有什麼問題呢?
使其可重複
理想情況下,您希望您的構建是可重現的(誰會想到)。透過執行apt update然後安裝儲存庫中的任何最新軟體包,您實際上會破壞這種可重複性,因為軟體包版本可能會在構建之間發生變化。
要旨:
- 僅安裝您要安裝的特定版本
- 首先避免在 Dockerfile 中為你的應用程式新增(你所選擇的軟體包管理器)相反,構建一個新的基礎映象,並在你的 Dockerfile 的 FROM 中使用它。這樣速度也會快很多!
圖層順序很重要
您需要確保將變化較大的圖層放置在底部Dockerfile,而更穩定的圖層應放置在頂部。
為什麼?因為在構建映象時,您需要從構建之間更改的層開始重建每個層。
一個實際的例子:想象一下,您想要將一個index.html檔案打包到您的映象中,該檔案變化很大,即比其他任何內容都更頻繁。
FROM eclipse-temurin:17-jdk |
您可以看到該COPY index.html index.html行幾乎新增到了 的頂部Dockerfile。
現在,每次index.html 檔案發生更改時,您都需要重建所有後續層,即圖層_RUN apt-update, ARG & COPY app.jar
這是一個巨大的時間消耗:在我的機器上,以上所有操作大約需要 17 秒才能完成。
但是,如果您將語句重新排序到底部,Docker 可以重新使用所有先前的層,因為它們沒有改變。
FROM eclipse-temurin:17-jdk |
現在一個新的docker build只需要 0.5 秒(在我的機器上),好多了!
以下是黃金分層規則:
- 很少更改或時間/網路密集型的檔案(例如安裝新軟體) → 頂部
- 經常更改的檔案(例如原始碼)→ 非常低
- ENV、CMD 等 → 底部
Docker什麼時候重新構建層?
每當您執行docker build.關於 Docker 何時以及如何快取層有一組特定的規則,您可以在官方文件中閱讀它們。
要點是,每當您執行 Docker 構建時,Docker 都會:
- 檢查 Dockerfile 中的命令是否有更改(例如,你是否將 RUN blah 改為 RUN doh)。
- 在 ADD 或 COPY 的情況下,是否有任何涉及的檔案(或者說它們的校驗和)發生了變化?
.dockerignore
當你執行 docker build -t <tag> .時,你的當前目錄 .實際上就是所謂的構建上下文。這意味著當前目錄下的所有檔案都將被壓縮併傳送給本地或遠端的 Docker 守護程序來執行構建。
如果你想確保某些目錄永遠不會被髮送到你的構建守護程序,從而保持快速和小巧,你可以建立一個 .dockerignore 檔案,其語法與 .gitignore 類似。
一般來說,你應該把與編譯無關的檔案/目錄放在這裡(比如你的 .git 資料夾),這在使用 COPY ./somewhere 等命令時尤為重要,因為這樣一來,整個專案都會出現在生成的映像中。
以 npm 為例:例如,你可能想在構建時執行 npm install,讓它下載其依賴項,而不是(慢慢地)將你的 node_modules 資料夾複製進去,因此這也是 dockerignore 檔案的一個很好的候選項。不過,如果你這麼做了,還有一個技巧你一定要知道:目錄快取。
目錄快取
例如,您執行 npm install、pip install gradlew build 等來構建映象。這將導致下載依賴項並建立新的映像層。現在,如果要重建該映像層,所有依賴項都將在下一次構建時重新下載,因為已經下載的依賴項中沒有 .npm、.cache 或 .gradle 資料夾可用。
但你可以改變這種情況!讓我們以 pip 為例,修改以下一行:
FROM ... |
到:
RUN --mount=type=cache,target=/root/.cache pip install -r requirements.txt |
這將告訴 Docker 在構建過程中將快取層/資料夾(/root/.cache)掛載到容器中,在本例中,就是 pip 為根使用者快取其依賴項的資料夾。訣竅在於:這個資料夾最終不會出現在生成的映像中,但/root/.cache 會在所有後續構建中提供給 pip,這樣你就能獲得不錯的速度!
NPM、Gradle 或其他軟體包管理器也是如此。只需確保指定正確的目標資料夾即可。