Docker 正在减肥
Docker 容器在 CI 和 CD 方面非常有用。作为一名开发者,我对它的可能性和易用性感到惊讶。然而,当我开始使用 Docker 时,我并没有意识到自己完全搞错了。
经过几个月的构建和将容器推送到我们的私人 Nexus Registry 之后,我们的开发人员找到我说:“嘿,你的镜像占用了注册表的所有磁盘空间,可能需要检查一下它们是否有点过大?”。
我有两个 Docker 镜像,一个用于 Python 应用程序,一个用于 Java 应用程序。它们的总大小高达 6.5GB(不含应用程序本身)。经过所有增强后,Python 镜像的大小为 1.4GB,Java 镜像的大小为 660MB(包含应用程序的二进制文件)。
在本文中,我将介绍优化 docker 镜像大小的技巧和窍门,这将节省您的注册表磁盘空间和(重新)部署时间。
创建基线
首先要做的是从图像中提取常见内容。
所有常用库和文件都应该放在基础镜像中,这样它们就会被缓存在 Docker 层中。就我而言,我将 Apache Thrift 和自定义日志轮换实用程序(从源代码构建)移到了基础镜像中,立即节省了 350MB 的空间。
关于层
每个 Docker 镜像都由一系列层级构成。这些层级是使用 FROM、RUN、COPY 等命令对镜像进行的更改,您可以将其视为 Git 仓库中的提交。Docker 利用联合文件系统将这些层级合并为单个镜像。联合文件系统允许将不同文件系统(称为分支)的文件和目录透明地叠加,从而形成一个统一的文件系统。
留意临时文件
图层很方便,因为它们可以在构建或部署期间重复使用,但它们也可以使图像变得更大。
想象一下为 nodejs 应用程序创建图像:
看起来像是我们在 Bash 脚本里写的,对吧?但对 docker 来说完全不行。新镜像的大小是 248Mb。我们来看看docker history命令:
输出显示我们有两倍的数据,每个都是 67.2MB。这是因为我们在一个层解压,在另一个层复制,而最后一个RUN命令中的rm对之前的层没有任何影响。因此,优化此示例的正确方法是:
注意,还有一个rm -rf /var/lib/apt/lists/命令,它会清除 aptitude 的缓存。我们再看看docker 的历史记录。
不要落入抄袭陷阱
COPY命令用于在构建过程中将主机中的数据放入图像中。
COPY的缺点是它会创建一个单独的层,因此复制的数据将永远驻留在镜像中。复制存档或一些临时数据绝对不是一个好主意。如果可能的话,最好使用 wget(或其他工具)而不是 COPY,否则……
使用多阶段构建
多阶段构建是一项相对较新的功能,需要 Docker 17.05 或更高版本。此功能允许创建临时镜像,然后在构建最终镜像时使用其文件系统。其基本思路是安装所有依赖项,并在中间镜像中编译所有源文件,然后将结果复制到最终镜像中。我们来看一个示例:
看起来我们有很多仅构建时的依赖项:
现在我们添加另一个构建阶段并从“编译器”映像复制二进制文件:
零钱和丰厚奖励:
但要小心,不要删除包的运行时依赖项!
Java 运行时
你听说过jlink吗?它太棒了!你可以只用你需要的模块创建自己的 JRE,它应该比任何“精简”镜像都小。我建议使用AdoptOpenJDK镜像作为 jlink 的提供程序。思路是先用 jlink 创建 JRE,然后使用多阶段构建复制它:
让我们看一下docker镜像
adoptopenjdk/openjdk11 x86_64-ubuntu-jdk-11.28 323MB
adoptopenjdk/openjdk11 jdk-11.0.5_10-ubuntu-slim 250MB
adoptopenjdk/openjdk11 x86_64-ubuntu-jre11u-nightly 127MB
custom-jre-build jre11 62.5MB
使用 jlink,我们实现了比最小 AdoptOpenJDK 镜像小两倍的镜像大小。更不用说自定义 JRE 与基准镜像之间的集成非常便捷(一条简单的 COPY 指令)。
Java 依赖项
大多数项目使用 Maven 或 Gradle 进行依赖管理。有时我们的依赖项非常多。在我们的项目中,依赖项多达 400MB!构建工具将所有依赖项的 jar 文件都放在主 jar 文件中,考虑到依赖项更新频率不高,我们会有很多具有相同依赖项的镜像。看起来非常适合迁移到其他层!
首先,您需要从项目中提取依赖项,如果它们已更新,则将它们推送到您的注册表。
让我们看看如何使用 Maven 和 SpringBoot 来实现。将excludeGroupIds参数添加到构建器插件配置中:
并使用这个小 bash 脚本:
现在,我们如何知道依赖项是否已更新以及是否需要推送它们?以及如何使用“依赖项镜像”作为应用程序镜像的基准?
我采用了一种 hack 的方法,计算 pom.xml 的 md5 哈希值,然后尝试从镜像仓库中拉取对应版本的 docker 镜像。如果镜像不存在,就创建并推送。我的做法如下:
接下来,我们通过ARG命令将依赖项哈希传递给应用程序的 dockerfile,并在FROM- image版本中使用它:
docker build --build-arg version=${DEPENDENCIES_HASHES} -f application.dockerfile .
请注意,现在有一个 maven/gradle 插件jib,其工作与我上面写的类似(但不适用于我们的情况,因为我们的 Gitlab 运行器在每次构建后都会清除缓存)。
Python 特定
我使用了与 Java 容器相同的方法。我计算 requirements.txt 文件的哈希值,并将其用作中间容器的版本。
在 dockerfile 中,你应该使用多阶段构建。在第一阶段,安装所有构建依赖项,然后执行pip install --no-cache-dir。在结果阶段,从上一阶段复制 /opt/miniconda3 目录。
我们来看一下 python-dependencies.dockerfile。别介意 apt-get install 部分,这些只是我的 Python 依赖项所需的包。
python-dependencies.dockerfile
剩下要做的是根据依赖镜像创建最终的 Python 镜像。我只需复制所有 Python 脚本(以及一个用于启动 Python 的 Bash 脚本)。
文章来源:https://dev.to/sammyvimes/docker-on-a-diet-1n6j