建立最小化的容器鏡像(四):靜態二進制文件

引言

這是如何製做最小化Docker鏡像系列文章的第四篇:靜態二進制文件。 在第一篇文章中,我談到了如何經過編寫更好的Dockerfiles建立較小的鏡像;在第二篇文章中,我討論瞭如何使用docker-squash壓縮鏡像層以製做較小的鏡像;在第三篇文章中,我介紹瞭如何將Alpine Linux用做較小的基礎鏡像。python

在這篇文章中,我將探討製做最小化鏡像的最終方式:靜態二進制文件。 若是應用程序沒有任何依賴關係,而且除了應用程序自己以外什麼都不須要,這種狀況下該怎麼作? 這就是靜態二進制文件所實現的,它們包括運行在二進制文件自己中的靜態編譯程序的全部依賴項。爲了理解其含義,讓咱們退後一步。linux

動態連接

大多數應用程序是使用稱爲動態連接的過程構建的,每一個應用程序在編譯時都是以這樣一種方式來完成的,即它定義了須要運行的庫,但實際上在其內部並不包含這些庫。 這對於操做系統發行版來講很是重要,由於能夠獨立於應用程序更新庫,可是在容器內運行應用程序時,它並非那麼重要。 每一個容器鏡像都包含它將要使用的全部文件,所以不管如何都不會重用這些庫。ios

來看一個例子,建立一個簡單的C++程序並按以下所示進行編譯,則將得到一個動態連接的可執行文件。c++

ianlewis@test:~$ cat hello.cpp 
#include <iostream>
int main() {
    std::cout << "Hello World!\n";
    return 0;
}
ianlewis@test:~$ g++ -o hello hello.cpp
$ ls -lh hello
-rwxrwxr-x 1 ianlewis ianlewis 8.9K Jul  6 07:31 hello

g++實際上正在執行兩個步驟,它正在編譯個人程序並將其連接。 編譯這一步只會建立一個普通的C++目標文件,連接這一步是添加運行應用程序所需的依賴項。 辛運的是,大多數編譯工具都作到了這一點,編譯和連接能夠按以下方式進行。git

ianlewis@test:~$ g++ -c hello.cpp -o hello.o
ianlewis@test:~$ g++ -o hello hello.o
ianlewis@test:~$ ls -lh
total 20K
-rwxrwxr-x 1 ianlewis ianlewis 8.9K Jul  6 07:41 hello
-rw-rw-r-- 1 ianlewis ianlewis   85 Jul  6 07:31 hello.cpp
-rw-rw-r-- 1 ianlewis ianlewis 2.5K Jul  6 07:41 hello.o

經過在Linux系統上對其運行ldd命令會輸出命令行指定的每一個程序或共享對象所需的共享對象(共享庫)
。 若是你使用的是Mac OS,則能夠經過運行otool -L得到相同的信息。github

ianlewis@test:~$ ldd hello
        linux-vdso.so.1 =>  (0x00007ffc0075c000)
        libstdc++.so.6 => /usr/lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f88c92d0000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f88c8f06000)
        libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f88c8bfc000)
        /lib64/ld-linux-x86-64.so.2 (0x0000558132cbf000)
        libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f88c89e6000)

能夠看到,個人程序依賴於C和C ++標準庫libc和libstdc ++。當運行程序時,動態連接器會找到我須要的庫,並在運行時將它們連接起來,在Linux上配置文件一般在/etc/ld.so.conf/下。docker

那麼,若是刪除其中一個庫或將其移動到動態連接器不知道的位置,會發生什麼?(!! 移動庫文件會破壞你的系統,不要輕易嘗試!)shell

ianlewis@test:~$ sudo mv /usr/lib/x86_64-linux-gnu/libstdc++.so.6 /usr/lib/x86_64-linux-gnu/libstdc++.so.6.bk
ianlewis@test:~$ ldd ./hello
        linux-vdso.so.1 =>  (0x00007ffd511c6000)
        libstdc++.so.6 => not found
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fdace840000)
        /lib64/ld-linux-x86-64.so.2 (0x0000560da65aa000)

