这篇博客聚焦于优化JVM Docker镜像的大小。它探讨了多阶段构建、jlink、jdeps以及尝试不同基础镜像等各种技术。通过实施这些优化,部署可以更快,资源使用也可以得到优化。
问题
自Java 11起,不再提供预打包的JRE。因此,基本的Dockerfile如果没有任何优化,可能导致镜像大小较大。在没有提供JRE的情况下,有必要探索技术和优化方法来减小JVM Docker镜像的大小。
现在,让我们看一下我们应用的最简单版本的Dockerfile,并看看其中存在的问题。我们将在所有示例中使用的项目是Spring Petclinic。
我们项目的最简单Dockerfile如下:
注意:不要忘记构建你的JAR文件。
FROM eclipse-temurin:17
VOLUME /tmp
COPY target/spring-petclinic-3.1.0-SNAPSHOT.jar app.jar
在构建了我们项目的JAR文件之后,让我们构建我们的Dockerfile镜像,并比较我们的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文件几乎小了九倍!接下来,让我们继续分析原因以及如何使其更小。
为什么Docker镜像会很大,以及如何减小它们的大小?
在我们继续优化我们的Docker镜像之前,我们需要找出到底是什么导致它相对较大。为此,我们将使用一个名为Dive的工具,该工具用于探索Docker镜像、层内容,并发现缩小Docker/OCI镜像大小的方法。
要安装Dive,请按照他们README中的指南进行操作:
现在,让我们通过使用以下命令来探索层,找出为什么我们的Docker镜像如此大:dive spring-pet-clinic/jdk(在spring-pet-clinic/jdk处使用你的Docker镜像名称)。
它的输出可能会让人有些不知所措,但不用担心,我们将一起探索它的输出。对于我们的目的,我们主要感兴趣的是左上角的部分,即我们Docker镜像的层。我们可以使用“箭头”按钮在层之间进行导航。现在,让我们找出我们的Docker镜像由哪些层组成。
请记住,这些是从我们基本的Dockerfile构建的Docker镜像的层。
- 第一层是我们的操作系统,默认情况下是Ubuntu。
- 在接下来的一层中,它安装了tzdata、curl、wget、locales以及一些其他不同的工具,占用了50MB!
- 第三层,正如你从上面的截图中看到的那样,是我们整个Eclipse Temurin 17 JDK,占用了279MB,相当大。
- 最后一层是我们构建的JAR文件,占用了58MB。
现在我们了解了我们的Docker镜像由什么组成,我们可以看到我们的Docker镜像的一个很大的部分包括整个JDK以及诸如时区、区域设置和不同的实用工具等不必要的内容。
我们Docker镜像的第一个优化是使用Java 9中包含的jlink工具以及模块化。使用jlink,我们可以创建一个包含只有必要组件的自定义Java运行时,从而得到更小的最终镜像。
现在,让我们看一下我们新的Dockerfile,它包含了jlink工具,理论上应该比之前的更小。
# 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_HOME和PATH的环境变量。它将第一阶段创建的自定义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模块的列表。以下是每个选项的作用:--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模块列表。
- 然后,我们将—add-modules jlink参数的ALL-MODULE-PATH值替换为$(cat modules.txt),以仅包含必要的模块
- # Define your base image 部分与之前的Dockerfile中的相同。
- # Continue with your application deployment 被修改为从上一个阶段复制出JAR文件。
唯一剩下的事情就是看看我们最新的Dockerfile使容器镜像缩小了多少:
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
在这里,我们甚至进一步传递了我们的JRE所使用的Java模块,使构建的JRE更小,从而减小了整个最终镜像的大小。
Dockerfile_jdeps
总之,减小JVM Docker镜像的大小可以显著优化资源使用和加快部署速度。采用多阶段构建、jlink、jdeps以及尝试不同基础镜像等技术可以产生实质性的差异。尽管在某些情况下,大小的减小可能看起来微不足道,但累积效应可能是显著的,特别是在运行多个容器的环境中。因此,在任何应用程序开发和部署过程中,优化Docker镜像应该是一个关键的考虑因素。
Tags:docker 强制删除镜像