Serverless時代のJavaEEコンテナ - Quarkus

はじめに

Quarkusをご存知ですか? Redhat社が出した爆速のJavaEEコンテナです。

Publickeyの記事でも紹介されていますがGraalVMのnative-imageでコンパイルされるため、JAX-RSCDIJPAや100ms以下の起動速度と省メモリを誇るスーパーソニックなが最大の特徴です。

WildflyやThorntailがあるのになんでまた作り始めたの?」となりますが、サーバレスへの対応は既存のマイクロサービスへの対応だけでは不十分だからです。 ここで言うサーバレスはAWS LambdaのようなFaaSに限ったことではなく、実際のコンピュートではなく使用リソースなどを指定してオンデマンドでプロセスを立ち上げたり課金がされるような仕組みのことをさします。

JavaEEコンテナの世代と移り変わり

JavaEEコンテナは現在大きな変革を求められています。J2EEからJavaEE5にシフトした時以来の大きな変化だと思います。 そもそもJavaEEWebアプリケーションフレームワークというより分散システムとして始まります。 当時はそれ向けエコシステムがあったわけではないでしょうし、基本的に全てJavaEE内でリソース管理をすることを想定しています。 なのでアプリケーション実行エンジンに加えて「負荷分散」「クラスタリング」「QoS/リソース管理」「モニタリング」「デプロイメント/バージョニング」「Isolation」までをカバーするのが他言語のフレームワークと少し違うところです。 ベンダーの独自拡張もありますがそうはいっても上記の基本的な要素はJavaEEの仕様としても入っており標準化されているのが良いですね。これを仮に第1世代と呼びます。

しかし、時代は代わりマイクロサービスが台等してくるとエコシステムも進化して、JavaEEがもともと持っていたような管理機能はk8sやIstioによって実現されるようになってきました。 こうなると、他のマイクロサービスと異なりJavaEEだけ独自の管理機能を持ってるのは不便です。そのため他システムとのインテグレーションのために再定義された仕様がMicroProfileであり、MicroProfileだけをターゲットにすることでスリム化を計ったのがPayara MicroやThorntailです。

以下の画像に載ってるように以下のようコンテナ機能およびその管理機能がk8sに移行されミドルウェアの領域が小さくなります。 f:id:pascal256:20190415045549p:plain 引用 - Kubernetesを軸に再定義されつつある、新しい「クラウド対応」の意味とは

これを仮に第2世代と呼ぶとします。JavaEEではありませんがSpringBootも第2世代ですね。

さて、ここからがQuarkusの話ですが第2世代ではまだ不十分です。 それはコンテナ化とマイクロサービス化が進んだ結果としてサーバレスが注目され始めたからです。 実のところサーバレスなアプリケーション実行環境というのは昔からあります。代表的なのはHerokuとGoogle AppEngine(GAE)です。 これらのサービスはリクエストに応じて処理をスケールさせることができ、使ってないときはインスタンスが起動してないのでコストを払う必要がありません。 この構成はコンテナと非常に相性が良い(というか上記サービスは元々独自コンテナでそれを実現していた)ので、リソースやコスト(含む運用コスト)の効率化のために再度注目されてると思います。 そこで重要になってくるのは要求があってから起動してリクエストを返すまでの時間、つまりスピンアップタイムです。なぜならサーバレスではインスタンスが一つも起動してないことは普通にあり得るので。 これはスケールアウトはあっても基本的には常駐させることが前提の従来的なマイクロサービスの管理とは要件が異なります。 これを第3世代---は少し言い過ぎなので2.5世代と呼ぶします。@kisからコメントがあったMicronautJavaEEでは無いですが2.5世代ですね。これもGraalVM使ってネイティブコンパイルしています。

Table: JavaEEコンテナの世代の比較

Item 第1世代 第2世代 第2.5世代
Product Wildfly, GlassFish Payara-Micro, Thorntail Quarkus, Helidon
サポート機能 Full JavaEE Micro Profile Micro Profile
スピンアップ 分オーダー 秒オーダー ミリ秒オーダー
Challenge 分散システムの管理 他のMicro Serviceとの統合 スピンアップタイムの高速化

ミリ秒オーダーの起動を実現するには従来のコンポーネントを徹底的に見直す必要があります。 また、基本的にはリクエスト毎にコンテナが立ち上がれば良いのでスレッド管理周りも強力なものは不要ですし、コンテナでデプロイするのでクラスローダ周りもシンプルにできます。 また、GraalVMのnative-imageによるバイナリへのコンパイルが大きな鍵になります。その際にSpringBoot等も含めて既存のモジュールではビルドが困難なので利用可能なものだけを組み合わせるためフルスクラッチで作り直してるのだと思います。 OracleHelidonもnative-imageはまだ実現していませんがコンセプトは同様です。

Quarkus

さて前置きが長くなりましたがQuarkusについてです。 MiroProfileによりJAX-RS, CDI, JPA, Config, OpenAPI, Tracingなどが使えます。それに加えてVert.xやKafkaとの連携も組み込まれています。 それでいて爆速起動/省メモリなのが特徴です。

f:id:pascal256:20190415053510p:plain 引用 - Quarkus公式

