Quarkusについて調べてみたらハマった話

今年も参加してみました。
滑り込みアウトですが、[ALH Advent Calendar 2020](https://qiita.com/advent-calendar/2020/alhinc)の6日目です。

前日は[「なんかDB遅いな。調べておいて～」って振られた時に見ること](https://qiita.com/wol/items/2555f9dd359198c9ca69)-wolさんでした。

# Quarkusとは何ぞや
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/297163/78ad5c3b-debe-f3c1-4c6c-725dae428cfd.png)

## きっかけ
普段、マイクロサービスやコンテナ周りの技術習得を目指し勉強しているのですが、Kubernetes向け軽量コンテナを実現するスタックとしてRedHatが何か作っているらしいぞ、という情報を耳にしたため調べてみました。


## Quarkus
[Quarkus - SUPERSONIC SUBATOMIC JAVA](https://quarkus.io/)
名前からしてそそられますね。

>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](https://12factor.net/)のアーキテクチャを採用している
- 手続き型とリアクティブ型の統合：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ソースコード<br>またはバイトコード|ネイティブコードの実行ファイル|
|動的コンパイラ(JIT)|メモリ上のjavaバイトコード|メモリ上のネイティブコード|

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/297163/20424dfc-3bf0-2794-abb8-b7240fd1361d.png)

JITコンパイラはデフォルトで有効になっており、Javaメソッドが呼び出されるとアクティブになります。 JITコンパイラは、そのメソッドのバイトコードをネイティブコードにコンパイルし、実行するために**"just in time"**コンパイルします。

### ここでGraalVM
以上を踏まえてGraalVMの特徴は、GraalというJITコンパイラが実装されています。JVM Compiler Interfaceを利用してC2コンパイラを置き換えます。

初めはC1を使い、HotSpotがさらに多くの呼び出しを検知するとメソッドはC2を使って再度コンパイルされます。ほとんどのJavaアプリケーションにとって、C2コンパイラが環境のもっとも重要なパーツの1つであり、これがプログラムのもっとも重要な部分に対し、高度に最適化されたマシンコードを生成するからです。

そのC2の元々c++で実装されている箇所が、Javaで置き換えられ、最適化技術に改良が加えられ、パフォーマンスが向上しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/297163/0079502d-a98d-b38e-5b47-e3a0bbb3dcb7.png)

### Native Image
GraalVMではJavaのコードをAOTコンパイルすることによりNative Image化を行なうことができます。AOTコンパイルとはJIT **"just in time"**（実行時）コンパイルではなく、**"ahead of time"**事前コンパイルを行うことです。

Native Image化を行なうと以下のような特徴を得ることができます。

- アプリ起動時にクラスロードや初期化処理が不要になるため、起動が早くなる
- メモリフットプリント（リソース占有）を削減することができる

以上から、スケールする軽量コンテナと相性がいいというわけですね。

### その他
GraalVMの特徴に多言語に対応できるTruffleという仕組みがありますが、本題ではないのでここでは割愛します。
GraalVM上ではJavaScriptやRubyも実行できます。

