1. 程式人生 > >兩個奇技淫巧,將 Docker 映象體積減小 99%

兩個奇技淫巧,將 Docker 映象體積減小 99%

原文連結:Docker Images : Part I - Reducing Image Size

對於剛接觸容器的人來說,他們很容易被自己構建的 Docker 映象體積嚇到,我只需要一個幾 MB 的可執行檔案而已,為何映象的體積會達到 1 GB 以上?本文將會介紹幾個奇技淫巧來幫助你精簡映象,同時又不犧牲開發人員和運維人員的操作便利性。本系列文章將分為三個部分:

第一部分著重介紹多階段構建(multi-stage builds),因為這是映象精簡之路至關重要的一環。在這部分內容中,我會解釋靜態連結和動態連結的區別,它們對映象帶來的影響,以及如何避免那些不好的影響。中間會穿插一部分對 Alpine

映象的介紹。

第二部分將會針對不同的語言來選擇適當的精簡策略,其中主要討論 Go,同時也涉及到了 JavaNodePythonRubyRust。這一部分也會詳細介紹 Alpine 映象的避坑指南。什麼?你不知道 Alpine 映象有哪些坑?我來告訴你。

第三部分將會探討適用於大多數語言和框架的通用精簡策略,例如使用常見的基礎映象、提取可執行檔案和減小每一層的體積。同時還會介紹一些更加奇特或激進的工具,例如 BazelDistrolessDockerSlimUPX,雖然這些工具在某些特定場景下能帶來奇效,但大多情況下會起到反作用。

本文介紹第一部分。

1. 萬惡之源

我敢打賭,每一個初次使用自己寫好的程式碼構建 Docker 映象的人都會被映象的體積嚇到,來看一個例子。

讓我們搬出那個屢試不爽的 hello world C 程式:

/* hello.c */
int main () {
  puts("Hello, world!");
  return 0;
}

並通過下面的 Dockerfile 構建映象:

FROM gcc
COPY hello.c .
RUN gcc -o hello hello.c
CMD ["./hello"]

然後你會發現構建成功的映象體積遠遠超過了 1 GB。。。因為該映象包含了整個 gcc 映象的內容。

如果使用 Ubuntu 映象,安裝 C 編譯器,最後編譯程式,你會得到一個大概 300 MB 大小的映象,比上面的映象小多了。但還是不夠小,因為編譯好的可執行檔案還不到 20 KB

$ ls -l hello
-rwxr-xr-x   1 root root 16384 Nov 18 14:36 hello

類似地,Go 語言版本的 hello world 會得到相同的結果:

package main

import "fmt"

func main () {
  fmt.Println("Hello, world!")
}

使用基礎映象 golang 構建的映象大小是 800 MB,而編譯後的可執行檔案只有 2 MB 大小:

$ ls -l hello
-rwxr-xr-x 1 root root 2008801 Jan 15 16:41 hello

還是不太理想,有沒有辦法大幅度減少映象的體積呢?往下看。

為了更直觀地對比不同映象的大小,所有映象都使用相同的映象名,不同的標籤。例如:hello:gcchello:ubuntuhello:thisweirdtrick 等等,這樣就可以直接使用命令 docker images hello 列出所有映象名為 hello 的映象,不會被其他映象所幹擾。

2. 多階段構建

要想大幅度減少映象的體積,多階段構建是必不可少的。多階段構建的想法很簡單:“我不想在最終的映象中包含一堆 C 或 Go 編譯器和整個編譯工具鏈,我只要一個編譯好的可執行檔案!”

多階段構建可以由多個 FROM 指令識別,每一個 FROM 語句表示一個新的構建階段,階段名稱可以用 AS 引數指定,例如:

FROM gcc AS mybuildstage
COPY hello.c .
RUN gcc -o hello hello.c
FROM ubuntu
COPY --from=mybuildstage hello .
CMD ["./hello"]

本例使用基礎映象 gcc 來編譯程式 hello.c,然後啟動一個新的構建階段,它以 ubuntu 作為基礎映象,將可執行檔案 hello 從上一階段拷貝到最終的映象中。最終的映象大小是 64 MB,比之前的 1.1 GB 減少了 95%