亚马逊AWS官方博客

使用 GraalVM 为 Java 应用程序构建最少的 Docker 镜像

优化 Docker 镜像的大小有诸多好处。其中一个好处是能加快部署速度。如果您的应用程序需要快速向外扩展以响应意外的突发流量,那么这一点将非常重要。在这篇博文中,我将向您展示通过一种有趣的方法来优化 Java 应用程序的 Docker 镜像,这也有助于缩短启动时间。所举的例子基于我几个月前发布的另一篇博文 Reactive Microservices Architecture on AWS

Java 应用程序的工作原理是怎样的?

Java 应用程序使用 Java 11 实现,并使用 Vert.x 3.6 作为主框架。Vert.x 是具有反应性和非阻塞特性的事件驱动型 Polyglot 框架,其作用是实施微服务。它在 Java 虚拟机 (JVM) 上运行,使用低级 I/O 库 Netty。该应用程序包含五个不同的 verticle,涵盖业务逻辑的不同方面。

为了构建此应用程序,我使用了采用不同配置文件的 Maven。第一个配置文件(默认配置文件)使用“标准”构建以创建 Uber JAR(一个包含所有依赖项的独立应用程序)。第二个配置文件使用 GraalVM 编译本地镜像。标准构建使用 jlink 来构建一个具有有限模块集的自定义 Java 运行时。(使用命令行工具 jlink 可关联一系列模块及其可传递依赖项,以创建运行时镜像。)

使用 jlink 构建自定义 JDK 分配

我们需要关注 JDK 9 中包含的 Java 平台模块功能 (JPMS),也称为 Project Jigsaw。该功能是为构建仅包含必要依赖项的模块化 Java 运行时而开发的。对于此应用程序,您只需要数量有限的一组模块(可在构建过程中指定)。在构建前的准备工作中,请下载 Amazon Corretto 11 并解压,然后删除 JDK 附带的 src.zip 文件等不必要的文件。为帮助读者更好地理解相关内容,以下部分使用了多阶段构建,并单独介绍了构建的不同部分。

第 1 步:构建自定义运行时模块

构建过程的第一步是构建自定义运行时(只包含运行应用程序所必需的几个模块),然后将结果写入 /opt/minimal

FROM debian:9-slim AS builder
LABEL maintainer="Sascha Möllering <smoell@amazon.de>"

# First step: build java runtime module
RUN set -ex && \
    apt-get update && apt-get install -y wget unzip && \
    wget https://d3pxv6yz143wms.cloudfront.net/11.0.3.7.1/amazon-corretto-11.0.3.7.1-linux-x64.tar.gz -nv && \
    mkdir -p /opt/jdk && \
    tar zxvf amazon-corretto-11.0.3.7.1-linux-x64.tar.gz -C /opt/jdk --strip-components=1 && \
    rm amazon-corretto-11.0.3.7.1-linux-x64.tar.gz && \
    rm /opt/jdk/lib/src.zip

RUN /opt/jdk/bin/jlink \
    --module-path /opt/jdk/jmods \
    --verbose \
    --add-modules java.base,java.logging,java.naming,java.net.http,java.se,java.security.jgss,java.security.sasl,jdk.aot,jdk.attach,jdk.compiler,jdk.crypto.cryptoki,jdk.crypto.ec,jdk.internal.ed,jdk.internal.le,jdk.internal.opt,jdk.naming.dns,jdk.net,jdk.security.auth,jdk.security.jgss,jdk.unsupported,jdk.zipfs \
    --output /opt/jdk-minimal \
    --compress 2 \
    --no-header-files

第 2 步:将自定义运行时复制到目标镜像

接下来,将新创建的自定义运行时从构建映像复制到实际目标镜像。在这一步,您要再次使用 debian:9-slim 作为基本镜像。复制最小运行时后,将 Java 应用程序复制到 /opt/,添加 Docker 运行状况检查,然后启动 Java 进程:

FROM debian:9-slim
LABEL maintainer="Sascha Möllering <smoell@amazon.de>"

COPY --from=builder /opt/jdk-minimal /opt/jdk-minimal

ENV JAVA_HOME=/opt/jdk-minimal
ENV PATH="$PATH:$JAVA_HOME/bin"

RUN mkdir /opt/app && apt-get update && apt-get install curl -y
COPY target/reactive-vertx-1.5-fat.jar /opt/app

HEALTHCHECK --interval=5s --timeout=3s --retries=3 \
  CMD curl -f http://localhost:8080/health/check || exit 1

EXPOSE 8080

CMD ["java", "-server", "-XX:+DoEscapeAnalysis", "-XX:+UseStringDeduplication", \
        "-XX:+UseCompressedOops", "-XX:+UseG1GC", \
        "-jar", "opt/app/reactive-vertx-1.5-fat.jar"]

使用 GraalVM 将 Java 编译为本机

GraalVM 是 Oracle 提供的一种开源、高性能 Polyglot 虚拟机。使用它可以提前编译本机镜像,以提高启动性能,并减少基于 JVM 的应用程序的内存消耗和文件大小。允许预先编译的框架称为 SubstrateVM

在以下部分,您可以看到 pom.xml 文件的相关片段。创建一个名为 native-image-fargate 的附加 Maven 配置文件,该文件使用 native-image-maven 插件在“包装”阶段将源代码编译为本机镜像:

