当前位置:Java -> 减小 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镜像之前,我们需要找出究竟是什么造成了它相对较大。为此,我们将使用一种名为Dive的工具,该工具用于探索Docker镜像、层内容,以及发现收缩Docker/OCI镜像大小的方法。
要安装Dive,请按照他们README中的指南进行:
现在,让我们探索层的命令来查找我们的Docker镜像为何如此大:dive spring-pet-clinic/jdk
(使用自己的Docker镜像名称,而不是spring-pet-clinic/jdk
)。
它的输出可能会让人感到有些不知所措,但不用担心,我们将一起探索其输出。对于我们的目的,我们主要感兴趣的是左上角,即我们Docker镜像的层。我们可以使用“箭头”按钮之间导航到不同的层。现在,让我们找出我们的Docker镜像由哪些层组成。
请记住,这些是从我们基本Docker文件构建的Docker镜像的层。
现在我们了解了我们的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是如何工作的,让我们仔细分析一下:
—add-modules ALL-MODULE-PATH
debian:buster-slim
基础镜像,并为JAVA_HOME
和PATH
设置了环境变量。它将在第一阶段创建的自定义JRE复制到镜像中。现在让我们构建我们的容器镜像,并找出它变小了多少。
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,是之前的两倍小。
假如我告诉你我们的容器镜像大小可以再变小?当与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变得更大了。现在让我们分析每个部分及其职责:
WORKDIR
设置为/app
。--ignore-missing-deps
:忽略任何缺失的依赖项,允许分析继续。--print-module-deps
:指定分析应该打印模块依赖项。--multi-release 17
:指示应用程序JAR与多个Java版本兼容,在我们的情况下是Java 17。--recursive
:执行递归分析,识别所有级别的依赖关系。--class-path 'BOOT-INF/lib/*'
:为分析定义类路径,指示“jdeps”在JAR文件的“BOOT-INF/lib”目录中查找。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。我们仅包含我们使用和需要的内容。
在这里,我们甚至进一步地将只使用的Java模块传递给我们的JRE,使构建的JRE变得更小,从而减小了整个最终镜像的大小。
总的来说,减小JVM Docker镜像的大小可以显著优化资源使用和加快部署速度。采用多阶段构建、jlink、jdeps等技术,并尝试使用不同的基础镜像,都可以产生显著的影响。尽管在某些情况下,大小的减小可能看起来微不足道,但累积效应可能是显著的,特别是在多个容器同时运行的环境中。因此,在任何应用程序开发和部署过程中,优化Docker镜像应该是一个关键考虑因素。
推荐阅读: 23.Redis是如何保证主从服务器一致处于连接状态以及命令是否丢失?
本文链接: 减小 JVM Docker 镜像大小的方法