ikuo’s blog

育児やエンジニアリングについて

"コンテナファースト" Java フレームワーク Quarks を試してみた

Quarksとは

https://ja.quarkus.io/

今更ぼくが紹介するまでもないと思うんですが、Redhat さん主導で開発されている新気鋭の Java フレームワークで、コンテナ環境での実行をメインターゲットにしている点が特徴ですかね。 会社で話題になっていたので試してみました。

ちょっとにわかには信じがたい性能のふれこみですね!

https://ja.quarkus.io/assets/images/quarkus_metrics_graphic_bootmem_wide.png

TL;DR

  • Native 版実行コンテナを作って起動するまで
# 雛形作成
❯ mvn io.quarkus:quarkus-maven-plugin:1.12.2.Final:create \
      -DprojectGroupId=org.acme \
      -DprojectArtifactId=getting-started \
      -DclassName="org.acme.getting.started.GreetingResource" \
      -Dpath="/hello"cd getting-started
# install Extension
❯ ./mvnw quarkus:add-extension -Dextensions="container-image-docker"
# build
❯ ./mvnw package -Pnative -Dquarkus.native.container-build=true -Dquarkus.container-image.build=true
# go!
❯ docker run -i --rm -p 8080:8080 ikuo.suyama/getting-started:1.0.0-SNAPSHOT
  • エコシステム含め開発体験◎、実戦投入に向けてより詳細に使い込んでみたい

開発

とりあえずチュートリアルをやってみます。ドキュメントは多くがすでに日本語化されていて気合を感じますね。 ちょっと分かりづらい気もする、、とくに初学者にはつらそうです(けどおそらくそこはターゲットではなさそう

ja.quarkus.io

セットアップ

❯ mvn io.quarkus:quarkus-maven-plugin:1.12.2.Final:create \
      -DprojectGroupId=org.acme \
      -DprojectArtifactId=getting-started \
      -DclassName="org.acme.getting.started.GreetingResource" \
      -Dpath="/hello"

mvn コマンド一発で雛形と Hello world, test 一式が生成されます。 .gitignore も含まれていて、IntelliJ の設定ファイルもちゃんとかいてある!

ああちゃんと使う人のことが考えられているなぁと感じます。 細かいけどこういう配慮があるプロダクトは信頼できますよね。

起動

開発モードで起動

cd getting-started
❯ ./mvnw compile quarkus:dev
:
istening for transport dt_socket at address: 5005
__  ____  __  _____   ___  __ ____  ______
 --/ __ \/ / / / _ | / _ \/ //_/ / / / __/
 -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/
2021-03-13 22:48:41,751 INFO  [io.quarkus] (Quarkus Main Thread) getting-started 1.0.0-SNAPSHOT on JVM (powered by Quarkus 1.12.2.Final) started in 1.098s. Listening on: http://localhost:8080
2021-03-13 22:48:41,754 INFO  [io.quarkus] (Quarkus Main Thread) Profile dev activated. Live Coding activated.
2021-03-13 22:48:41,754 INFO  [io.quarkus] (Quarkus Main Thread) Installed features: [cdi, resteasy]

curlしてみます。いいですね!

❯ curl localhost:8080/hello
Hello RESTEasy⏎

もちろんホットリロード対応で、変更を加えるとアプリケーションが高速にリロードされます。

コードはこんなノリです

@Path("/hello")
public class GreetingResource {

    @Inject
    GreetingService service;

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String hello() {
        return "Hello RESTEasy";
    }

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    @Path("/greeting/{name}")
    public String greeting(@PathParam("name") String name) {
        return service.greeting(name);
    }
}

サンプルはCRUDEすら無いシンプル(すぎる)アプリケーションだけど、 まんま JAX-RS だし Java で Backend やってる人はだいたいふんいきでわかるんじゃないでしょうか。

Native Build

目玉のネイティブ実行ファイルを作成してみます。

ja.quarkus.io

ここをなぞるだけです。 ビルド時に、native プロパティを指定すると GraalVM なネイティブ実行ファイルが作成できます。簡単。

❯ ./mvnw package -Pnative

めんどうくさそうなところは飛ばして、とりあえず実行すると当然死にます。

[ERROR] Failed to execute goal io.quarkus:quarkus-maven-plugin:1.12.2.Final:build (default) on project getting-started: Failed to build quarkus application: io.quarkus.builder.BuildException: Build failure: Build failed due to errors
[ERROR]     [error]: Build step io.quarkus.deployment.pkg.steps.NativeImageBuildStep#build threw an exception: java.lang.RuntimeException: Cannot find the `native-image` in the GRAALVM_HOME, JAVA_HOME and System PATH. Install it using `gu install native-image`
[ERROR]     at io.quarkus.deployment.pkg.steps.NativeImageBuildStep.getNativeImageBuildRunner(NativeImageBuildStep.java:306)
[ERROR]     at io.quarkus.deployment.pkg.steps.NativeImageBuildStep.build(NativeImageBuildStep.java:102)
[ERROR]     at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
[ERROR]     at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
[ERROR]     at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
[ERROR]     at java.base/java.lang.reflect.Method.invoke(Method.java:566)
[ERROR]     at io.quarkus.deployment.ExtensionLoader$2.execute(ExtensionLoader.java:920)
[ERROR]     at io.quarkus.builder.BuildContext.run(BuildContext.java:277)
[ERROR]     at org.jboss.threads.EnhancedQueueExecutor$Task.run(EnhancedQueueExecutor.java:2415)
[ERROR]     at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1452)
[ERROR]     at java.base/java.lang.Thread.run(Thread.java:829)
[ERROR]     at org.jboss.threads.JBossThread.run(JBossThread.java:501)
[ERROR] -> [Help 1]
:

ふむ。GraalVM をインストールしてないからですね。それはそうだ。ドキュメント読みましょう。でもインストールするのは面倒くさいです。

どうやら GraalVM をインストールしなくても、ビルド用イメージを引っ張ってきてコンテナ内でビルドしてくれるオプションがあるみたいです。すばらしい。 もちろんMacOS用の実行ファイルにはならないですけど。

❯ ./mvnw package -Pnative -Dquarkus.native.container-build=true

今度は違うエラーで死。OOMだそう。

[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  31.431 s
[INFO] Finished at: 2021-03-13T23:19:52+09:00
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal io.quarkus:quarkus-maven-plugin:1.12.2.Final:build (default) on project getting-started: Failed to build quarkus application: io.quarkus.builder.BuildException: Build failure: Build failed due to errors
[ERROR]     [error]: Build step io.quarkus.deployment.pkg.steps.NativeImageBuildStep#build threw an exception: java.lang.RuntimeException: Failed to build native image
[ERROR]     at io.quarkus.deployment.pkg.steps.NativeImageBuildStep.build(NativeImageBuildStep.java:282)
[ERROR]     at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
[ERROR]     at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
[ERROR]     at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
[ERROR]     at java.base/java.lang.reflect.Method.invoke(Method.java:566)
[ERROR]     at io.quarkus.deployment.ExtensionLoader$2.execute(ExtensionLoader.java:920)
[ERROR]     at io.quarkus.builder.BuildContext.run(BuildContext.java:277)
[ERROR]     at org.jboss.threads.EnhancedQueueExecutor$Task.run(EnhancedQueueExecutor.java:2415)
[ERROR]     at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1452)
[ERROR]     at java.base/java.lang.Thread.run(Thread.java:829)
[ERROR]     at org.jboss.threads.JBossThread.run(JBossThread.java:501)
[ERROR] 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
[ERROR]     at io.quarkus.deployment.pkg.steps.NativeImageBuildStep.imageGenerationFailed(NativeImageBuildStep.java:439)
[ERROR]     at io.quarkus.deployment.pkg.steps.NativeImageBuildStep.build(NativeImageBuildStep.java:254)
[ERROR]     ... 10 more
[ERROR] -> [Help 1]
:

quarkus.native.native-image-xmx を指定するといいよ、とあるけど、追加しても同様。 いま Docker コンテナ内でビルドしてるはずなので、 Docker 側の設定の問題じゃないかと踏んで確認してみたら、案の定何故か Docker のメモリ制限が 2G とかになっていました。 アップグレードされた際にリセットされてしまったのかな。とにかく適当に大きめに設定します。