<profile>
    <id>native-image-fargate</id>
    <build>
        <plugins>
            <plugin>
                <groupId>com.oracle.substratevm</groupId>
                <artifactId>native-image-maven-plugin</artifactId>
                <version>${graal.version}</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>native-image</goal>
                        </goals>
                        <phase>package</phase>
                    </execution>
                </executions>
                <configuration>
                    <imageName>${project.artifactId}</imageName>
                    <mainClass>${vertx.verticle}</mainClass>
                    <buildArgs>--enable-all-security-services -H:+ReportUnsupportedElementsAtRuntime --allow-incomplete-classpath</buildArgs>
                </configuration>
            </plugin>
        </plugins>
    </build>
</profile>

Docker 多阶段构建

您的目标是定义一个包含尽可能少相关项的可重现构建环境。为此,要创建一个使用 Docker 多阶段构建的自包含构建过程。

对于多阶段构建而言,一个值得关注的方面在于,您可以在 Dockerfile 中使用多个 FROM 语句。每条 FROM 指令可以使用不同的基本镜像,并开始构建的新阶段。您可以选择必需的文件并将其从一个阶段复制到另一个阶段,这非常有用,因为这样可以限制必须复制的文件数。使用此功能可以在一个阶段构建应用程序,并将已编译的构件和其他文件复制到目标镜像。

在以下部分,您可以看到构建的两个不同阶段。您的 Dockerfile(称为 Dockerfile-native)分为两部分:构建器镜像和目标镜像。

第一个代码示例显示了基于 graalvm-ce 的生成器镜像。在构建过程中,必须安装 Maven、设置一些环境变量,并将必要的文件复制到 Docker 镜像中。对于构建,您需要源代码和 pom.xml 文件。成功将文件复制到 Docker 镜像后,将使用配置文件 native-image-fargate 启动应用程序到可执行二进制文件的构建。当然,也可以使用 Maven 基本镜像并安装 GraalVM(整个构建过程会稍有不同)。

FROM oracle/graalvm-ce:1.0.0-rc16 AS build-aot

RUN yum update -y
RUN yum install wget -y
RUN wget https://www-eu.apache.org/dist/maven/maven-3/3.6.1/binaries/apache-maven-3.6.1-bin.tar.gz -P /tmp
RUN tar xf /tmp/apache-maven-3.6.1-bin.tar.gz -C /opt
RUN ln -s /opt/apache-maven-3.6.1 /opt/maven
RUN ln -s /opt/graalvm-ce-1.0.0-rc16 /opt/graalvm

ENV JAVA_HOME=/opt/graalvm
ENV M2_HOME=/opt/maven
ENV MAVEN_HOME=/opt/maven
ENV PATH=${M2_HOME}/bin:${PATH}
ENV PATH=${JAVA_HOME}/bin:${PATH}

COPY ./pom.xml ./pom.xml
COPY src ./src/

ENV MAVEN_OPTS='-Xmx6g'
RUN mvn -Dmaven.test.skip=true -Pnative-image-fargate clean package

现在,开始执行多阶段构建过程中第二部的操作:创建实际目标镜像。此镜像基于 debian:9-slim,并将两个环境变量设置为 TLS 特定设置(因为应用程序使用 TLS 与 Amazon Kinesis 数据流通信)。

FROM debian:9-slim
LABEL maintainer="Sascha Möllering <smoell@amazon.de>"

ENV javax.net.ssl.trustStore /cacerts
ENV javax.net.ssl.trustAnchors /cacerts

RUN apt-get update && apt-get install -y curl

COPY --from=build-aot target/reactive-vertx /usr/bin/reactive-vertx
COPY --from=build-aot /opt/graalvm/jre/lib/amd64/libsunec.so /libsunec.so
COPY --from=build-aot /opt/graalvm/jre/lib/security/cacerts /cacerts

HEALTHCHECK --interval=5s --timeout=3s --retries=3 \
  CMD curl -f http://localhost:8080/health/check || exit 1

EXPOSE 8080

CMD [ "/usr/bin/reactive-vertx" ]

构建目标镜像非常简单。运行以下命令:

docker build . -t <your_docker_repo>/reactive-vertx-native -f Dockerfile-native

要使用 Uber JAR 构建标准 Docker 镜像,请运行以下命令:

docker build . -t <your_docker_repo>/reactive-vertx -f Dockerfile

成功完成两个构建后,运行命令 Docker 镜像将显示以下结果:

REPOSITORY                     TAG                 IMAGE ID            CREATED             SIZE
smoell/reactive-vertx          latest              391f944bb553        19 minutes ago      181MB
<none>                         <none>              389ee5ec6a8c        19 minutes ago      411MB
smoell/reactive-vertx-native   latest              ecd72b58a3d2        25 minutes ago      133MB
<none>                         <none>              d93993d1d5ab        26 minutes ago      2.89GB
debian                         9-slim              92d2f0789514        4 days ago          55.3MB
oracle/graalvm-ce              1.0.0-rc16          131b80926177        2 weeks ago         1.72G

您可以看到在构建中使用的不同基本镜像(oracle/graalvm-ce:1.0.0-rc16debian:9-slim)、构建期间使用的临时映像(没有合适的名称)以及目标镜像 smoell/reactive-vertxsmoell/reactive-vertx-native

小结

在本博文中,我介绍了如何通过基于 Docker 多阶段构建的自包含应用程序,使用 GraalVM 将 Java 应用程序编译为本机镜像。我还演示了如何使用 jlink 为较小的目标镜像创建自定义 JDK 分配。我们希望这些内容能为您带来一些灵感,帮助您优化现有 Java 应用程序以减少启动时间和内存消耗。