また、ビルド形式をJARとnative-imageの2種類用意してあります。

まずはmvnコマンドでプロジェクトを作成します。

mvn io.quarkus:quarkus-maven-plugin:create        
....
[INFO] --- quarkus-maven-plugin:0.13.1:create (default-cli) @ standalone-pom ---
Set the project groupId [org.acme.quarkus.sample]: 
Set the project artifactId [my-quarkus-project]: 
Set the project version [1.0-SNAPSHOT]: 
Do you want to create a REST resource? (y/n) [no]: y
Set the resource classname [org.acme.quarkus.sample.HelloResource]: 
Set the resource path  [/hello]: 
Creating a new project in /private/tmp/my-quarkus-project
Configuration file created in src/main/resources/META-INF/application.properties

...

[INFO] BUILD SUCCESS

続いて実行してみます。以下のコマンドでホットデプロイが効く開発モードで起動が可能です。

$ mvn compile quarkus:dev
...
2019-04-14 13:31:50,697 INFO  [io.qua.dep.QuarkusAugmentor] (main) Beginning quarkus augmentation
2019-04-14 13:31:51,988 INFO  [io.qua.dep.QuarkusAugmentor] (main) Quarkus augmentation completed in 1291ms
2019-04-14 13:31:52,564 INFO  [io.quarkus] (main) Quarkus 0.13.1 started in 1.966s. Listening on: http://[::]:8080
2019-04-14 13:31:52,566 INFO  [io.quarkus] (main) Installed features: [cdi, resteasy]

curlで実行してみます。

$ curl -i http://localhost:8080/hello
HTTP/1.1 200 OK
Connection: keep-alive
Content-Type: text/plain;charset=UTF-8
Content-Length: 5
Date: Sun, 14 Apr 2019 20:33:55 GMT

hello% 

次にネイティブイメージにコンパイルして実行します。 ネイティブイメージへのビルドは依存関係を全部コンパイルし直してるのでちょっと時間がかかります。

$ export GRAALVM_HOME=/Applications/Graalvm.app/Contents/Home/
$ mvn package -Pnative

実行してみましょう。

まずはJVMから。

% java -jar my-quarkus-project-1.0-SNAPSHOT-runner.jar   
...
INFO: Quarkus 0.13.1 started in 1.356s. Listening on: http://[::]:8080
Apr 14, 2019 2:07:52 PM io.quarkus.runtime.Timing printStartupTime
INFO: Installed features: [cdi, resteasy]

1.3秒で起動しました。まあ悪くない数字です。第2世代と比べても速いですし、第1世代では質レベルで違う速度。 続いて使用メモリをチェックしてみましょう。

% ps aux 18124
USER     PID  %CPU %MEM      VSZ    RSS   TT  STAT STARTED      TIME COMMAND
koduki 18124   0.0  0.2  8014528  18684 s002  S+    2:07PM   0:03.41 /usr/bin/java -jar my-quarkus-project-1.0-SNAPSHOT

8MB程度使っていますね。いつも1GBはおろか32GBとか平気でJavaに指定してる身からすれば劇的に小さいです。

では続いてネイティブバージョン。

$ ./my-quarkus-project-1.0-SNAPSHOT-runner     
2019-04-14 13:59:01,168 INFO  [io.quarkus] (main) Quarkus 0.13.1 started in 0.011s. Listening on: http://[::]:8080
2019-04-14 13:59:01,303 INFO  [io.quarkus] (main) Installed features: [cdi, resteasy]

0.011秒という劇的な速度で起動してることが分かります。10msとか並みのレスポンス速度よりも速いですね。。。 続いて使用メモリ。

$ ps aux 18084          
USER     PID  %CPU %MEM      VSZ    RSS   TT  STAT STARTED      TIME COMMAND
koduki 18084   0.0  0.1  4353676   4448 s002  S+    1:59PM   0:00.04 ./my-quarkus-project-1.0-SNAPSHOT-runne
/Users/koduki% 

こちらも4MB程度でJVM版よりもさらに小さい使用量となります。

比べると以下の通り。

Item JVM Native
起動速度(sec) 1.356 0.011
使用メモリ(MB) 7.6 4.2

まとめ

Quarkusが爆速であることと、なぜその速度がServerless時代には求められるかを書きました。 このレベルの速度で起動するならプロセス常駐させる必要はなく、結果としてJavaの鬼門の一つであるGCも実質解決ができます。 つまり、QuarkusはJavaの難点と言われていたスピンアップタイムとGCを改善したクラウドネイティブなJavaEE環境と言えるかと思います。 OracleのHelidonも同じ路線のはずなので、いい感じに刺激しあえると良いですね。

追加: ブコメで「これCGIじゃね?」と言うのがありましたが私も同感です。特にFaaSやKNativeは今風の良い感じなCGI2.0的なものかと。 実のところCGIはスレッドではなくプロセスなので取り回しは悪くなかったと思うのですがパフォーマンスの問題がありました。 で、現在のインフラだとリソースを安全にシェアできるので横に必要に応じてたくさん並べれば多少効率落ちても良くね? と言うことで回帰してると思っています。

それではHappy Hacking!

参考