Java framework born of "cloud": building native executable files

Today, let's take a look at how fast the native executable built by Quarkus can be faster than the Spring application. The maturity of ecology is not discussed here.

TLDR

First, let's draw a conclusion and make a comparison with the Spring Web application with only one Controller.

Application start time: 0.012s vs 2.294s

Image size: 49MB vs 237 MB

The Spring application image uses openjdk:11.0-jre-slim as the base image, with a size of 220MB.

docker images
REPOSITORY                                    TAG             IMAGE ID       CREATED          SIZE
spring/spring-getting-started                 latest          5f47030c5c3f   6 minutes ago    237MB
quarkus/quarkus-getting-started               distroless2     fe973c5ac172   24 minutes ago   49MB
quarkus/quarkus-getting-started               distroless      6fe27dd44e86   31 minutes ago   51MB
quarkus/quarkus-getting-started               ubi             8f86f5915715   58 minutes ago   132MB

The dilemma of Java application containerization

In the cloud native world, application containerization is a significant feature. When Java applications are containerized, they face the following problems:

  • Slow application startup: in fact, this is the problem of Java applications. Java applications occupy more memory; When starting a JVM virtual machine, you need to initialize the environment, preload a large number of classes, initialize threads, and so on. The startup time can take several seconds or even minutes depending on the application. The longer start-up time also inhibits horizontal scalability. Even in Serverless scenarios where the response time is not high, it will be rejected.
  • Too large image: in fact, the layered design of image is used, which is a common spring cloud application ̈ The BER jar package may have 7 or 80MB.
  • Space occupation: Although image layering is used, a little makes a mickle, which will also increase the storage cost.

Quarkus and native image

Quarkus's development follows the principle of container first:

  • Graal/SubstrateVM support
  • Process metadata at build time
  • Reduce the use of reflections
  • Native image pre boot

Native image is a technology that compiles java code into executable files (called native image) in advance. The executable includes application classes, classes in their dependencies, runtime classes, and statically linked native code in the JDK. It does not run on the Java VM, but includes necessary components, such as memory management, thread scheduling, etc. these components come from another runtime system "substrate VM". "Substrate VM" is the name of the runtime component (e.g. de optimizer, garbage collector, thread scheduling, etc.). Compared with the JVM, the generated program has faster startup time and lower runtime memory overhead.

How to build a native image

Refer to the previous article for environment configuration. You can download the source code directly from here.

Configuring GraalVM

Previously, we used sdkman to install GraalVM. Set graavm_ Home environment variable:

export GRAALVM_HOME=`sdk home java 21.0.0.2.r11-grl`

To install native image using gu:

${GRAALVM_HOME}/bin/gu install native-image

Build native executable

In the source POM In XML, we can see the following profile:

<profiles>
    <profile>
        <id>native</id>
        <properties>
            <quarkus.package.type>native</quarkus.package.type>
        </properties>
    </profile>
</profiles>

We use this profile to build the native executable. The whole construction takes several minutes.

./mvnw package -Pnative

Partial build log:

[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-java11
21.0.0-java11: Pulling from quarkus/ubi-quarkus-native-image
Digest: sha256:becf08de869e707beaa5e57444b533ef93ebef15aad90c92ac660ddf7cea2b11
Status: Image is up to date for quay.io/quarkus/ubi-quarkus-native-image:21.0.0-java11
quay.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 GB
00: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] ------------------------------------------------------------------------

If something like caused by: Java occurs during construction 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. You need to adjust the settings of Docker, such as the Mac OS used by the author, open Docker desktop > preference > resource > advanced, and increase the memory from the default 2GB, such as 8GB.

It can be seen from the build log that the build process is in quay Completed in the container of IO / quarkus / UBI quarkus native image. Although the exception prompt adjusts "quarkus. Native. Native image Xmx", it is actually caused by the small memory of the container.

After successful construction, you can find quarkus-getting-started-1.0.0-SNAPSHOT-runner in target. This is an executable file with a size of 28MB.

Trying to execute the file, received Zsh: exec format error:/ Target / quarkus-getting-started-1.0.0-snapshot-runner error. Because this is a Linux executable, we need to run it in the container.

Build native image

In the src/main/docker directory of the source file, we can find dockerfile native :

FROM registry.access.redhat.com/ubi8/ubi-minimal:8.3
WORKDIR /work/
RUN chown 1001 /work \
    && chmod "g+rwX" /work \
    && chown 1001:root /work
COPY --chown=1001:root target/*-runner /work/application

EXPOSE 8080
USER 1001

CMD ["./application", "-Dquarkus.http.host=0.0.0.0"]

Run mirror

Run it locally, and you can see that it only takes 0.013s to start.

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:8080
2021-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]

Test the endpoint:

http :8080/hello/greeting/quarkus
HTTP/1.1 200 OK
Content-Length: 14
Content-Type: text/plain;charset=UTF-8

Hello, quarkus

Take a look at the image information. The size is 132MB, of which the base image UBI minimal accounts for 103 MB. It still feels a little big. Do you want to continue to simplify it?

docker images
REPOSITORY                                    TAG             IMAGE ID       CREATED          SIZE
quarkus/quarkus-getting-started               latest          8f86f5915715   4 minutes ago    132MB
registry.access.redhat.com/ubi8/ubi-minimal   8.3             604ddd554fec   2 weeks ago      103MB

Mirror slimming

There is also a file named Dockerfile in src/main/docker The Dockerfile of native destroy uses quay IO / quarkus / quarkus destroy image: 1.0 as base image

Using this Dockerfile to build, the image is much smaller, only 51MB:

docker images
REPOSITORY                                    TAG          IMAGE ID       CREATED          SIZE
quarkus/quarkus-getting-started               distroless   6fe27dd44e86   33 seconds ago   51MB
quarkus/quarkus-getting-started               ubi          8f86f5915715   27 minutes ago   132MB
quay.io/quarkus/quarkus-distroless-image      1.0          062663862a83   6 days ago       21.3MB
registry.access.redhat.com/ubi8/ubi-minimal   8.3          604ddd554fec   2 weeks ago      103MB

Run successfully:

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:8080
2021-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]

Extreme slimming, refer to here, we create dockerfile native-distroless2 .

The size of the final image is 49MB, which is 2MB smaller than the official disaster base image.

docker images
REPOSITORY                                    TAG           IMAGE ID       CREATED          SIZE
quarkus/quarkus-getting-started               distroless2   fe973c5ac172   3 seconds ago    49MB

In the previous comparison, the base image openjdk:11.0-jre-slim used to build Spring applications has 220MB, which does not count the size of the application. Even openjdk: 17-alpine3 182 MB, too.

Original link: https://atbug.com/quarkus-build-native-executable-file/

If you think this article is helpful to you, you can pay attention to my official account and reply to the key words [interview] to get a Java core knowledge and arrange an interview gift! There are more technical dry goods articles and related materials to share. Let's learn and make progress together!

 

Keywords: Docker Spring openjdk graalvm

Added by dfownz on Thu, 03 Mar 2022 22:45:29 +0200