今年も参加してみました。
滑り込みアウトですが、ALH Advent Calendar 2020の6日目です。
前日は「なんかDB遅いな。調べておいて~」って振られた時に見ること-wolさんでした。
Quarkusとは何ぞや
きっかけ
普段、マイクロサービスやコンテナ周りの技術習得を目指し勉強しているのですが、Kubernetes向け軽量コンテナを実現するスタックとしてRedHatが何か作っているらしいぞ、という情報を耳にしたため調べてみました。
Quarkus
Quarkus - SUPERSONIC SUBATOMIC JAVA
名前からしてそそられますね。
A Kubernetes Native Java stack tailored for OpenJDK HotSpot and GraalVM, crafted from the best of breed Java libraries and standards.
- コンテナファースト:コンテナでの実行に最適化された最小フットプリント(リソース占有が少ない)のJavaアプリケーション
- クラウドネイティブ:Kubernetes環境のようにThe Twelve-Factor Appのアーキテクチャを採用している
- 手続き型とリアクティブ型の統合:1つのプログラミングモデルの下で、命令型と非ブロッキングの開発スタイルを実現する
- スタンダードに基づく:Java標準と各種フレームワークに対応(RESTEasy and JAX-RS, Hibernate ORM and JPA, Netty, Eclipse Vert.x, Eclipse MicroProfile, Apache Camel...)
- マイクロサービスファースト:非常に高速な起動時間
- 開発者にやさしい:妥協のない開発中心のエクスペリエンス
引用:https://github.com/quarkusio/quarkus
OpenJDK HotSpotおよびGraalVM用に調整されたKubernetesネイティブJavaスタックだそうです。
Javaはアプリケーションの起動が遅いなんてよく言われますが、アプリケーションのコンテナ化が主流になってきた中で、リソース弾力性のあるアプリが求められる中、スケールアウト時の挙動が遅いと瞬間的なアクセスに対応できません。
GraalVMという仮想マシンを使って起動のリードタイム短縮するためのスタックが生まれた、という背景があるようですね。
そもそもGraalVMって何?
その前にJVMの話
JavaはJVM上で動くという、初歩的な話から進めます。
最近は、Webアプリケーション開発から入ってEclipse上で開発だ!が主流だとクラスファイル*.class
が生成されてjar
とかwar
で固められる、ぐらいの理解はあっても、実際にjavac
を使ってコンパイルする、という機会はほとんどないと思います。
ひと昔前はAntビルド、最近はMavenが主流ですね。
知らなくてもよいことを知る必要がない、のはライブラリなどに代表されるようにカプセル化・隠ぺいされることはよいことなのですが、パフォーマンスチューニングとか障害発生時とか、仕組みを知っていないと困る時もあります。
初級プログラマーを抜けていくためのステップですね。プログラマにコンピュータサイエンスが必要かどうかという話もありますがこの話はここでは割愛します。
また、知らないことは必要になったときに調べればよいですが、何も知らないと調べること自体が大変になります。
Javaはコンパイラ言語ということは知ってると思いますが、コンパイラにも種類があります。
コンパイラの種類 | 何から | 何へ |
---|---|---|
Javaコンパイラ | javaソースコード(*.java) | クラスファイル(*.class) |
ネイティブコンパイラ | javaソースコード またはバイトコード |
ネイティブコードの実行ファイル |
動的コンパイラ(JIT) | メモリ上のjavaバイトコード | メモリ上のネイティブコード |
JITコンパイラはデフォルトで有効になっており、Javaメソッドが呼び出されるとアクティブになります。 JITコンパイラは、そのメソッドのバイトコードをネイティブコードにコンパイルし、実行するために"just in time"コンパイルします。
ここでGraalVM
以上を踏まえてGraalVMの特徴は、GraalというJITコンパイラが実装されています。JVM Compiler Interfaceを利用してC2コンパイラを置き換えます。
初めはC1を使い、HotSpotがさらに多くの呼び出しを検知するとメソッドはC2を使って再度コンパイルされます。ほとんどのJavaアプリケーションにとって、C2コンパイラが環境のもっとも重要なパーツの1つであり、これがプログラムのもっとも重要な部分に対し、高度に最適化されたマシンコードを生成するからです。
そのC2の元々c++で実装されている箇所が、Javaで置き換えられ、最適化技術に改良が加えられ、パフォーマンスが向上しました。
Native Image
GraalVMではJavaのコードをAOTコンパイルすることによりNative Image化を行なうことができます。AOTコンパイルとはJIT "just in time"(実行時)コンパイルではなく、"ahead of time"事前コンパイルを行うことです。
Native Image化を行なうと以下のような特徴を得ることができます。
- アプリ起動時にクラスロードや初期化処理が不要になるため、起動が早くなる
- メモリフットプリント(リソース占有)を削減することができる
以上から、スケールする軽量コンテナと相性がいいというわけですね。
その他
GraalVMの特徴に多言語に対応できるTruffleという仕組みがありますが、本題ではないのでここでは割愛します。
GraalVM上ではJavaScriptやRubyも実行できます。
困らない程度のJDK入門 - slideshare
Getting to Know Graal, the New Java JIT Compiler - InfoQ
JVM JITコンパイラの仕組み - Qiita
GraalVMに入門する - Uzabase Tech Blog
GraalVM の概要と、Native Image 化によるSpring Boot 爆速化の夢 - slideshare
GraalVMのNativeImage化をやってみた
よしよし、なんとなく仕組みはわかりました。なので実際にGraalVMのNative Image化してみましょう。
前提
- macOS Catalina
- zsh
- Java 11
- graalvm-ce
準備
GraalVMのインストール
公式の手順に従って設定していきます。
以下のリリースからパッケージをダウンロードします。
https://github.com/graalvm/graalvm-ce-builds/releases
解凍します。
$ tar -xvf graalvm-ce-java11-darwin-amd64-20.30.0.tar.gz
macOSの場合、Javaのシステムディレクトリに移動させます。
$ sudo mv graalvm-ce-java11-20.3.0 /Library/Java/JavaVirtualMachines
※パスワード入力が求められます。
インストールしたGraalVMが表示されることを確認します。
$ /usr/libexec/java_home -V
Matching Java Virtual Machines (3):
12, x86_64: "Java SE 12" /Library/Java/JavaVirtualMachines/jdk-12.jdk/Contents/Home
11.0.9, x86_64: "GraalVM CE 20.3.0" /Library/Java/JavaVirtualMachines/graalvm-ce-java11-20.3.0/Contents/Home
11.0.2, x86_64: "Java SE 11.0.2" /Library/Java/JavaVirtualMachines/jdk-11.0.2.jdk/Contents/Home
/Library/Java/JavaVirtualMachines/jdk-12.jdk/Contents/Home
以下、環境によって違いますが、プロファイルでPATHの変更をします。
参考:MacのBrewで複数バージョンのJavaを利用する + jEnv
当環境はzshなので.zsh_profile
に以下追記します
export JAVA_HOME=/Library/Java/JavaVirtualMachines/graalvm-ce-java11-20.3.0/Contents/Home
# インストールしているJavaバージョンによっては以下でも可能です
# export JAVA_HOME=`/usr/libexec/java_home -v "11"`
PATH=${JAVA_HOME}/bin:${PATH}
PATH変更前
$ java -version
java version "11.0.2" 2019-01-15 LTS
Java(TM) SE Runtime Environment 18.9 (build 11.0.2+9-LTS)
Java HotSpot(TM) 64-Bit Server VM 18.9 (build 11.0.2+9-LTS, mixed mode)
PATH変更後
$ java -version
openjdk version "11.0.9" 2020-10-20
OpenJDK Runtime Environment GraalVM CE 20.3.0 (build 11.0.9+10-jvmci-20.3-b06)
OpenJDK 64-Bit Server VM GraalVM CE 20.3.0 (build 11.0.9+10-jvmci-20.3-b06, mixed mode, sharing)
Native Imageのインストール
NativeImageビルドの機能を利用したいので、こちらもインストールします。
GraalVMをインストールすると、GraalVM Updater Toolもインストールされるので以下のコマンドを実行します。
$ gu install native-image
コーディング
GraalVM公式のリファレンスに沿ってNativeImageを試しに実装してみます。
再帰呼び出しにより文字列を反転させます。
$ mkdir graalvm
$ cd graalvm
public class Sample {
public static void main(String[] args) {
String str = "Native Image is awesome";
String reversed = reverseString(str);
System.out.println("reverse: " + reversed); // reverse: mosewa si egamI evitaN
}
private static String reverseString(String str) {
if (str.isEmpty())
return str;
return reverseString(str.substring(1)) + str.charAt(0);
}
}
実行ファイルのビルドの分だけ時間がかかりますが、このレベルのクラスの実行速度でも全然違いますね!
$ time javac Sample.java
javac Sample.java 1.09s user 0.20s system 106% cpu 1.210 total
$ time java Sample
reverse: emosewa si egamI evitaN
java Sample 0.15s user 0.08s system 43% cpu 0.534 total
$ time javac Sample.java
# コンパイルまでは同様のため割愛
$ time native-image Sample
[sample:22975] classlist: 1,220.44 ms, 0.96 GB
[sample:22975] (cap): 3,556.39 ms, 0.96 GB
[sample:22975] setup: 4,944.21 ms, 0.96 GB
[sample:22975] (clinit): 157.30 ms, 1.22 GB
[sample:22975] (typeflow): 4,157.74 ms, 1.22 GB
[sample:22975] (objects): 3,755.36 ms, 1.22 GB
[sample:22975] (features): 200.18 ms, 1.22 GB
[sample:22975] analysis: 8,530.65 ms, 1.22 GB
[sample:22975] universe: 299.25 ms, 1.22 GB
[sample:22975] (parse): 1,030.67 ms, 1.22 GB
[sample:22975] (inline): 923.63 ms, 1.67 GB
[sample:22975] (compile): 6,578.55 ms, 2.28 GB
[sample:22975] compile: 9,004.62 ms, 2.28 GB
[sample:22975] image: 1,189.74 ms, 2.28 GB
[sample:22975] write: 305.47 ms, 2.28 GB
[sample:22975] [total]: 25,641.41 ms, 2.28 GB
native-image Sample 107.22s user 3.87s system 415% cpu 26.733 total
$ time ./sample
reverse: emosewa si egamI evitaN
./sample 0.00s user 0.00s system 1% cpu 0.343 total
実行方法 | コンパイル時間 | ビルド時間 | 起動時間 |
---|---|---|---|
クラスファイル | 1.09秒 | - | 0.15秒 |
ネイティブコード | 同上 | 107.22秒 | 0.00秒 |
msecでないと測れないようです。
Quarkusを使ってみる
ここから本題。今まではGraalVMの説明でした。では、実際にQuarkusを使ってみましょう。
GraalVMとしては、SpringはDIコンテナをはじめとして、リグレクションやダイナミックプロキシによる動的なクラス生成を多用しているため、Native Image化とは相性が悪い=対応していないようです。
2019年時点では対応中ですとのことでしたが、2020年も厳しいようです。
Spring Native for GraalVM 0.8.3 available now - spring.io
※2020/11/23時点
正式版ではないですが開発は進められているようなので期待しましょう!詳細はこちら
それとは別にQuarkusでもSpring対応しているようなので、今回はSpringWebを試してみたいと思います。
Archetypeの取得
MavenのArchetypeが用意されているので取得します。
$ mvn io.quarkus:quarkus-maven-plugin:1.10.2.Final:create \
-DprojectGroupId=org.acme \
-DprojectArtifactId=spring-web-quickstart \
-DclassName="org.acme.spring.web.GreetingController" \
-Dpath="/greeting" \
-Dextensions="spring-web"
pom.xml
を見てみると専用のプラグインも用意されているようですね。
<plugin>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-maven-plugin</artifactId>
<version>${quarkus-plugin.version}</version>
<extensions>true</extensions>
<executions>
<execution>
<goals>
<goal>build</goal>
<goal>generate-code</goal>
<goal>generate-code-tests</goal>
</goals>
</execution>
</executions>
</plugin>
Controller
がいるのでとりあえず実行してみます。
package org.acme.spring.web.spring.web;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/greeting")
public class GreetingController {
@GetMapping
public String hello() {
return "Hello Spring";
}
}
ビルド
公式リファレンスには開発モードでの実行が紹介されていますが、せっかくなのでdockerで動かしてみます。
spring-web-quickstart/src/docker
にDockerfile.native
というファイルがあるのでこれに従ってビルドします。
$ ./mvnw package -Pnative
イメージビルドします。
$ docker build -f src/main/docker/Dockerfile.native -t quarkus/spring-web-quickstart .
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
quarkus/spring-web-quickstart latest df12a35b655b About a minute ago 138MB
起動します。
$ docker run -i --rm -p 8080:8080 quarkus/spring-web-quickstart
standard_init_linux.go:211: exec user process caused "exec format error"
あら。。。エラーで立ち上がりません。
調べてみると、Rasberry-PIでdockerが起動しない場合と同様、ビルド環境と実行環境が同一CPUアーキテクチャでないとダメなようです。ネイティブコードの時点で確かにそうですね。。
Linux上の仮想環境上でビルドデプロイしないとなので、今回はタイムアップで次回試してみようと思います、というところでお茶を濁しちゃいます。
感想
結論、QuarkusのメリットであるGraalVMを使用したNativeImage化までできなかったのですが、SpringスタックもNativeImageに対応するそうなので、今後注視していきたいと思います。
まだ新しめの技術スタックなので、今後のRedHatにも期待ですが、現行システムでRHELの場合は、RedHat公式イメージ(ubi)もあり相性はよいのではと思ってます。
参考
spring-graal-nativeでSpring BootをGraalVM native imageにしてみる - Qiita
Quarkus: コンテナ上で Java アプリを高速起動する新しい手法のご紹介
JavaアプリをNativeコンパイルして爆速で起動するQuarkusを試してたら利用例にプルリクエストがマージされた - HatenaBlog