:
[INFO] [io.quarkus.deployment.pkg.steps.NativeImageBuildStep] -J-Djava.util.logging.manager=org.jboss.logmanager.LogManager -J-Dsun.nio.ch.maxUpdateArraySize=100 -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=ja -J-Duser.country=JP -J-Dfile.encoding=UTF-8 --initialize-at-build-time= -H:InitialCollectionPolicy=com.oracle.svm.core.genscavenge.CollectionPolicy\$BySpaceAndTime -H:+JNI -H:+AllowFoldMethods -jar getting-started-1.0.0-SNAPSHOT-runner.jar -H:FallbackThreshold=0 -H:+ReportExceptionStackTraces -H:-AddAllCharsets -H:EnableURLProtocols=http --no-server -H:-UseServiceLoaderFeature -H:+StackTrace getting-started-1.0.0-SNAPSHOT-runner
[getting-started-1.0.0-SNAPSHOT-runner:26]    classlist:   4,692.18 ms,  0.96 GB
[getting-started-1.0.0-SNAPSHOT-runner:26]        (cap):     538.10 ms,  0.96 GB
[getting-started-1.0.0-SNAPSHOT-runner:26]        setup:   2,192.05 ms,  0.96 GB
14:22:21,626 INFO  [org.jbo.threads] JBoss Threads version 3.2.0.Final
[getting-started-1.0.0-SNAPSHOT-runner:26]     (clinit):     528.50 ms,  1.95 GB
[getting-started-1.0.0-SNAPSHOT-runner:26]   (typeflow):   9,915.57 ms,  1.95 GB
[getting-started-1.0.0-SNAPSHOT-runner:26]    (objects):   9,587.44 ms,  1.95 GB
[getting-started-1.0.0-SNAPSHOT-runner:26]   (features):     519.44 ms,  1.95 GB
[getting-started-1.0.0-SNAPSHOT-runner:26]     analysis:  21,527.90 ms,  1.95 GB
[getting-started-1.0.0-SNAPSHOT-runner:26]     universe:   1,069.14 ms,  2.67 GB
[getting-started-1.0.0-SNAPSHOT-runner:26]      (parse):   2,067.55 ms,  2.67 GB
[getting-started-1.0.0-SNAPSHOT-runner:26]     (inline):   3,448.59 ms,  3.37 GB
[getting-started-1.0.0-SNAPSHOT-runner:26]    (compile):  13,496.82 ms,  4.52 GB
[getting-started-1.0.0-SNAPSHOT-runner:26]      compile:  20,469.25 ms,  4.52 GB
[getting-started-1.0.0-SNAPSHOT-runner:26]        image:   2,609.89 ms,  4.52 GB
[getting-started-1.0.0-SNAPSHOT-runner:26]        write:   1,690.58 ms,  4.52 GB
[getting-started-1.0.0-SNAPSHOT-runner:26]      [total]:  54,532.52 ms,  4.52 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 60944ms
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  01:07 min
[INFO] Finished at: 2021-03-13T23:23:01+09:00

はいはい、今度は成功しました。メモリは5Gくらい食ったみたいですね。

いつもどおり target/ 下に実行ファイルができています。

❯ ll target
total 57632
drwxr-xr-x  14 ikuo.suyama  staff   448B  3 13 23:38 ./
drwxr-xr-x  14 ikuo.suyama  staff   448B  3 13 23:42 ../
-rwxr-xr-x   1 ikuo.suyama  staff    28M  3 13 23:23 getting-started-1.0.0-SNAPSHOT-runner*

... 28MB??

いやいやご冗談でしょう。。Javaですよ?いくら Hello World だけとはいえ。 linux 用にビルドしたものなので当然Macでは動きませんけど。。

Image作成

せっかくなので Docker Image も作成してみましょう。 package に -Dquarkus.container-image.build=true オプションが用意されていてスムーズです。 Document が少し分かりづらいけど、今は3通り Image ビルドする方法が用意されています。

ja.quarkus.io

どれかのエクステンションは追加しないと Image ビルドできないみたいですね。 こんなWARNが出ていることに気づかず、Image が生成されないなーとしばらく悩みました。

