优化 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:
第 2 步:将自定义运行时复制到目标镜像
接下来,将新创建的自定义运行时从构建映像复制到实际目标镜像。在这一步,您要再次使用 debian:9-slim 作为基本镜像。复制最小运行时后,将 Java 应用程序复制到 /opt/,添加 Docker 运行状况检查,然后启动 Java 进程:
使用 GraalVM 将 Java 编译为本机
GraalVM 是 Oracle 提供的一种开源、高性能 Polyglot 虚拟机。使用它可以提前编译本机镜像,以提高启动性能,并减少基于 JVM 的应用程序的内存消耗和文件大小。允许预先编译的框架称为 SubstrateVM。
在以下部分,您可以看到 pom.xml 文件的相关片段。创建一个名为 native-image-fargate 的附加 Maven 配置文件,该文件使用 native-image-maven 插件在“包装”阶段将源代码编译为本机镜像:
Docker 多阶段构建
您的目标是定义一个包含尽可能少相关项的可重现构建环境。为此,要创建一个使用 Docker 多阶段构建的自包含构建过程。
对于多阶段构建而言,一个值得关注的方面在于,您可以在 Dockerfile 中使用多个 FROM 语句。每条 FROM 指令可以使用不同的基本镜像,并开始构建的新阶段。您可以选择必需的文件并将其从一个阶段复制到另一个阶段,这非常有用,因为这样可以限制必须复制的文件数。使用此功能可以在一个阶段构建应用程序,并将已编译的构件和其他文件复制到目标镜像。
在以下部分,您可以看到构建的两个不同阶段。您的 Dockerfile(称为 Dockerfile-native)分为两部分:构建器镜像和目标镜像。
第一个代码示例显示了基于 graalvm-ce 的生成器镜像。在构建过程中,必须安装 Maven、设置一些环境变量,并将必要的文件复制到 Docker 镜像中。对于构建,您需要源代码和 pom.xml 文件。成功将文件复制到 Docker 镜像后,将使用配置文件 native-image-fargate 启动应用程序到可执行二进制文件的构建。当然,也可以使用 Maven 基本镜像并安装 GraalVM(整个构建过程会稍有不同)。
现在,开始执行多阶段构建过程中第二部的操作:创建实际目标镜像。此镜像基于 debian:9-slim,并将两个环境变量设置为 TLS 特定设置(因为应用程序使用 TLS 与 Amazon Kinesis 数据流通信)。
构建目标镜像非常简单。运行以下命令:
要使用 Uber JAR 构建标准 Docker 镜像,请运行以下命令:
成功完成两个构建后,运行命令 Docker 镜像将显示以下结果:
您可以看到在构建中使用的不同基本镜像(oracle/graalvm-ce:1.0.0-rc16 和 debian:9-slim)、构建期间使用的临时映像(没有合适的名称)以及目标镜像 smoell/reactive-vertx 和 smoell/reactive-vertx-native。
小结
在本博文中,我介绍了如何通过基于 Docker 多阶段构建的自包含应用程序,使用 GraalVM 将 Java 应用程序编译为本机镜像。我还演示了如何使用 jlink 为较小的目标镜像创建自定义 JDK 分配。我们希望这些内容能为您带来一些灵感,帮助您优化现有 Java 应用程序以减少启动时间和内存消耗。