OCI 容器镜像的优化
OCI 容器镜像在云原生应用的开发中起着非常重要的作用。如何优化 OCI 容器镜像也是一个重要的话题。一般来说,OCI 容器镜像的优化有三个方向:
- 容器镜像的尺寸
- 容器镜像的拉取和推送速度
- 容器镜像的构建时间
这三个方向有各自不同的目标。容器镜像的尺寸影响的是所占用的存储空间;拉取速度会影响容器的启动时间;构建时间则会影响持续集成的速度。这三个方向的侧重点也有所不同。
容器镜像的尺寸
容器镜像的尺寸的最重要的决定因素是所要运行的应用。在很多时候,减少镜像的尺寸只能从应用自身入手。以 Java 应用为例,有些以传递依赖的方式引入的第三方库,对应用来说可能是不需要的。删除这些第三方库,可以减少镜像的尺寸。通过 GraalVM 创建 Java 应用的原生可执行文件,又可以进一步的减少镜像尺寸。
除了对应用的修改之外,另外一个可行的做法是删除镜像中不必要的内容。最佳的情况是镜像中只包含应用运行时必需的内容。
第一个需要考虑的方面是选择适合的基础镜像。比如,基于 Alpine Linux 的镜像的尺寸,就比基于 Ubuntu 的镜像的尺寸要小得多。同样是 Java 相关的镜像,JDK 和 JRE 镜像的尺寸也大不相同。以 Eclipse Temurin 的 OpenJDK 11 相关的镜像为例,下表给出了相关镜像的尺寸。最大的镜像的尺寸是最小的镜像的尺寸的五倍。
镜像标签 | 尺寸(MB) |
---|---|
11-jdk-alpine | 186.92 |
11-jre-alpine | 43.79 |
11-jdk-focal | 235.73 |
11-jre-focal | 92.11 |
很明显,如果 JRE 的 Alpine 镜像能满足需求,它是最好的选择。
第二个需要考虑的方面是对辅助工具的处理。在镜像中,除了应用自身之外,通常还会添加一些辅助工具。这些工具的主要作用是为了调试。比如,curl
和 jq
是常用的工具。举例来说,如果在运行时发现应用无法调用第三方服务,在调试时,可以连接到运行时的容器,并使用 curl
工具来进行测试。这样可以更快的定位问题。可以有几种不同的解决方案:
- 直接把
curl
作为应用镜像的一部分。这种做法的好处是使用简单,当需要时直接可用。坏处是增加了镜像的尺寸。这个工具实际上被使用的频率可能非常低,造成了不必要的浪费。 - 辅助工具也可以在需要使用时再安装。基础镜像一般都提供了包管理工具,比如
apt
,可以在运行时安装工具。这种做法的好处是节省了镜像的空间。坏处是使用起来比较麻烦,而且在运行时不一定具备安装工具的条件。比如,容器可能运行在隔离的网络内部,无法访问外部网络;也可能容器以非 root 用户来运行,不具备安装工具的权限。 - 为应用调试提供专门的镜像。在构建时,可以构建两种不同的镜像。一个是实际运行时使用的镜像,另外一个是包含了相关工具的调试镜像。当出现问题时,可以更新应用的部署,以调试镜像进行替换。这种做法的好处是提供了很大的灵活性,可以兼顾不同的场景。坏处是实现起来比较复杂,并且新运行的容器不一定可以重现之前的问题。
第三个需要考虑的方面是对包安装工具的处理。大部分的基础镜像都包含了系统自带的包管理工具,比如 apt
或 yum
。包管理工具的好处是方便了镜像的创建。
下面的镜像使用 apt
安装了 curl
和 jq
两个工具。
FROM ubuntu
RUN apt-get update && apt-get install -y curl jq
一个需要注意的问题是处理包安装之后的附加文件。以 apt
为例,可以在安装结束之后删除相关的附加文件,从而进一步减少镜像的尺寸。
FROM ubuntu
RUN apt-get update && apt-get install -y \
curl \
jq \
&& rm -rf /var/lib/apt/lists/*
对比上面两个 Dockerfile
,由 apt
产生的层的尺寸会从 49MB 下降到 17MB。
镜像的拉取和推送
在优化镜像的拉取和推送速度时,最重要的因素是层的组织。由于每个层都是不可变的,每次拉取和推送都只会处理变化的层。举例来说,如果某个镜像中包含了 5 个层,并且已经推送到了注册中心。如果仅有第 3 个层发生改变,那么只需要推送这个层即可,其他层不需要推送。在镜像拉取时,只会拉取本地不存在的层。
在对镜像的层进行组织时,最重要的原则是把变化频率相同的文件组织在同一个层。这样可以尽可能的避免层的改动。以 Java 应用为例,应用的镜像中的内容可以分成如下三类:
- JDK 或 JRE
- 应用依赖的第三方库
- 应用自身的代码
很明显地,在镜像的多次构建中,JDK 或 JRE 改动的频率极低;应用依赖的第三方库的改动相对较低;而应用自身的代码改动最频繁。按照这样的方式,可以把镜像划分成 3 个层。在大部分时候,只需要推送应用代码对应的层即可。另外两个层可以保持不变,不需要推送。
如果把应用依赖的第三方库和应用自身的代码放在同一个层,在每次构建时,需要推送的层中总是会包含全部的第三方库的内容。这就增大了所需要的存储空间,同时降低了推送和拉取的速度。
镜像的构建
镜像的构建一般由持续集成服务器来完成。相对于前两个优化方向来说,镜像的构建时间只会影响应用的构建时间,重要性相对较低。镜像构建的时间取决于使用的构建工具。以 Docker 为例,下面是一些优化的建议:
- 充分的利用构建时的缓存,可以复用之前构建的层。当
Dockerfile
发生变化时,构建可以从发生变化的行开始往下进行。 - 使用
.dockerignore
来忽略不会出现在镜像中的文件。这些被忽略的文件不会作为构建上下文传递给 Docker,可以提高构建速度。 - 尽可能的减少层的数量。层的数量的减少不应该违背上一节中介绍的层的组织原则。
关于 OCI 镜像的优化的相关介绍就到这里。