写点什么

应“云”而生的 Java 框架 Quarkus:构建小而快的镜像

用户头像
张晓辉
关注
发布于: 2021 年 04 月 27 日


电影《功夫》中,火云邪神有句话:“天下武功无坚不摧,唯快不破。”


上一篇文章 中,我们写了第一个 Quarkus 应用,并尝试着构建了 legacy-jarfast-jar


今天来看一下 Quarkus 构建出来的本机可执行文件到底比 Spring 应用能快多少,生态的成熟度不在这里讨论

TLDR

先上结论, 与只有一个 Controller 的 Spring Web 应用做下对比。

应用启动时间:0.012s vs 2.294s


镜像大小:49MB vs 237 MB

Spring 应用镜像使用 openjdk:11.0-jre-slim 作为 base 镜像,大小为 220MB。


docker imagesREPOSITORY                                    TAG             IMAGE ID       CREATED          SIZEspring/spring-getting-started                 latest          5f47030c5c3f   6 minutes ago    237MBquarkus/quarkus-getting-started               distroless2     fe973c5ac172   24 minutes ago   49MBquarkus/quarkus-getting-started               distroless      6fe27dd44e86   31 minutes ago   51MBquarkus/quarkus-getting-started               ubi             8f86f5915715   58 minutes ago   132MB
复制代码

Java 应用容器化的困境

云原生世界中,应用容器化是个显著的特点。Java 应用容器化时面临了如下问题:


  • 应用启动慢:其实这是 Java 应用的问题。Java 应用占用内存多;JVM 虚拟机启动时需要做环境的初始化、预加载大量的类、初始化线程等等。启动耗时视应用情况需要几秒,甚至可达分钟级。较长的启动耗时,也抑制了水平伸缩性。即使在 Serverless 这种响应耗时要求不高的场景,也会被嫌弃。

  • 镜像过大:其实使用了镜像的分层设计,常见的一个 SpringCloud 应用的 über-jar 包可能都有 7、80MB。

  • 空间占用:虽然用了镜像分层,但积少成多,也会增加存储成本。

Quarkus 与本机映像(native image)

Quarkus 的开发遵从了容器优先的原则:


  • 支持 Graal/SubstrateVM

  • 构建时处理元数据

  • 减少反射的使用

  • 本机映像预启动


本机映像是将 Java 代码提前编译为可执行文件(称为本机映像)的技术。该可执行文件包括应用程序类、其依赖项中的类、运行时库类以及 JDK 中的静态链接本机代码。它不是在 Java VM 上运行,而是包括必要的组件,例如内存管理,线程调度等,这些组件来自另一个运行时系统 “Substrate VM”。“Substrate VM” 是运行时组件(例如反优化器,垃圾收集器,线程调度等)的名称。与 JVM 相比,生成的程序具有更快的启动时间和更低的运行时内存开销。

如何构建本机映像

环境配置参考上一篇文章,可以直接从这里下载源码

配置 GraalVM

之前我们使用了 sdkman 进行 GraalVM 安装。设置 GRAALVM_HOME 环境变量:


export GRAALVM_HOME=`sdk home java 21.0.0.2.r11-grl`
复制代码


使用 gu 安装 native-image


${GRAALVM_HOME}/bin/gu install native-image
复制代码

构建本机可执行文件

在源码的 pom.xml 中,我们可以看到如下的 profile


<profiles>    <profile>        <id>native</id>        <properties>            <quarkus.package.type>native</quarkus.package.type>        </properties>    </profile></profiles>
复制代码


我们使用这个 profile 进行本机可执行文件的构建,整个构建耗时 几分钟


./mvnw package -Pnative
复制代码


部分构建日志:


[INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ quarkus-getting-started ---[INFO][INFO] --- quarkus-maven-plugin:1.13.0.Final:build (default) @ quarkus-getting-started ---[INFO] [org.jboss.threads] JBoss Threads version 3.2.0.Final[INFO] [io.quarkus.deployment.pkg.steps.JarResultBuildStep] Building native image source jar: /Users/addo/Workspaces/private_w/quarkus-getting-started/target/quarkus-getting-started-1.0.0-SNAPSHOT-native-image-source-jar/quarkus-getting-started-1.0.0-SNAPSHOT-runner.jar[INFO] [io.quarkus.deployment.pkg.steps.NativeImageBuildStep] Building native image from /Users/addo/Workspaces/private_w/quarkus-getting-started/target/quarkus-getting-started-1.0.0-SNAPSHOT-native-image-source-jar/quarkus-getting-started-1.0.0-SNAPSHOT-runner.jar[INFO] [io.quarkus.deployment.pkg.steps.NativeImageBuildContainerRunner] Using docker to run the native image builder[INFO] [io.quarkus.deployment.pkg.steps.NativeImageBuildContainerRunner] Checking image status quay.io/quarkus/ubi-quarkus-native-image:21.0.0-java1121.0.0-java11: Pulling from quarkus/ubi-quarkus-native-imageDigest: sha256:becf08de869e707beaa5e57444b533ef93ebef15aad90c92ac660ddf7cea2b11Status: Image is up to date for quay.io/quarkus/ubi-quarkus-native-image:21.0.0-java11quay.io/quarkus/ubi-quarkus-native-image:21.0.0-java11[INFO] [io.quarkus.deployment.pkg.steps.NativeImageBuildStep] Running Quarkus native-image plugin on GraalVM Version 21.0.0 (Java Version 11.0.10+8-jvmci-21.0-b06)[INFO] [io.quarkus.deployment.pkg.steps.NativeImageBuildRunner] docker run --env LANG=C --rm -v /Users/addo/Workspaces/private_w/quarkus-getting-started/target/quarkus-getting-started-1.0.0-SNAPSHOT-native-image-source-jar:/project:z quay.io/quarkus/ubi-quarkus-native-image:21.0.0-java11 -J-Dsun.nio.ch.maxUpdateArraySize=100 -J-Djava.util.logging.manager=org.jboss.logmanager.LogManager -J-Dvertx.logger-delegate-factory-class-name=io.quarkus.vertx.core.runtime.VertxLogDelegateFactory -J-Dvertx.disableDnsResolver=true -J-Dio.netty.leakDetection.level=DISABLED -J-Dio.netty.allocator.maxOrder=1 -J-Duser.language=en -J-Duser.country=CN -J-Dfile.encoding=UTF-8 --initialize-at-build-time= -H:InitialCollectionPolicy=com.oracle.svm.core.genscavenge.CollectionPolicy\$BySpaceAndTime -H:+JNI -H:+AllowFoldMethods -jar quarkus-getting-started-1.0.0-SNAPSHOT-runner.jar -H:FallbackThreshold=0 -H:+ReportExceptionStackTraces -J-Xmx5g -H:-AddAllCharsets -H:EnableURLProtocols=http --no-server -H:-UseServiceLoaderFeature -H:+StackTrace quarkus-getting-started-1.0.0-SNAPSHOT-runner[quarkus-getting-started-1.0.0-SNAPSHOT-runner:25]    classlist:   5,859.24 ms,  0.96 GB[quarkus-getting-started-1.0.0-SNAPSHOT-runner:25]        (cap):     633.34 ms,  0.94 GB[quarkus-getting-started-1.0.0-SNAPSHOT-runner:25]        setup:   2,468.19 ms,  0.94 GB00:06:00,437 INFO  [org.jbo.threads] JBoss Threads version 3.2.0.Final[quarkus-getting-started-1.0.0-SNAPSHOT-runner:25]     (clinit):     516.65 ms,  2.23 GB[quarkus-getting-started-1.0.0-SNAPSHOT-runner:25]   (typeflow):  12,642.02 ms,  2.23 GB[quarkus-getting-started-1.0.0-SNAPSHOT-runner:25]    (objects):  11,340.37 ms,  2.23 GB[quarkus-getting-started-1.0.0-SNAPSHOT-runner:25]   (features):     525.87 ms,  2.23 GB[quarkus-getting-started-1.0.0-SNAPSHOT-runner:25]     analysis:  26,032.67 ms,  2.23 GB[quarkus-getting-started-1.0.0-SNAPSHOT-runner:25]     universe:   1,394.06 ms,  2.16 GB[quarkus-getting-started-1.0.0-SNAPSHOT-runner:25]      (parse):   2,690.38 ms,  2.16 GB[quarkus-getting-started-1.0.0-SNAPSHOT-runner:25]     (inline):   4,336.77 ms,  2.73 GB[quarkus-getting-started-1.0.0-SNAPSHOT-runner:25]    (compile):  17,580.03 ms,  2.71 GB[quarkus-getting-started-1.0.0-SNAPSHOT-runner:25]      compile:  26,152.06 ms,  2.71 GB[quarkus-getting-started-1.0.0-SNAPSHOT-runner:25]        image:   3,288.43 ms,  2.70 GB[quarkus-getting-started-1.0.0-SNAPSHOT-runner:25]        write:   1,904.64 ms,  2.70 GB[quarkus-getting-started-1.0.0-SNAPSHOT-runner:25]      [total]:  67,414.16 ms,  2.70 GB[WARNING] [io.quarkus.deployment.pkg.steps.NativeImageBuildStep] objcopy executable not found in PATH. Debug symbols will not be separated from executable.[WARNING] [io.quarkus.deployment.pkg.steps.NativeImageBuildStep] That will result in a larger native image with debug symbols embedded in it.[INFO] [io.quarkus.deployment.QuarkusAugmentor] Quarkus augmentation completed in 74739ms[INFO] ------------------------------------------------------------------------[INFO] BUILD SUCCESS[INFO] ------------------------------------------------------------------------[INFO] Total time:  01:21 min[INFO] Finished at: 2021-04-17T08:06:47+08:00[INFO] ------------------------------------------------------------------------
复制代码


假如构建时出现类似 Caused by: java.lang.RuntimeException: Image generation failed. Exit code was 137 which indicates an out of memory error. Consider increasing the Xmx value for native image generation by setting the "quarkus.native.native-image-xmx" property 这种报错。需要调整下 Docker 的设置,比如笔者使用的 macOS,打开 Docker Desktop > Preference > Resource > Advanced,将内存从默认的 2GB 调大,比如 8GB。

从构建日志可以看出,构建的过程是在 quay.io/quarkus/ubi-quarkus-native-image 的容器中完成的。虽然异常提示调整 "quarkus.native.native-image-xmx" ,其实是容器内存太小导致的。


构建成功后,可以在 target 中找到 quarkus-getting-started-1.0.0-SNAPSHOT-runner。这是一个可执行文件,大小为 28MB。


尝试执行该文件,收到 zsh: exec format error: ./target/quarkus-getting-started-1.0.0-SNAPSHOT-runner 错误。因为这是一个 Linux 可执行文件,因此我们需要在容器中运行。

构建本机镜像

在源文件的 src/main/docker 目录中,我们可以找到 Dockerfile.native


FROM registry.access.redhat.com/ubi8/ubi-minimal:8.3WORKDIR /work/RUN chown 1001 /work \    && chmod "g+rwX" /work \    && chown 1001:root /workCOPY --chown=1001:root target/*-runner /work/application
EXPOSE 8080USER 1001
CMD ["./application", "-Dquarkus.http.host=0.0.0.0"]
复制代码

运行镜像

本地运行一下,可以看出启动只需要 0.013s


docker run --rm -p 8080:8080 quarkus/quarkus-getting-started:latest__  ____  __  _____   ___  __ ____  ______ --/ __ \/ / / / _ | / _ \/ //_/ / / / __/ -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \--\___\_\____/_/ |_/_/|_/_/|_|\____/___/2021-04-17 00:22:27,146 INFO  [io.quarkus] (main) quarkus-getting-started 1.0.0-SNAPSHOT native (powered by Quarkus 1.13.0.Final) started in 0.013s. Listening on: http://0.0.0.0:80802021-04-17 00:22:27,147 INFO  [io.quarkus] (main) Profile prod activated.2021-04-17 00:22:27,147 INFO  [io.quarkus] (main) Installed features: [cdi, resteasy]
复制代码


测试一下端点:


http :8080/hello/greeting/quarkusHTTP/1.1 200 OKContent-Length: 14Content-Type: text/plain;charset=UTF-8
Hello, quarkus
复制代码


看下镜像的信息,大小为 132MB,其中 base 镜像 ubi-minimal 就占了 103 MB。感觉还是有点大,是否继续精简一下?


docker imagesREPOSITORY                                    TAG             IMAGE ID       CREATED          SIZEquarkus/quarkus-getting-started               latest          8f86f5915715   4 minutes ago    132MBregistry.access.redhat.com/ubi8/ubi-minimal   8.3             604ddd554fec   2 weeks ago      103MB
复制代码

镜像瘦身

src/main/docker 中还有个名为 Dockerfile.native-distroless 的 Dockerfile,里面使用了 quay.io/quarkus/quarkus-distroless-image:1.0 作为 base 镜像


使用这个 Dockerfile 进行构建,得到的镜像就小很多,只有 51MB:


docker imagesREPOSITORY                                    TAG          IMAGE ID       CREATED          SIZEquarkus/quarkus-getting-started               distroless   6fe27dd44e86   33 seconds ago   51MBquarkus/quarkus-getting-started               ubi          8f86f5915715   27 minutes ago   132MBquay.io/quarkus/quarkus-distroless-image      1.0          062663862a83   6 days ago       21.3MBregistry.access.redhat.com/ubi8/ubi-minimal   8.3          604ddd554fec   2 weeks ago      103MB
复制代码


运行成功:


docker run --rm -p 8080:8080 quarkus/quarkus-getting-started:distroless__  ____  __  _____   ___  __ ____  ______ --/ __ \/ / / / _ | / _ \/ //_/ / / / __/ -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \--\___\_\____/_/ |_/_/|_/_/|_|\____/___/2021-04-17 00:51:26,070 INFO  [io.quarkus] (main) quarkus-getting-started 1.0.0-SNAPSHOT native (powered by Quarkus 1.13.0.Final) started in 0.013s. Listening on: http://0.0.0.0:80802021-04-17 00:51:26,071 INFO  [io.quarkus] (main) Profile prod activated.2021-04-17 00:51:26,071 INFO  [io.quarkus] (main) Installed features: [cdi, resteasy]
复制代码


极致瘦身,参考了这里,我们创建 Dockerfile.native-distroless2


最终镜像的大小为 49MB,与官方提供的 distroless base 镜像只小了 2MB。


docker imagesREPOSITORY                                    TAG           IMAGE ID       CREATED          SIZEquarkus/quarkus-getting-started               distroless2   fe973c5ac172   3 seconds ago    49MB
复制代码


前面对比,用来构建 Spring 应用的 base 镜像 openjdk:11.0-jre-slim 已经有 220MB,这还没算上应用的大小。即使是 openjdk:17-alpine3.13 也有 182 MB。

NEXT

下一回,我们试试 Quarkus 在 Serverless 中的应用,看下 Serverless 上的使用体验如何

发布于: 2021 年 04 月 27 日阅读数: 42
用户头像

张晓辉

关注

大胆尝试,小心求证 2018.04.09 加入

胡说八道

评论

发布
暂无评论
应“云”而生的 Java 框架 Quarkus:构建小而快的镜像