制作轻量级 Docker 镜像

一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / Java 学习路线 / 一对一提问 / 学习打卡/ 赠书活动

目前,正在 星球 内带小伙伴们做第一个项目:全栈前后端分离博客项目,采用技术栈 Spring Boot + Mybatis Plus + Vue 3.x + Vite 4手把手,前端 + 后端全栈开发,从 0 到 1 讲解每个功能点开发步骤,1v1 答疑,陪伴式直到项目上线,目前已更新了 204 小节,累计 32w+ 字,讲解图:1416 张,还在持续爆肝中,后续还会上新更多项目,目标是将 Java 领域典型的项目都整上,如秒杀系统、在线商城、IM 即时通讯、权限管理等等,已有 870+ 小伙伴加入,欢迎点击围观

人们常说容器是“轻量级”的。当我最初听到这个时,我错误地认为我们在谈论文件系统占用空间,因为磁盘上 VM 映像的绝对大小是必然的;它限制了我的基础设施设计选择。 Docker 容器 旨在运行单个进程;一个诱人的细节,从中我进一步错误地推断出某种神奇的、无依赖的进程隔离,并且占用空间很小。但在现实生活中,我们使用的大多数 Docker 容器的大小(以兆字节为单位)与其 VM 对应物大致相同,“轻量级”是指与 VM 模拟硬件层时产生的处理开销相比,处理开销相对较小.

事实证明,Docker 镜像在大多数情况下都很大。这是关于 Librato 的 Ops 团队如何缩小它们的故事。

链接二进制文件没有国家

问题是,您不能只是将程序扔进进程隔离的监狱并期望它能够运行。我们每天运行的大多数计算机程序都是动态链接的。就像 Master Pandemonium 一样,我们的程序缺少自身的关键部分。他们需要生活的碎片。系统链接器通常在运行时连接到它们的片段。但是在进程隔离中,我们的程序看不到文件系统的其余部分;他们无法访问可以填补他们灵魂空洞的图书馆文件。链接器无法帮助它们,因此它们会短暂地连枷,然后猛烈地死亡。

就像月球人和松鼠桑迪一样,如果我们的程序要在过程隔离中生存下来,它就需要自带空气、水和食物。实现这一目标的两种方法是编译一个静态二进制文件(即,通过将我们程序的所有部分编译成一个大二进制文件来绕过链接器),或者为我们的进程提供它自己的 chroot。换句话说:弄清楚它需要什么(它链接到的每个库,以及它依赖的每个文件),然后将它与我们实际想要运行的东西一起复制到图像中。

然而,要弄清楚像 Nginx 这样的东西到底需要什么并不容易,而且没有人能够预测一些随机的 Ruby 脚本需要什么。强大到足以模拟流体动力学的计算机会花费数千毫秒来尝试解决运行 Ruby 脚本所需的依赖关系。因此,我们通常会采用更方便的第三条路径:我们只需将整个 Ubuntu 文件系统复制到其中,减去您在 /boot、/dev/、/proc/ 和朋友中找到的内容。我们的图像通常在 500MB-1GB 范围内结束。

谁在乎较小的 Docker 镜像?

事实上,所有对图像文件大小感兴趣的 Docker 拥护者都可以毫不费力地在 Market 街上的任何一家餐厅中找到 12:30 的步入式午餐桌。正如比我年轻的人不断提醒我的那样:*耸耸肩*磁盘很便宜。

很公平,但是当我开始使用 Docker 来探索我们如何使用它来改进我们在 Librato 的部署管道时,我发现一些有趣的模式由于图像的巨大尺寸而变得不切实际。假设我们想在每个单独的节点上运行一个本地的、支持 s3 的注册表,而不是中央注册表。从理论上讲,这消除了网络依赖性(没有中央注册表),同时确保每个节点都可以访问相同的图像(每个人都指向同一个 s3 存储桶)。

然而,在实践中,这意味着复制超过 500MB 只是为了启动本地注册表(注册表本身就是一个 Docker 映像),然后下载并运行从支持 s3 的本地注册表启动应用程序所需的任何实际映像。