2021-03-13 23:33:07,289 WARN  [io.qua.config] (main) Unrecognized configuration key "quarkus.container-image.build" was provided; it will be ignored; verify that the dependency extension for this configuration is set or you did not make a typo

とりあえずDockerでビルドすることにして、quarkus-container-image-docker エクステンションを追加します。

❯ ./mvnw quarkus:add-extension -Dextensions="container-image-docker"

Native 版

ビルドするコマンドは以下。

❯ ./mvnw package -Pnative -Dquarkus.native.container-build=true -Dquarkus.container-image.build=true

できました!

❯ docker images | grep getting-started
ikuo.suyama/getting-started                                                              1.0.0-SNAPSHOT        4e99006a3194   25 seconds ago   132MB

132MB! いやー小さいですね。Golang とかに比べたらまだ大きいのかも知れませんけど... ちなみに BaseImage には Redhat さんの ubi ってやつが使われてるみたいです。38MBくらい。

起動してみましょう。
※ グループ名は自分のものに置き換えてください。ビルド時のオプションで指定もできます。

❯ docker run -i --rm -p 8080:8080 ikuo.suyama/getting-started:1.0.0-SNAPSHOT
__  ____  __  _____   ___  __ ____  ______
 --/ __ \/ / / / _ | / _ \/ //_/ / / / __/
 -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/
2021-03-13 15:20:51,266 INFO  [io.quarkus] (main) getting-started 1.0.0-SNAPSHOT native (powered by Quarkus 1.12.2.Final) started in 0.017s. Listening on: http://0.0.0.0:8080
2021-03-13 15:20:51,266 INFO  [io.quarkus] (main) Profile prod activated.
2021-03-13 15:20:51,266 INFO  [io.quarkus] (main) Installed features: [cdi, resteasy]

早つ。1s かからない。いいかんじですね!

jar 版との比較

せっかくなので jar 版も作って、サイズなど比較してみましょう。 -Pnative せずにビルドします。GraalVMがいらないので -Dquarkus.native.container-build=true も不要です。

区別するために Image の名前を適当に変えておきます。こういうオプションがちゃんと用意されているところにも好感が持てます。

❯ ./mvnw package -Dquarkus.container-image.name=quarks-getting-started-jar
❯ d images | grep getting-started
ikuo.suyama/getting-started                                                              1.0.0-SNAPSHOT        4e99006a3194   9 minutes ago    132MB
ikuo.suyama/quarks-getting-started-jar                                                   1.0.0-SNAPSHOT        c0281da22b67   46 minutes ago   380MB

やはりjarだとサイズがおおきいです。約2.8倍!いや、ネイティブ版が小さすぎるんですけど。 起動してみます。

❯ docker run -i --rm -p 8080:8080 ikuo.suyama/quarks-getting-started-jar:1.0.0-SNAPSHOT
exec java -Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager -XX:+ExitOnOutOfMemoryError -cp . -jar /deployments/quarkus-run.jar
__  ____  __  _____   ___  __ ____  ______
 --/ __ \/ / / / _ | / _ \/ //_/ / / / __/
 -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/
2021-03-13 15:27:38,805 INFO  [io.quarkus] (main) getting-started 1.0.0-SNAPSHOT on JVM (powered by Quarkus 1.12.2.Final) started in 0.673s. Listening on: http://0.0.0.0:8080
2021-03-13 15:27:38,830 INFO  [io.quarkus] (main) Profile prod activated.
2021-03-13 15:27:38,830 INFO  [io.quarkus] (main) Installed features: [cdi, resteasy]
^C2021-03-13 15:27:45,104 INFO  [io.quarkus] (Shutdown thread) getting-started stopped in 0.076s

java経由で起動するのでネイティブ版よりワンテンポ遅く感じます。

まとめ

ネイティブ実行ファイルの作成、コンテナイメージの作成まで非常にスムーズに実施できました。 おそらく本格的なアプリケーションを開発し始めるとそれなりに詰まるポイントがあるんでしょうけど、それでもかなり可能性を感じますね。
特にやはりGraalVMによるネイティブ実行ファイルのイメージサイズの小ささや実行速度はかなり魅力的に感じます。つぎはk8sとも組み合わせてみようかな。
実戦投入に向けて、よりディープに使い込んでみたいなと思えるプロダクトでした!