如果您使用基于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文件才能使其正常运行。
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 最适合您的应用程序。但是,无论使用哪种选项,您都可以大幅减小镜像大小。