如果您对 Docker 有一定的了解,就会知道这些图像是由层组成的,并且 Docker 依靠此属性通过仅复制尚未驻留在本地主机上的层来最大程度地减少复制所有不必要的内容。换句话说,便宜的是你只需要复制一次“沉重的东西”:第一次。之后,拉取新版本的镜像就比较自由了。

但是,如果您运行临时基础架构,就像我们在 Librato 所做的那样,您通常会创建新实例以根据需求进行扩展,或者执行自动中断/修复。这意味着除了部署(取决于你的部署方式),每次你 docker 运行一个镜像,这将是第一次,你将支付全部转移税。我没有对我们的基础设施进行过计算,但我可以毫不犹豫地告诉你,税收是……必然的。我怀疑,这是 Docker 在裸机上流行的一个不常被提及的理由。

所以我写了一个 Shell 脚本...

暂时忽略我对小图像的渴望是否是非理性的共济失调症,真的有办法让这些东西变小吗?事实上,今天有一些工具可以提供帮助。 Dockerize 将采用像 wuftpd 或 Nginx 这样的简单二进制文件,并创建一个仅包含二进制文件及其动态链接到的所有库的小型容器。但是,如果您想在 Docker 容器中运行 Java 或 Python 或 Ruby 脚本怎么办?这些运行时环境是复杂的、自我参照的和庞大的。它们不是 Dockerize 的设计目的。

如果你四处打听,你可能会发现有几个人在使用 buildroot (唉,一切都是旧的又是新的),这是一系列旨在从头开始构建小型嵌入式 Linux 系统的 makefile。这有点笨拙,但您可以通过这种方式有效地构建小型基础镜像。不过在一天结束时,您的图像中仍然会有一堆文件,除了构建运行时所必需的之外,这些文件与您的运行时无关。

但是在 Docker 中,这些东西是层,对吧?每次我们在 Docker 容器中安装一些东西,然后提交它,Docker 都会为我们创建一个新层。因此,如果我们从一个基础镜像开始,并在其上安装 Java 并提交结果,Docker 已经在一个层中有效地为我们隔离了一个 Java 运行时。我们需要做的就是提取该层,然后从父映像复制 Java 二进制文件链接的所有库,我们应该有一个功能性的、最小的、无 cruft 的 Java 运行时映像。

所以我写了一个 shell 脚本来帮助你提取这些层,解析并复制它们的 lib 依赖项并将结果提交到一个新图像中。它叫做 Skinnywhale ,到目前为止,它对我来说非常好用,所以我想你可能也想看看它。

它是如何工作的?

让我们一起创建一个 java 运行时映像。你像往常一样开始,使用像“ubuntu”这样的基础镜像(Skinnywhale 可以使用任何类型的基础镜像)。只需运行映像并像往常一样在上面安装任何你想要的东西。


 #download and run the ubuntu docker image
sudo docker run -ti ubuntu
#install java
apt-get update
apt-get install -y software-properties-common
sudo add-apt-repository ppa:webupd8team/java
sudo apt-get update
sudo apt-get -y install oracle-java8-installer
sudo rm -Rf /var/cache

然后,退出容器,在不提交更改的情况下,使用刚刚运行的容器的 ID 运行 Skinnywhale。您可以在事后从 bash 提示符或 Docker 的 ps 命令复制容器 ID:


 #download and run the ubuntu docker image
sudo docker run -ti ubuntu
#install java
apt-get update
apt-get install -y software-properties-common
sudo add-apt-repository ppa:webupd8team/java
sudo apt-get update
sudo apt-get -y install oracle-java8-installer
sudo rm -Rf /var/cache

Skinnywhale 会监听一些环境变量。设置 DEBUG 将打开详细输出,而 BRUTELIB BRUTEUSRLIB 将分别从父映像强制复制 /lib /usr/lib 的全部内容。准备就绪后,使用您的图像 ID 运行 Skinnywhale,如下所示:


 #download and run the ubuntu docker image
sudo docker run -ti ubuntu
#install java
apt-get update
apt-get install -y software-properties-common
sudo add-apt-repository ppa:webupd8team/java
sudo apt-get update
sudo apt-get -y install oracle-java8-installer
sudo rm -Rf /var/cache

