当前位置:Java -> 减小 JVM Docker 镜像大小的方法

减小 JVM Docker 镜像大小的方法

这篇博客主要讨论了如何优化JVM Docker镜像的大小。它探讨了诸如多阶段构建、jlink、jdeps以及尝试使用基础镜像等各种技术。通过实施这些优化,部署可以更快速,资源使用也可以得到优化。

问题

自从Java 11开始,没有提供预打包的JRE。因此,没有任何优化的基本Docker文件可能会导致镜像大小较大。在没有提供JRE的情况下,有必要探索技术和优化方式来减小JVM Docker镜像的大小。

现在,让我们看一下我们应用程序的最简单的Docker文件,并找出其中的问题。我们将在所有示例中使用的项目是Spring Petclinic

我们项目的最简单Docker文件如下:

注意:不要忘记构建您的JAR文件。

FROM eclipse-temurin:17
VOLUME /tmp
COPY target/spring-petclinic-3.1.0-SNAPSHOT.jar app.jar


在构建了我们的项目的JAR文件之后,让我们构建我们的Docker镜像并比较我们的JAR文件和创建的Docker镜像的大小。

docker build -t spring-pet-clinic/jdk -f Dockerfile .
docker image ls spring-pet-clinic/jdk

# REPOSITORY              TAG       IMAGE ID       CREATED          SIZE
# spring-pet-clinic/jdk   latest    3dcd0ab89c3d   23 minutes ago   465MB


如果我们看一下SIZE列,可以看到我们的Docker镜像的大小是465MB!你可能会觉得这很多,但也许是因为我们的JAR文件很大?

为了验证这一点,让我们使用以下命令查看我们的JAR文件的大小:

ls -lh target/spring-petclinic-3.1.0-SNAPSHOT.jar | awk '{print $9, $5}'

# target/spring-petclinic-3.1.0-SNAPSHOT.jar 55M


根据我们的命令输出,可以看到我们的JAR文件大小只有55MB。如果将其与构建的Docker镜像的大小进行比较,我们的JAR文件几乎要小9倍!让我们继续分析原因以及如何减小JAR文件的大小。

为什么Docker镜像如此庞大,以及如何减小?

在我们继续优化Docker镜像之前,我们需要找出究竟是什么造成了它相对较大。为此,我们将使用一种名为Dive的工具,该工具用于探索Docker镜像、层内容,以及发现收缩Docker/OCI镜像大小的方法。

要安装Dive,请按照他们README中的指南进行:

现在,让我们探索层的命令来查找我们的Docker镜像为何如此大:dive spring-pet-clinic/jdk(使用自己的Docker镜像名称,而不是spring-pet-clinic/jdk)。

Docker image

它的输出可能会让人感到有些不知所措,但不用担心,我们将一起探索其输出。对于我们的目的,我们主要感兴趣的是左上角,即我们Docker镜像的层。我们可以使用“箭头”按钮之间导航到不同的层。现在,让我们找出我们的Docker镜像由哪些层组成。

output

请记住,这些是从我们基本Docker文件构建的Docker镜像的层。

  1. 第一层是我们的操作系统,默认为Ubuntu。
  2. 在接下来的一层中,安装了tzdata、curl、wget、locales以及一些其他不同的工具,占用了50MB!
  3. 第三层,如上面的屏幕截图所示,是整个Eclipse Temurin 17 JDK,占用了279MB,相当大。
  4. 最后一层是我们构建的JAR,占用了58MB。

现在我们了解了我们的Docker镜像包含的内容,可以看到我们的Docker镜像的大部分包括整个JDK以及时区、语言环境和其他不必要的工具。

对于我们的Docker镜像的第一个优化是使用Java 9中附带的jlink工具及模块化。使用jlink,我们可以创建一个包含必要组件的自定义Java运行时,从而实现更小的最终镜像。

现在,让我们看一下集成了jlink工具的新Dockerfile,理论上,该文件应该比之前的更小。

# Example of custom Java runtime using jlink in a multi-stage container build
FROM eclipse-temurin:17 as jre-build

# Create a custom Java runtime
RUN $JAVA_HOME/bin/jlink \
         --add-modules ALL-MODULE-PATH \
         --strip-debug \
         --no-man-pages \
         --no-header-files \
         --compress=2 \
         --output /javaruntime

# Define your base image
FROM debian:buster-slim
ENV JAVA_HOME=/opt/java/openjdk
ENV PATH "${JAVA_HOME}/bin:${PATH}"
COPY --from=jre-build /javaruntime $JAVA_HOME

# Continue with your application deployment
RUN mkdir /opt/app
COPY target/spring-petclinic-3.1.0-SNAPSHOT.jar /opt/app/app.jar
CMD ["java", "-jar", "/opt/app/app.jar"]


为了了解我们新的Dockerfile是如何工作的,让我们仔细分析一下:

  • 在这个Dockerfile中,我们使用了多阶段Docker构建,它由2个阶段组成。
  • 对于第一个阶段,我们使用了与之前Dockerfile中相同的基础镜像。
  • 此外,我们使用jlink工具创建了一个自定义JRE,包括所有Java模块,使用—add-modules ALL-MODULE-PATH
  • 第二阶段使用了debian:buster-slim基础镜像,并为JAVA_HOMEPATH设置了环境变量。它将在第一阶段创建的自定义JRE复制到镜像中。
  • 然后,Dockerfile创建一个应用程序目录,将应用程序JAR文件复制到其中,并指定在容器启动时运行Java应用程序的命令。

