1

引言

这是如何制作最小化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.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获得相同的信息。

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.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)

可以看到动态链接器未找到该库, 如果我们尝试运行程序会发生什么?

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之类的现代语言轻松构建,怎么会不让人喜欢呢?


EngineerLeo
598 声望38 粉丝

专注于云原生、AI等相关技术