如果您使用基于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。
# 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 看看它有多大。
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的选项。
让我们首先看一下通用的Docker镜像。
# 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"]
让我们快速浏览一下这个文件。
现在让我们构建这个新的Dockerfile并检查镜像大小。
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,几乎是原始镜像大小的三分之一,并且包含所有模块。我们可以通过仅包含所需的模块来进一步缩减大小吗?是的,但主要问题是如何确定应用程序正常运行需要哪些模块。
我们可以使用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。
# 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并检查镜像大小。
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,接近原始镜像大小的四分之一,这不是更好吗?
从之前的结果中我们知道,精简JRE优于通用JRE。然而,Slim JRE又一个小缺陷。如果应用程序仍在开发中,我们可能需要频繁更改Dockerfile。此外,由于我们正在更改 Dockerfile,Docker 可能无法重用所有层Dockerfile。
如果您继续依赖使用精简的 JRE,我们至少可以自动化上述过程,让我们的生活变得更轻松一些。要自动化该过程,请参阅以下 GitHub 要点:
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"]
正如您所看到的,我们能够以最小的努力将图像尺寸缩小近三倍。我们有两种选择。
由您决定哪个 JRE 最适合您的应用程序。但是,无论使用哪种选项,您都可以大幅减小镜像大小。