OCI 容器镜像的优化

OCI 容器镜像在云原生应用的开发中起着非常重要的作用。如何优化 OCI 容器镜像也是一个重要的话题。一般来说,OCI 容器镜像的优化有三个方向:

  1. 容器镜像的尺寸
  2. 容器镜像的拉取和推送速度
  3. 容器镜像的构建时间

这三个方向有各自不同的目标。容器镜像的尺寸影响的是所占用的存储空间;拉取速度会影响容器的启动时间;构建时间则会影响持续集成的速度。这三个方向的侧重点也有所不同。

容器镜像的尺寸

容器镜像的尺寸的最重要的决定因素是所要运行的应用。在很多时候,减少镜像的尺寸只能从应用自身入手。以 Java 应用为例,有些以传递依赖的方式引入的第三方库,对应用来说可能是不需要的。删除这些第三方库,可以减少镜像的尺寸。通过 GraalVM 创建 Java 应用的原生可执行文件,又可以进一步的减少镜像尺寸。

除了对应用的修改之外,另外一个可行的做法是删除镜像中不必要的内容。最佳的情况是镜像中只包含应用运行时必需的内容。

第一个需要考虑的方面是选择适合的基础镜像。比如,基于 Alpine Linux 的镜像的尺寸,就比基于 Ubuntu 的镜像的尺寸要小得多。同样是 Java 相关的镜像,JDK 和 JRE 镜像的尺寸也大不相同。以 Eclipse Temurin 的 OpenJDK 11 相关的镜像为例,下表给出了相关镜像的尺寸。最大的镜像的尺寸是最小的镜像的尺寸的五倍。

镜像标签尺寸(MB)
11-jdk-alpine186.92
11-jre-alpine43.79
11-jdk-focal235.73
11-jre-focal92.11

很明显,如果 JRE 的 Alpine 镜像能满足需求,它是最好的选择。

第二个需要考虑的方面是对辅助工具的处理。在镜像中,除了应用自身之外,通常还会添加一些辅助工具。这些工具的主要作用是为了调试。比如,curljq 是常用的工具。举例来说,如果在运行时发现应用无法调用第三方服务,在调试时,可以连接到运行时的容器,并使用 curl 工具来进行测试。这样可以更快的定位问题。可以有几种不同的解决方案:

  • 直接把 curl 作为应用镜像的一部分。这种做法的好处是使用简单,当需要时直接可用。坏处是增加了镜像的尺寸。这个工具实际上被使用的频率可能非常低,造成了不必要的浪费。
  • 辅助工具也可以在需要使用时再安装。基础镜像一般都提供了包管理工具,比如 apt,可以在运行时安装工具。这种做法的好处是节省了镜像的空间。坏处是使用起来比较麻烦,而且在运行时不一定具备安装工具的条件。比如,容器可能运行在隔离的网络内部,无法访问外部网络;也可能容器以非 root 用户来运行,不具备安装工具的权限。
  • 为应用调试提供专门的镜像。在构建时,可以构建两种不同的镜像。一个是实际运行时使用的镜像,另外一个是包含了相关工具的调试镜像。当出现问题时,可以更新应用的部署,以调试镜像进行替换。这种做法的好处是提供了很大的灵活性,可以兼顾不同的场景。坏处是实现起来比较复杂,并且新运行的容器不一定可以重现之前的问题。

第三个需要考虑的方面是对包安装工具的处理。大部分的基础镜像都包含了系统自带的包管理工具,比如 aptyum。包管理工具的好处是方便了镜像的创建。

下面的镜像使用 apt 安装了 curljq 两个工具。

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 镜像的优化的相关介绍就到这里。

版权所有 © 2024 灵动代码