最佳化 Dockerfile - 活用 cache

今天來看看如何利用 cache 讓 build image 更加順利。

Build image 第一次會正常執行每一個指令,第二次如果發現是同一個 commit 上,執行同一個指令,且結果推測不會變的時候,會把前一次執行結果的 commit 直接拿來用,並標上 Using cache 訊息。

---> Using cache

有效利用 cache 可以提升開發或測試 build image 的效率,這也是最佳化 Dockerfile 的一環。

延續使用昨天的 Dockerfile:

FROM php:7.3

WORKDIR /source
COPY . .
RUN curl -sS https://getcomposer.org/installer | php && mv composer.phar /usr/local/bin/composer
RUN apt update && apt install unzip
RUN composer install

CMD ["php", "artisan", "serve", "--host", "0.0.0.0"]

上面這個範例無法有效利用 cache,可以用以下指令做個實驗:

# 先確認 build image 可以抓 cache
make build

# 隨意新增檔案,修改檔案也會有一樣的效果
touch some

# 先確認 build image 無法抓 cache
make build

從這個範例可以發現,程式有做任何修改,build image 都會需要全部重跑,浪費時間也浪費網路頻寬。

而根據 cache 生效的規則,我們只要找到第一個沒有 Using cache 的指令,就有機會知道問題在哪。

Step 1/7 : FROM php:7.3
---> d9b8167b4a1c
Step 2/7 : WORKDIR /source
---> Using cache
---> 6db08839d8a0
Step 3/7 : COPY . .
---> 70a56357ee18

從這個輸出資訊來看,是 COPY . . 沒有使用 cache。這是因為 touch 新檔案也包含在 COPY . . 的範圍裡,但新檔案 Docker 也不知道跟 build image 過程有關係,因此會讓 COPY . . 重新執行,接著後面全部都需要一起重新執行。

類似的問題,COPY 是針對 host 檔案,另外還有一個很像的指令是 ADD,後面可以接下載檔案的端口,如:

ADD https://example.com/download.zip .

它會在 build image 的時候下載連結的檔案並複製進 container。這功能看似很方便,實際上是會嚴重拖累 build image 時間的。因為一開始有提到:「結果推測不會變的時候」才會使用 cache。上面 ADD 例子的問題點在於,必須要把檔案下載回來才知道檔案內容有沒有被修改過,於是變成每次 build image 都需要下載檔案。

回到範例的 Dockerfile,調整方法很簡單,記住一個原則:

不常變動的 能早做就早做
常變動的 能晚做就晚做

我們把 Dockerfile 分做幾個部分:

  1. 全域設定
  2. 安裝環境,如 unzip 指令
  3. 安裝程式工具,如 composer 指令
  4. 程式碼

概念上來說,環境與工具會是變動最少的,最常變動的則是程式碼。通常全域設定會放在一開始,偶爾在常調整設定的時候,會放在最後方便測試,最終還是要看使用情境與習慣。

根據上述順序調整如下:

FROM php:7.3

# 全域設定
WORKDIR /source

# 安裝環境、安裝工具
RUN curl -sS https://getcomposer.org/installer | php && mv composer.phar /usr/local/bin/composer
RUN apt update && apt install unzip

# 程式碼
COPY . .
RUN composer install

CMD ["php", "artisan", "serve", "--host", "0.0.0.0"]

用一樣的流程測看看,會看到 Using cache 變多,速度也變快很多。

Step 1/7 : FROM php:7.3
---> d9b8167b4a1c
Step 2/7 : WORKDIR /source
---> Using cache
---> 6db08839d8a0
Step 3/7 : RUN curl -sS https://getcomposer.org/installer | php && mv composer.phar /usr/local/bin/composer
---> Using cache
---> c5a5b2b80982
Step 4/7 : RUN apt update && apt install unzip
---> Using cache
---> ddf04cc99574
Step 5/7 : COPY . .
---> d1bc2db13e9f

但一樣卡在時間花最久的 composer install,它不能放到 COPY . . 的上面,因為它會需要複製進去的 composer.jsoncomposer.lock

咦?既然它需要這兩個檔,那就先複製進去再執行 composer install 就好了呀!改法如下:

FROM php:7.3

# 全域設定
WORKDIR /source

# 安裝環境、安裝工具
RUN curl -sS https://getcomposer.org/installer | php && mv composer.phar /usr/local/bin/composer
RUN apt update && apt install unzip

# 安裝程式依賴套件
COPY composer.* ./
RUN composer install --no-scripts

# 複製程式碼
COPY . .
RUN composer run post-autoload-dump

CMD ["php", "artisan", "serve", "--host", "0.0.0.0"]

這次會發現 composer install 有用到 cache,而且速度已經快到可以全程錄進 GIF 了。

Sending build context to Docker daemon  501.8kB
Step 1/9 : FROM php:7.3
---> d9b8167b4a1c
Step 2/9 : WORKDIR /source
---> Using cache
---> 6db08839d8a0
Step 3/9 : RUN curl -sS https://getcomposer.org/installer | php && mv composer.phar /usr/local/bin/composer
---> Using cache
---> c5a5b2b80982
Step 4/9 : RUN apt update && apt install unzip
---> Using cache
---> ddf04cc99574
Step 5/9 : COPY composer.* ./
---> Using cache
---> 7e6b6308c79c
Step 6/9 : RUN composer install --no-scripts
---> Using cache
---> 6c8d6d6d168d
Step 7/9 : COPY . .
---> 63ab9135e738

這個 Dockerfile 對 cache 的最佳化就到此結束,從剛實驗看起來,目前改程式重 build image 都非常快速,已達成今天所預期的目標。

不使用 cache

最後補充一點,有些情況會希望 Docker 強制重新 build image 而不要使用 cache,這時可以使用之前提供的 Makefile 裡的 make rebuild 指令:

rebuild:
docker build -t=$(IMAGE):$(VERSION) --no-cache .

相信不需要解釋,--no-cache 正是不使用 cache。

今日自我回顧

今天介紹的最佳化方法,是著重在利用 cache 加速 build image。但不要忘了今天開頭提到的,cache 就是 commit,有時候必須要把它視為 commit 來做最佳化,這將會是明天的主題。

  • 了解 Using cache 在 build image 是怎麼運作的
  • 練習利用 cache 加速 build image 的方法