[困らない程度のJDK入門 - slideshare](https://www.slideshare.net/yodajp/jdk-73391996)
[Getting to Know Graal, the New Java JIT Compiler - InfoQ](https://www.infoq.com/articles/Graal-Java-JIT-Compiler/)
[JVM JITコンパイラの仕組み - Qiita](https://qiita.com/kinshotomoya/items/c66954aee31f06db99a0)
[GraalVMに入門する - Uzabase Tech Blog](https://tech.uzabase.com/entry/2020/01/27/090000)
[GraalVM の概要と、Native Image 化によるSpring Boot 爆速化の夢 - slideshare](https://www.slideshare.net/apkiban/graalvm-native-image-spring-boot)

# GraalVMのNativeImage化をやってみた
よしよし、なんとなく仕組みはわかりました。なので実際にGraalVMのNative Image化してみましょう。

## 前提
- macOS Catalina
- zsh
- Java 11
- graalvm-ce

## 準備
### GraalVMのインストール
公式の手順に従って設定していきます。

https://www.graalvm.org/docs/getting-started-with-graalvm/macos/

以下のリリースからパッケージをダウンロードします。
https://github.com/graalvm/graalvm-ce-builds/releases
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/297163/a03cc27d-b7fa-e0bd-ecb7-6ee47e78ad65.png)

解凍します。

```terminal
$ tar -xvf graalvm-ce-java11-darwin-amd64-20.30.0.tar.gz
```

macOSの場合、Javaのシステムディレクトリに移動させます。

```terminal
$ sudo mv graalvm-ce-java11-20.3.0 /Library/Java/JavaVirtualMachines
```
※パスワード入力が求められます。

インストールしたGraalVMが表示されることを確認します。

```terminal
$ /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](https://qiita.com/seijikohara/items/56cc4ac83ef9d686fab2)

当環境はzshなので`.zsh_profile`に以下追記します

```vim
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変更前

```terminal
$ 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変更後

```terminal
$ 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](https://www.graalvm.org/reference-manual/graalvm-updater/)もインストールされるので以下のコマンドを実行します。

```terminal:terminal
$ gu install native-image
```

## コーディング
[GraalVM公式のリファレンス](https://www.graalvm.org/reference-manual/native-image/)に沿ってNativeImageを試しに実装してみます。
再帰呼び出しにより文字列を反転させます。

```terminal:terminal
$ mkdir graalvm
$ cd graalvm 
```

```java:Sample.java
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);
    }
}
```

実行ファイルのビルドの分だけ時間がかかりますが、このレベルのクラスの実行速度でも全然違いますね！

```terminal:javac
$ 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
```

```temrinal:native-image
$ 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](https://spring.io/blog/2020/11/23/spring-native-for-graalvm-0-8-3-available-now)
※2020/11/23時点
正式版ではないですが開発は進められているようなので期待しましょう！[詳細はこちら](https://github.com/spring-projects-experimental/spring-graalvm-native)

それとは別にQuarkusでもSpring対応しているようなので、今回は[SpringWeb](https://quarkus.io/guides/spring-web)を試してみたいと思います。

## Archetypeの取得
MavenのArchetypeが用意されているので取得します。

```terminal:terminal
$ 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`を見てみると専用のプラグインも用意されているようですね。

```xml: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`がいるのでとりあえず実行してみます。

```java:GreetingController.java
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`というファイルがあるのでこれに従ってビルドします。

```terminal:terminal
$ ./mvnw package -Pnative
```

イメージビルドします。

```terminal:terminal
$ 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
```

起動します。

```terminal:terminal
$ 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が起動しない](https://qiita.com/reireias/items/03bfdaeb7f3afd7c4339)場合と同様、ビルド環境と実行環境が同一CPUアーキテクチャでないとダメなようです。ネイティブコードの時点で確かにそうですね。。

Linux上の仮想環境上でビルドデプロイしないとなので、今回はタイムアップで次回試してみようと思います、というところでお茶を濁しちゃいます。

# 感想
結論、QuarkusのメリットであるGraalVMを使用したNativeImage化までできなかったのですが、SpringスタックもNativeImageに対応するそうなので、今後注視していきたいと思います。

まだ新しめの技術スタックなので、今後のRedHatにも期待ですが、現行システムでRHELの場合は、RedHat公式イメージ（ubi）もあり相性はよいのではと思ってます。

# 参考
[spring-graal-nativeでSpring BootをGraalVM native imageにしてみる - Qiita](https://qiita.com/tkani/items/37878d268db54d581a1b)
[Quarkus: コンテナ上で Java アプリを高速起動する新しい手法のご紹介](https://yoshio3.com/2019/03/11/try-quarkus/)
[JavaアプリをNativeコンパイルして爆速で起動するQuarkusを試してたら利用例にプルリクエストがマージされた - HatenaBlog](https://kdnakt.hatenablog.com/entry/2019/03/15/080000
)