现在让我们构建我们的容器镜像,并找出它变小了多少。

docker build -t spring-pet-clinic/jlink -f Dockerfile_jlink .
docker image ls spring-pet-clinic/jlink

# REPOSITORY                TAG       IMAGE ID       CREATED       SIZE
# spring-pet-clinic/jlink   latest    e7728584dea5   1 hours ago   217MB


我们的新容器镜像大小为217MB,是之前的两倍小。

使用Java依赖分析工具(jdeps)进一步减小容器镜像大小

假如我告诉你我们的容器镜像大小可以再变小?当与jlink配对使用时,你还可以使用Java依赖分析工具(jdeps),该工具在Java 8中首次引入,用于了解应用程序和库的静态依赖关系。

在我们的先前示例中,对于jlink的—add-modules参数,我们设置了ALL-MODULE-PATH,它添加了我们自定义JRE中所有现有的Java模块,显然,我们不需要包含每个模块。这样,我们可以使用jdeps分析项目的依赖关系并删除任何未使用的内容,进一步减小镜像大小。让我们看看如何在我们的Dockerfile中使用jdeps:

# Example of custom Java runtime using jlink in a multi-stage container build
FROM eclipse-temurin:17 as jre-build

COPY target/spring-petclinic-3.1.0-SNAPSHOT.jar /app/app.jar
WORKDIR /app

# List jar modules
RUN jar xf app.jar
RUN jdeps \
    --ignore-missing-deps \
    --print-module-deps \
    --multi-release 17 \
    --recursive \
    --class-path 'BOOT-INF/lib/*' \
    app.jar > modules.txt

# Create a custom Java runtime
RUN $JAVA_HOME/bin/jlink \
         --add-modules $(cat modules.txt) \
         --strip-debug \
         --no-man-pages \
         --no-header-files \
         --compress=2 \
         --output /javaruntime

# Define your base image
FROM debian:buster-slim
ENV JAVA_HOME=/opt/java/openjdk
ENV PATH "${JAVA_HOME}/bin:${PATH}"
COPY --from=jre-build /javaruntime $JAVA_HOME

# Continue with your application deployment
RUN mkdir /opt/server
COPY --from=jre-build /app/app.jar /opt/server/
CMD ["java", "-jar", "/opt/server/app.jar"]


即使不深入细节,你也可以看到我们的Dockerfile变得更大了。现在让我们分析每个部分及其职责:

  • 我们仍然使用多阶段Docker构建。
  • 复制我们构建好的Java应用程序,并将WORKDIR设置为/app
  • 解压JAR文件,使其内容对jdeps工具可见。
  • 第二个RUN指令在提取的JAR文件上运行jdeps工具,分析其依赖关系并创建所需的Java模块列表。以下是每个选项的作用:
    1. --ignore-missing-deps:忽略任何缺失的依赖项,允许分析继续。
    2. --print-module-deps:指定分析应该打印模块依赖项。
    3. --multi-release 17:指示应用程序JAR与多个Java版本兼容,在我们的情况下是Java 17。
    4. --recursive:执行递归分析,识别所有级别的依赖关系。
    5. --class-path 'BOOT-INF/lib/*':为分析定义类路径,指示“jdeps”在JAR文件的“BOOT-INF/lib”目录中查找。
    6. app.jar > modules.txt:重定向“jdeps”命令的输出到名为“modules.txt”的文件,其中包含应用程序所需的Java模块列表。
  • 然后,我们将ALL-MODULE-PATH值替换为$(cat modules.txt),以仅包括必需的模块
  • # 定义你的基础镜像部分与之前的Dockerfile中相同。
  • # 继续部署你的应用程序 被修改为从上一个阶段复制JAR文件。
docker build -t spring-pet-clinic/jlink_jdeps -f Dockerfile_jdeps .
docker image ls spring-pet-clinic/jlink_jdeps

# REPOSITORY                      TAG       IMAGE ID       CREATED        SIZE
# spring-pet-clinic/jlink_jdeps   latest    d24240594f1e   3 hours ago   184MB


因此,通过仅使用我们需要运行应用程序的模块,我们将容器镜像的大小减小了33MB,虽然不多,但还是不错的。

结论

让我们再次使用Dive来查看我们的Docker镜像在优化后的变化。

在这种情况下,我们没有使用整个JDK,而是使用jlink工具构建了我们的自定义JRE,并使用了debian-slim基础镜像。这显著减小了我们的镜像大小。并且,正如你所见,我们没有不必要的东西,比如时区、地区设置、大型操作系统和整个JDK。我们仅包含我们使用和需要的内容。

Dockerfile_jlink

Dockerfile_jlink

在这里,我们甚至进一步地将只使用的Java模块传递给我们的JRE,使构建的JRE变得更小,从而减小了整个最终镜像的大小。

Dockerfile_jdeps

Dockerfile_jdeps

总的来说,减小JVM Docker镜像的大小可以显著优化资源使用和加快部署速度。采用多阶段构建、jlink、jdeps等技术,并尝试使用不同的基础镜像,都可以产生显著的影响。尽管在某些情况下,大小的减小可能看起来微不足道,但累积效应可能是显著的,特别是在多个容器同时运行的环境中。因此,在任何应用程序开发和部署过程中,优化Docker镜像应该是一个关键考虑因素。

推荐阅读: 23.Redis是如何保证主从服务器一致处于连接状态以及命令是否丢失?

本文链接: 减小 JVM Docker 镜像大小的方法