关于container:创建最小化的容器镜像四静态二进制文件

3次阅读

共计 5957 个字符,预计需要花费 15 分钟才能阅读完成。

引言

这是如何制作最小化 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”这样的谬误,它的描述性不是很高,然而跟咱们之前看到的雷同,示意的是该程序找不到其中某个动静链接的依赖项。

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

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

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

正文完
 0