如何将JVM Docker镜像大小减少至少60%

2023-09-11 10:56:31 浏览数 (1)

背景

如果您使用基于JVM的语言(Java、Kotlin、Scala等)已有一段时间,你可能已经注意到,从Java 11开始,Java运行时环境(JRE)不再有单独的发行版。由于这一决定,许多Java开发工具包(JDK)Docker镜像分发商(例如:OpenJDK、Amazon Correto等)不提供JRE作为单独的Docker镜像,使用这些镜像整体Docker镜像大小约为360MB,而实际应用程序Jar包大小约为26MB。在我看来,整个Docker镜像的大小太大了,应用减小它,以便为每个将使用该Docker镜像的人节省空间和网络带宽。现在,让我们看看如何大幅减小Docker镜像的大小。

这个问题的根源

Java平台模块系统(JPMS)是随Java 9引入的。我们可以使用 JPMS 创建适合特定应用程序的自己的自定义 JRE。例如,如果应用程序不使用音频、图像或JavaBeans相关功能,我们可以 java.desktop完全删除该模块以释放 Docker 映像中的空间。如前所述,从Java 11开始,不再有单独的JRE发行版。这意味着即使我们只想运行一个简单的基于JVM的应用程序,我们也必须安装整个JDK。这是由于Java 9中引入的模块化。主要理念是,每个人都应用能够创建自己的JRE,而不是提供满足每个人需求的通用JRE。许多JDK镜像提供商都遵循相同的理念,省略JRE发行版。不幸的是,使用此类镜像会显着增加Docker镜像的大小。为了更好地理解这个问题,让我们看一下运行一个简单的基于JVM的应用程序所需的基本Dockerfile。

代码语言:javascript复制
# greetings.Dockerfile

FROM amazoncorretto:17-alpine
EXPOSE 8080
COPY ./greetings/build/libs/greetings.jar /app/
WORKDIR /app

CMD ["java", "-jar", "greetings.jar"]

我们在这里使用它amazoncorretto:17-alpine作为基础镜像,并将应用程序Jar包复制到其中。最后我们运行Jar包 让我们运行这个 Dockerfile 看看它有多大。

代码语言:javascript复制
ls -lh greetings/build/libs/greetings.jar | awk '{print $5, $9}'
# 26M greetings/build/libs/greetings.jar

docker build -t greetings:jdk -f greetings.Dockerfile .

docker image ls | grep greetings

# The output looks like following
# greetings    jdk     ca39786a6f62   2 hours ago      361MB

也就是说,361MB的镜像对于26MB的Jar包来说相当大,不是吗?那么,我们怎样才能让它变小呢?

解决方案

除了模块化之外,Java 9还包含一个名为jlink的工具。该工具的主要目的是帮助我们根据需要创建自定义JRE。该工具提供了一些用于微调JRE和所需模块的选项,但它还提供了创建包含所有模块的通用JRE的选项。

自定义JRE

让我们首先看一下通用的Docker镜像。

代码语言:javascript复制
# greetings.Dockerfile

FROM amazoncorretto:17-alpine as corretto-jdk

# required for strip-debug to work
RUN apk add --no-cache binutils

# Build small JRE image
RUN jlink 
         --add-modules ALL-MODULE-PATH 
         --strip-debug 
         --no-man-pages 
         --no-header-files 
         --compress=2 
         --output /jre

FROM alpine:latest
ENV JAVA_HOME=/jre
ENV PATH="${JAVA_HOME}/bin:${PATH}"

COPY --from=corretto-jdk /jre $JAVA_HOME

EXPOSE 8080
COPY ./greetings/build/libs/greetings.jar /app/
WORKDIR /app

CMD ["java", "-jar", "greetings.jar"]

让我们快速浏览一下这个文件。

  • 在本例中,我们使用了 Docker 多阶段构建。
  • 我们amazoncorretto:17-alpine在第一阶段使用相同的 Docker 镜像作为基础镜像。
  • 接下来,我们安装binutils该jlink工具所需的 。jlink然后使用该工具创建自定义 JRE。该命令最重要的部分是--add-modules ALL-MODULE-PATH,它将所有模块添加到 JRE。在Oracle 文档页面上,您可以了解有关所有选项的更多信息。
  • 该alpine:latest镜像用作第二阶段的基础镜像。
  • 然后我们复制上一阶段新创建的自定义 JRE。
  • 最后,我们正在运行应用程序 jar 文件。

现在让我们构建这个新的Dockerfile并检查镜像大小。

代码语言:javascript复制
docker build -t greetings:jre -f greetings.Dockerfile .

docker image ls | grep greetings

# The output looks like following
# greetings    jre    d5f20dab834c   2 hours ago      123MB

也就是说,新的镜像大小只有123MB,几乎是原始镜像大小的三分之一,并且包含所有模块。我们可以通过仅包含所需的模块来进一步缩减大小吗?是的,但主要问题是如何确定应用程序正常运行需要哪些模块。

瘦身JRE

