引言

这是如何制作最小化Docker镜像系列文章的第四篇:动态二进制文件。 在第一篇文章中,我谈到了如何通过编写更好的Dockerfiles创立较小的镜像;在第二篇文章中,我探讨了如何应用docker-squash压缩镜像层以制作较小的镜像;在第三篇文章中,我介绍了如何将Alpine Linux用作较小的根底镜像。

在这篇文章中,我将探讨制作最小化镜像的最终形式:动态二进制文件。 如果应用程序没有任何依赖关系,并且除了应用程序自身之外什么都不须要,这种状况下该怎么做? 这就是动态二进制文件所实现的,它们包含运行在二进制文件自身中的动态编译程序的所有依赖项。为了了解其含意,让咱们退后一步。

动静链接

大多数应用程序是应用称为动静链接的过程构建的,每个应用程序在编译时都是以这样一种形式来实现的,即它定义了须要运行的库,但实际上在其外部并不蕴含这些库。 这对于操作系统发行版来说十分重要,因为能够独立于应用程序更新库,然而在容器内运行应用程序时,它并不是那么重要。 每个容器镜像都蕴含它将要应用的所有文件,因而无论如何都不会重用这些库。

来看一个例子,创立一个简略的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++指标文件,链接这一步是增加运行应用程序所需的依赖项。 辛运的是,大多数编译工具都做到了这一点,编译和链接能够按如下形式进行。

ianlewis@test:~$ g++ -c hello.cpp -o hello.oianlewis@test:~$ g++ -o hello hello.oianlewis@test:~$ ls -lhtotal 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取得雷同的信息。

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/下。

那么,如果删除其中一个库或将其挪动到动静链接器不晓得的地位,会产生什么?(!! 挪动库文件会毁坏你的零碎,不要轻易尝试!)

ianlewis@test:~$ sudo mv /usr/lib/x86_64-linux-gnu/libstdc++.so.6 /usr/lib/x86_64-linux-gnu/libstdc++.so.6.bkianlewis@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)

能够看到动静链接器未找到该库, 如果咱们尝试运行程序会产生什么?

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.cppianlewis@test:~$ cat << EOF > DockerfileFROM alpine COPY hello /helloENTRYPOINT [ "/hello" ]EOFianlewis@test:~$ docker build -t hello .Sending build context to Docker daemon  29.18kBStep 1/3 : FROM alpinelatest: Pulling from library/alpine88286f41530e: Pull complete Digest: sha256:1072e499f3f655a032e88542330cf75b02e7bdf673278f701d7ba61629ee3ebeStatus: Downloaded newer image for alpine:latest ---> 7328f6f8b418Step 2/3 : COPY hello /hello ---> 6f5aca4d2acbRemoving intermediate container 904f7c441936Step 3/3 : ENTRYPOINT /hello ---> Running in 635f6cbde8d6 ---> bbcaa65bf2e5Removing intermediate container 635f6cbde8d6Successfully built bbcaa65bf2e5Successfully tagged hello:latestianlewis@test:~$ docker run hellostandard_init_linux.go:187: exec user process caused "no such file or directory"

“no such file or directory”这样的谬误,它的描述性不是很高,然而跟咱们之前看到的雷同,示意的是该程序找不到其中某个动静链接的依赖项。

对于容器,咱们心愿镜像尽可能小,治理动静链接的应用程序的依赖项是一项沉重的工作,须要大量工具,例如自身就有大量依赖项的编译包管理器。 当只想运行一个繁多的应用程序时,它将给咱们的运行时环境带来很多累赘,如何解决这个问题?

动态链接使咱们能够将应用程序依赖的所有库捆绑到一个二进制文件中。 这将使得程序在运行状态时从单个二进制文件中复制利用程序代码及其所有依赖项,来尝试操作一下。

ianlewis@test:~$ g++ -o hello -static hello.cpp ianlewis@test:~$ ls -lhtotal 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.cppianlewis@test:~$ ./hello Hello World!ianlewis@test:~$ ldd hello        not a dynamic executable很好,这意味着当初有了一个二进制可执行文件,能够在任何容器镜像中进行复制,并且能够失常工作!ianlewis@test:~$ cat << EOF > Dockerfile> FROM scratch> COPY hello /hello> ENTRYPOINT [ "/hello" ]> EOFianlewis@test:~$ docker build -t hello .Sending build context to Docker daemon  2.202MBStep 1/3 : FROM scratch ---> Step 2/3 : COPY hello /hello ---> d3b2040b4df0Removing intermediate container 78e434104023Step 3/3 : ENTRYPOINT /hello ---> Running in b6340a5907f5 ---> 88af34342471Removing intermediate container b6340a5907f5Successfully built 88af34342471Successfully tagged hello:latestianlewis@test:~$ docker run helloHello World!

如前所述,该程序当初蕴含所有依赖项,因而它实际上能够在任何其余的Linux服务器上运行。可能会存在一些正告,例如,程序须要在具备与之雷同的CPU架构的服务器上运行,然而在大多数状况下,都能将其复制并失常工作。

镜像的大小

以编译过的动态二进制文件为根底的镜像大小可能比以Python或Java等语言编写的须要运行VM的应用程序的镜像小得多。 在上一篇文章中,咱们钻研了以Alpine Linux为根底镜像的Python镜像,用于部署Python应用程序。

ianlewis@test:~$ docker images python:2.7.13-alpineREPOSITORY          TAG                 IMAGE ID            CREATED             SIZEpython              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 helloianlewis@test:~$ docker images helloREPOSITORY          TAG                 IMAGE ID            CREATED             SIZEhello               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/kubernetesCloning 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之类的古代语言轻松构建,怎么会不让人喜爱呢?