能夠看到動態連接器未找到該庫, 若是咱們嘗試運行程序會發生什麼?api

ianlewis@test:~$ ./hello 
./hello: error while loading shared libraries: libstdc++.so.6: cannot open shared object file: No such file or directory

和預想的一致:沒法加載libstdc ++庫,應用程序崩潰,這使咱們瞭解了爲何這會對容器不利。服務器

爲何動態連接會對容器不利?

動態連接對容器不利的主要緣由是,編譯應用程序的系統可能與運行應用程序的系統徹底不一樣。 對於Linux發行版,他們能夠將應用程序打包爲動態連接的可執行文件,由於他們知道如何設置動態連接程序。 可是即便對於相似的Linux發行版(如Ubuntu或Debian),將二進制文件從另外一個系統複製到另外一個系統,即便是將它們命名爲不一樣名稱,也可能會致使問題。

這就是爲何大多數Dockerfile都在相同容器鏡像中構建應用程序的緣由。使用Docker多階段構建會變得更好,但仍未被普遍採用(截至撰寫本文時)。 有關在系統之間複製文件的全部問題,即便使用多階段構建,你可能仍然但願在與構建應用程序相同的Linux發行版上運行應用程序。

來嘗試一下在在Alpine Linux版本的Ubuntu上編譯hello程序。

ianlewis@test:~$ g++ -o hello hello.cpp
ianlewis@test:~$ cat << EOF > Dockerfile
FROM alpine 
COPY hello /hello
ENTRYPOINT [ "/hello" ]
EOF
ianlewis@test:~$ docker build -t hello .
Sending build context to Docker daemon  29.18kB
Step 1/3 : FROM alpine
latest: Pulling from library/alpine
88286f41530e: Pull complete 
Digest: sha256:1072e499f3f655a032e88542330cf75b02e7bdf673278f701d7ba61629ee3ebe
Status: Downloaded newer image for alpine:latest
 ---> 7328f6f8b418
Step 2/3 : COPY hello /hello
 ---> 6f5aca4d2acb
Removing intermediate container 904f7c441936
Step 3/3 : ENTRYPOINT /hello
 ---> Running in 635f6cbde8d6
 ---> bbcaa65bf2e5
Removing intermediate container 635f6cbde8d6
Successfully built bbcaa65bf2e5
Successfully tagged hello:latest
ianlewis@test:~$ docker run hello
standard_init_linux.go:187: exec user process caused "no such file or directory"

「no such file or directory」這樣的錯誤,它的描述性不是很高,可是跟咱們以前看到的相同,表示的是該程序找不到其中某個動態連接的依賴項。

對於容器,咱們但願鏡像儘量小,管理動態連接的應用程序的依賴項是一項繁重的工做,須要大量工具,例如自己就有大量依賴項的編譯包管理器。 當只想運行一個單一的應用程序時,它將給咱們的運行時環境帶來不少負擔,如何解決這個問題?

image.png

靜態連接使咱們能夠將應用程序依賴的全部庫捆綁到一個二進制文件中。 這將使得程序在運行狀態時從單個二進制文件中複製應用程序代碼及其全部依賴項,來嘗試操做一下。

ianlewis@test:~$ g++ -o hello -static hello.cpp 
ianlewis@test:~$ ls -lh
total 2.1M
-rwxrwxr-x 1 ianlewis ianlewis 2.1M Jul  6 08:08 hello
-rw-rw-r-- 1 ianlewis ianlewis   85 Jul  6 07:31 hello.cpp
ianlewis@test:~$ ./hello 
Hello World!
ianlewis@test:~$ ldd hello
        not a dynamic executable
很好,這意味着如今有了一個二進制可執行文件,能夠在任何容器鏡像中進行復制,而且能夠正常工做!
ianlewis@test:~$ cat << EOF > Dockerfile
> FROM scratch
> COPY hello /hello
> ENTRYPOINT [ "/hello" ]
> EOF
ianlewis@test:~$ docker build -t hello .
Sending build context to Docker daemon  2.202MB
Step 1/3 : FROM scratch
 ---> 