我们可以使用jdeps命令来确定所需的模块。首次在Java 8 jdeps中引入,用于检查应用程序中的依赖关系。此外。还可以发现每个库依赖项使用的每个Java模块。在运行命令之前,我们必须提取Jar文件才能使其正常运行。

代码语言:javascript复制
unzip ./greetings/build/libs/greetings.jar -d temp

jdeps 
  --print-module-deps 
  --ignore-missing-deps 
  --recursive 
  --multi-release 17 
  --class-path="./temp/BOOT-INF/lib/*" 
  --module-path="./temp/BOOT-INF/lib/*" 
  ./greetings/build/libs/greetings.jar

# The output will look like following
# java.base,java.compiler,java.desktop,java.instrument,java.management,java.naming,java.prefs,java.rmi,java.scripting,java.security.jgss,java.sql,jdk.httpserver,jdk.jfr,jdk.unsupported

rm -rf temp

我们首先将应用程序 Jar包提取到临时目录,然后运行jdeps带有少量配置选项的命令。最后,我们删除临时目录。

注意:jdeps将无法打印Reflection. 例如,如果应用程序包含spring security,我们需要手动添加jdk.crypto.ec和jdk.crypto.cryptoki模块

现在我们将替换ALL-MODULE-PATH为 打印的列表jdeps。

代码语言:javascript复制
# greetings.Dockerfile

FROM amazoncorretto:17-alpine as corretto-jdk

# required for strip-debug to work
RUN apk add --no-cache binutils

# Build small JRE image
RUN jlink 
         --verbose 
         --add-modules java.base,java.compiler,java.desktop,java.instrument,java.management,java.naming,java.prefs,java.rmi,java.scripting,java.security.jgss,java.sql,jdk.httpserver,jdk.jfr,jdk.unsupported,jdk.crypto.ec,jdk.crypto.cryptoki 
         --strip-debug 
         --no-man-pages 
         --no-header-files 
         --compress=2 
         --output /jre

FROM alpine:latest
ENV JAVA_HOME=/jre
ENV PATH="${JAVA_HOME}/bin:${PATH}"

COPY --from=corretto-jdk /jre $JAVA_HOME

EXPOSE 8080
COPY ./greetings/build/libs/greetings.jar /app/
WORKDIR /app

CMD ["java", "-jar", "greetings.jar"]

现在让我们构建这个新的Dockerfile并检查镜像大小。

代码语言:javascript复制
docker build -t greetings:slimjre -f greetings.Dockerfile .

docker image ls | grep greetings

# The output looks like following
# greetings    slimjre    450a64815cb3   46 minutes ago   89.8MB

新镜像大小仅有90MB,接近原始镜像大小的四分之一,这不是更好吗?

Slim JRE 的问题以及如何修复它

从之前的结果中我们知道,精简JRE优于通用JRE。然而,Slim JRE又一个小缺陷。如果应用程序仍在开发中,我们可能需要频繁更改Dockerfile。此外,由于我们正在更改 Dockerfile,Docker 可能无法重用所有层Dockerfile。

自动化 Slim JRE

如果您继续依赖使用精简的 JRE,我们至少可以自动化上述过程,让我们的生活变得更轻松一些。要自动化该过程,请参阅以下 GitHub 要点:

代码语言:javascript复制
FROM amazoncorretto:17-alpine as corretto-deps

COPY ./greetings/build/libs/greetings.jar /app/

RUN unzip /app/greetings.jar -d temp &&  
    jdeps  
      --print-module-deps 
      --ignore-missing-deps 
      --recursive 
      --multi-release 17 
      --class-path="./temp/BOOT-INF/lib/*" 
      --module-path="./temp/BOOT-INF/lib/*" 
      /app/greetings.jar > /modules.txt

FROM amazoncorretto:17-alpine as corretto-jdk

COPY --from=corretto-deps /modules.txt /modules.txt

# hadolint ignore=DL3018,SC2046
RUN apk add --no-cache binutils && 
    jlink 
     --verbose 
     --add-modules "$(cat /modules.txt),jdk.crypto.ec,jdk.crypto.cryptoki" 
     --strip-debug 
     --no-man-pages 
     --no-header-files 
     --compress=2 
     --output /jre

# hadolint ignore=DL3007
FROM alpine:latest
ENV JAVA_HOME=/jre
ENV PATH="${JAVA_HOME}/bin:${PATH}"

COPY --from=corretto-jdk /jre $JAVA_HOME

EXPOSE 8080
COPY ./greetings/build/libs/greetings.jar /app/
WORKDIR /app

CMD ["java", "-jar", "greetings.jar"]

小结

正如您所看到的,我们能够以最小的努力将图像尺寸缩小近三倍。我们有两种选择。

  • Slim JRE,镜像尺寸极小,仅包含所需的Java模块,可能需要频繁更新dockerfile,并且Docker可能无法跨项目复用层。
  • 通用JRE,通用JRE的镜像大小比slim JRE稍大,但包含了所有的Java模块。

由您决定哪个 JRE 最适合您的应用程序。但是,无论使用哪种选项,您都可以大幅减小镜像大小。

0 人点赞