此时,Skinnywhale 将在 /tmp 中为您的父图像和更改层创建目录。然后它遍历您更改的目录树,列出所有动态链接到某物的文件。然后,对于该列表中的每个文件,它运行 ldd,并生成每个依赖项的唯一列表。最后,它将每个依赖项从父映像复制到您的更改目录,并使用 tar 管道传输到 docker import 将结果作为新映像注入回 Docker。

根据您尝试隔离的运行时,您可能会看到一些来自 Skinnywhale 的关于未解决的依赖项的错误和/或警告。这意味着您隔离的运行时中的某些文件实际上不存在于您安装它的系统上。例如,你会看到很多试图隔离 Java 运行时的警告,因为 Java 是一个二进制发行版,它附带了很多链接到系统 X11 库的文件,而整个 X11 并不存在于用于 IaaS 和 PaaS 环境的服务器镜像,例如 Docker 的 Ubuntu 镜像。软件不可怕吗?这些通常不是问题,例如,如果它们不阻止您在 ubuntu 上运行 java,它们将不会阻止您在 Skinnywhale 提取的图像下运行 Java。只要你在运行结束时看到一条 ASCII 码的饥饿鲸鱼,Skinnywhale 就成功了:


 #download and run the ubuntu docker image
sudo docker run -ti ubuntu
#install java
apt-get update
apt-get install -y software-properties-common
sudo add-apt-repository ppa:webupd8team/java
sudo apt-get update
sudo apt-get -y install oracle-java8-installer
sudo rm -Rf /var/cache

噢,可怜的东西,它的肋骨都露出来了。无论如何,此时您应该在图像列表中看到一个以 skinny- 开头的新图像:


 #download and run the ubuntu docker image
sudo docker run -ti ubuntu
#install java
apt-get update
apt-get install -y software-properties-common
sudo add-apt-repository ppa:webupd8team/java
sudo apt-get update
sudo apt-get -y install oracle-java8-installer
sudo rm -Rf /var/cache

哦,顺便说一句,您可能还会遇到使用 dlopen() 的程序的问题,因为 Skinnywhale 无法检测到这些依赖项(它确实需要解析源代码)。如果您不熟悉它,dlopen() 函数,就像 goto 和 javascript 中的 void 运算符一样,是由仇恨者创建的,目的是阻止像你我这样的好人的崇高追求。因此,Java 毫不奇怪地在一些上下文中使用 dlopen(),显然包括使用 dlopen() 手动加载系统解析器库并与之交互。因此,如果您在 Skinnywhale 隔离的运行时容器下运行您的 java 程序时遇到与 DNS 相关的问题,请尝试使用 BRUTELIB 集重新创建您的图像。

复制你的脚本

现在您已经有了一个极简主义的运行时镜像,您可以使用 docker cp 或像这样的 docker build 文件将代码复制到其中:


 #download and run the ubuntu docker image
sudo docker run -ti ubuntu
#install java
apt-get update
apt-get install -y software-properties-common
sudo add-apt-repository ppa:webupd8team/java
sudo apt-get update
sudo apt-get -y install oracle-java8-installer
sudo rm -Rf /var/cache

现在你应该准备好运行它了:


 #download and run the ubuntu docker image
sudo docker run -ti ubuntu
#install java
apt-get update
apt-get install -y software-properties-common
sudo add-apt-repository ppa:webupd8team/java
sudo apt-get update
sudo apt-get -y install oracle-java8-installer
sudo rm -Rf /var/cache

祝你好运!

Skinnywhale 最初是 Librato 黑客日项目。您可以 在此处 详细了解我们如何开展令人敬畏且充满乐趣的黑客日。我真诚地希望您觉得它有用,并且希望您能提供反馈。我特别感谢关于为什么这是一个愚蠢无用的工具的负面反馈,因为我从根本上误解了 Docker 应该如何工作。没有什么比发现有一种创建微型运行时 Docker 映像的神奇方法更让我高兴的了。也欢迎关于为什么我不应该关心的令人信服的争论。祝你好运!