Step 2/3 : COPY hello /hello
 ---> d3b2040b4df0
Removing intermediate container 78e434104023
Step 3/3 : ENTRYPOINT /hello
 ---> Running in b6340a5907f5
 ---> 88af34342471
Removing intermediate container b6340a5907f5
Successfully built 88af34342471
Successfully tagged hello:latest
ianlewis@test:~$ docker run hello
Hello World!

如前所述,該程序如今包含全部依賴項,所以它實際上能夠在任何其餘的Linux服務器上運行。可能會存在一些警告,例如,程序須要在具備與之相同的CPU架構的服務器上運行,可是在大多數狀況下,都能將其複製並正常工做。

鏡像的大小

以編譯過的靜態二進制文件爲基礎的鏡像大小可能比以Python或Java等語言編寫的須要運行VM的應用程序的鏡像小得多。 在上一篇文章中,咱們研究了以Alpine Linux爲基礎鏡像的Python鏡像,用於部署Python應用程序。

ianlewis@test:~$ docker images python:2.7.13-alpine
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
python              2.7.13-alpine       3dd614730c9c        4 days ago          72.02 MB

這個python鏡像只有72MB,應用程序代碼僅需添加到之上便可。 若是僅包括靜態二進制文件,鏡像可能會小得多,只須要和二進制文件同樣大便可。

ianlewis@test:~$ ls -lh hello
-rwxrwxr-x 1 ianlewis ianlewis 2.1M Jul  6 08:41 hello
ianlewis@test:~$ docker images hello
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
hello               latest              88af34342471        5 minutes ago       2.18MB

如今,終於達到了鏡像尺寸幾乎沒有一點多餘的水平。
但實際上,你可能但願在鏡像中包括其餘應用程序,以幫助進行故障排除和調試,在這種狀況下,你可能須要結合使用Alpine Linux來爲應用程序安裝帶有靜態二進制文件的編譯工具,包括shell、trace之類的工具,可能會對你後續工做很是有幫助。

使用go編寫容器化應用

我不能在不說起Go的狀況下寫關於編寫靜態連接應用程序的文章,因爲本文章範圍以外的緣由,在沒有太多的奉獻精神和意志力的狀況下,將大型C++應用程序編譯爲靜態二進制文件多是不切實際的。 許多第三方或開源程序甚至都沒有提供將應用程序編譯爲靜態二進制文件的方法,所以不得不使用基於大型Linux發行版的鏡像進行部署。

Go將靜態連接的二進制文件做爲其工具的一部分使得編譯變得很是容易,能夠這麼說,Go就是經過這種方式建立的,由於Google在其生產系統中將靜態連接的二進制文件部署在容器中,而Go就是專門爲了使其易於實現而建立的,即便是像Kubernetes這樣的大型應用程序。

ianlewis@test:~$ git clone https://github.com/kubernetes/kubernetes
Cloning into 'kubernetes'...
...
ianlewis@test:~$ cd kubernetes/
ianlewis@test:~/kubernetes$ make quick-release
+++ [0711 06:33:32] Verifying Prerequisites....
+++ [0711 06:33:32] Building Docker image kube-build:build-36cca30eef-5-v1.8.3-1
+++ [0711 06:34:18] Creating data container kube-build-data-36cca30eef-5-v1.8.3-1
+++ [0711 06:34:19] Syncing sources to container
+++ [0711 06:34:22] Running build command...
...
ianlewis@test:~/kubernetes$ ldd _output/dockerized/bin/linux/amd64/kube-apiserver 
        not a dynamic executable

綜上所述,以靜態二進制文件方式獲得的鏡像最小,同時包含了全部運行所需的依賴,所以能夠輕鬆地在容器中運行,而且可使用Go之類的現代語言輕鬆構建,怎麼會不讓人喜歡呢?

相關文章
相關標籤/搜索