GAE + Java 11 + Quarkusってどんなもんよ

7 min read読了の目安(約6800字

基本的に今までTypeScript + Node.jsで書いてましたが、そろそろJVMを書きたいという気持ちが出てきました。
ただし、Standard環境のGAEは良いものだと知ってしまった、、、ということでJava 11でかけないかなと思いました。
GAE + Java 11を利用する上で考えるのは、 初回リクエストのレスポンス速度 (JVMの起動速度+アプリケーションの起動速度) が問題になるかと思います。
では、高速に起動する(?)と言われるQuarkusを使って見たらどうだろうと思い、ちょっと調査してみました。

Javaと言いながらKotlinで作ってますが、あんまり変わらない(と思っている)ので、温かい目で見てください

とりあえずGAE上で動かしてみる

実装は以下の記事を参考にしております。

https://quarkify.net/how-to-deploy-quarkus-on-google-app-engine/

上記サンプルの通り、GAEはスタンダード環境になります。
今回のアプリケーションはダウンロードしてきたものに以下を追加しただけのとてもシンプルなものになっています。

HelloResource.kt
import javax.ws.rs.GET
import javax.ws.rs.Path
import javax.ws.rs.Produces
import javax.ws.rs.core.MediaType

@Path("/greeting")
class GreetingResource {
    @GET
    @Produces(MediaType.TEXT_PLAIN)
    fun hello() = "hello"
}

とりあえず起動してみました(起動より前に最初に200が返っていますね。。。なぞ)

2020-11-07 08:17:26 api[0-0-1]  "GET /greeting HTTP/1.1" 200
2020-11-07 08:17:30 api[0-0-1]  __  ____  __  _____   ___  __ ____  ______
2020-11-07 08:17:30 api[0-0-1]   --/ __ \/ / / / _ | / _ \/ //_/ / / / __/
2020-11-07 08:17:30 api[0-0-1]   -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \
2020-11-07 08:17:30 api[0-0-1]  --\___\_\____/_/ |_/_/|_/_/|_|\____/___/
2020-11-07 08:17:30 api[0-0-1]  2020-11-07 08:17:30,220 INFO  [io.quarkus] (main) api 1.0.0-SNAPSHOT on JVM (powered by Quarkus 1.9.2.Final) started in 2.858s. Listening on: http://0.0.0.0:8081
2020-11-07 08:17:30 api[0-0-1]  2020-11-07 08:17:30,266 INFO  [io.quarkus] (main) Profile prod activated.
2020-11-07 08:17:30 api[0-0-1]  2020-11-07 08:17:30,267 INFO  [io.quarkus] (main) Installed features: [cdi, jdbc-mysql, kotlin, mutiny, reactive-mysql-client, resteasy, resteasy-jackson, smallrye-context-propagation, vertx]

初回アクセスは4秒程度。その後は200ミリ秒未満。
起動時のログに (main) api 1.0.0-SNAPSHOT on JVM (powered by Quarkus 1.9.2.Final) started in 2.858s
ってありますね。JVMとアプリケーションの起動は4:6か5:5ぐらいの起動時間でしょうか?
まぁ、この件は一旦おいておきます。(ネタバレ

チューニングしてみる

この状態でのチューニングをしてみます。
初回アクセス以外は全く問題ないので、これならはじめから1インスタンス立てておいたほうが良いと思います。
そのためにはapp.yamlを修正します。具体的にはmin_instancesの数値を1にします

app.yaml
runtime: java11
instance_class: F1
service: api

automatic_scaling:
  min_instances: 1

一応これで常に1インスタンスが立ち上がります。(もちろん課金されます
しかし、ぐっとアクセス数が増えてきたら。。。やはりJVMの起動を待つことが許されないこともあるかもしれません。
そのため、「起動している後ろでアクセスを待ち受けておく(スタンバイしておく)instanceが何個あるか」という設定がありますので、これを増やしておくと良いでしょう(個人的には数秒ぐらい良いじゃないかという気持ち)

app.yaml
runtime: java11
instance_class: F1
service: api

automatic_scaling:
  min_idle_instances: 1
  min_instances: 1

もちろんこれもお金かかります。上記なら常に2インスタンスが立っていることになります。
一応これで対応できましたね!!

でも本当にこれで良いんですかね?

ほんとうの意味(?)でのチューニング

問題となっているのは今の所、初期起動だけです(最初っから言ってる)。じゃあ、これを本当の意味で早く立ち上げるにはどうすればいいでしょうか。
ということで一旦思いつくのが「native imageを使ってみたらどうなるのか?」になります。

アプリケーションをnative imageにする

Quarkusでnative imageを作る方法はREADMEにもある通りです。

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

一応docker imageとして起動するには以下のとおりです(起動するかどうかは確認しておきましょう

docker build -f src/main/docker/Dockerfile.native -t quarkus/api .
docker run -i --rm -p 8080:8080 quarkus/api

GAEの設定を変更する

このdocker imageをGAEで起動するには custom ランタイムという形で動かす必要が出てきます

https://cloud.google.com/appengine/docs/flexible/custom-runtimes/build

そのためapp.yamlを以下のように修正していきます

app.yaml
runtime: custom
env: flex
service: api

readiness_check:
  path: "/greeting"

readiness_check:
  path: "/greeting"

automatic_scaling:
  min_num_instances: 1
  cool_down_period_sec: 180

そして、一度ローカルで作成したdocker imageをGoogle Container Registory(GCR)にあげておく必要があります。
そのため、先程のdocker imageとしての起動の際のコマンドにおけるbuild部分を以下のようにして、GCRにpushします(GCRへのpushの細かい部分は省略しますが、きちんと認証を通しておきましょうね)

docker build -f src/main/docker/Dockerfile.native -t asia.gcr.io/[your-project-id]/api .
docker push asia.gcr.io/[your-project-id]/api

今ほどpushしたimageを使うようにdeployするには以下のようなコマンドになります(app.yamlの場所は変更していない体です)

gcloud app deploy src/main/appengine/app.yaml --image-url asia.gcr.io/[your-project-id]/api

数分待ち、deployが終わったら、URLにアクセスしてみます

2020-11-07 10:58:43 api[20201107t195005]  __  ____  __  _____   ___  __ ____  ______
2020-11-07 10:58:43 api[20201107t195005]   --/ __ \/ / / / _ | / _ \/ //_/ / / / __/
2020-11-07 10:58:43 api[20201107t195005]   -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \
2020-11-07 10:58:43 api[20201107t195005]  --\___\_\____/_/ |_/_/|_/_/|_|\____/___/
2020-11-07 10:58:43 api[20201107t195005]  2020-11-07 10:58:43,718 INFO  [io.quarkus] (main) api 1.0.0-SNAPSHOT native (powered by Quarkus 1.9.2.Final) started in 0.054s. Listening on: http://0.0.0.0:8080
2020-11-07 10:58:43 api[20201107t195005]  2020-11-07 10:58:43,718 INFO  [io.quarkus] (main) Profile prod activated.
2020-11-07 10:58:43 api[20201107t195005]  2020-11-07 10:58:43,718 INFO  [io.quarkus] (main) Installed features: [cdi, jdbc-mysql, kotlin, mutiny, reactive-mysql-client, resteasy, resteasy-jackson, smallrye-context-propagation, vertx]
2020-11-07 10:59:09 api[20201107t195005]  "GET /greeting" 200

先程みた起動時間はものすごいことになりました。

(main) api 1.0.0-SNAPSHOT native (powered by Quarkus 1.9.2.Final) started in 0.054s. Listening on: http://0.0.0.0:8080

実際のアクセスと、起動時間の差があるのはhealthcheckのためです。

はてさて、トレースはどうなったかというと、、、自分で作成したimageになるため、でてこなくなりました。。。
このあたりはtraceを設定すればいけます https://cloud.google.com/trace/docs/setup?hl=ja (今回はこの設定は除外させてもらいます)

ただし

app.yaml
env: flex

になっているため、最低1インスタンス立たないと行けないということです。(せっかく起動時間早くしたのに。。。)

一旦まとめ

Standard環境GAE上でJava(Kotlin)も出来ますね。考えても良い気がしてきました。
ただし、Native Imageを使うのであれば、traceなどのGAEの恩恵が受けにくくなるので、正直Cloud Runも検討してもいいかと思います。

そして気になる料金比較(Cloud Run、GCE)

上記の様にNative Imageを使う場合での 2020/11/07 の時点での料金は以下のようになります。
asia-northeast1としております

GAE(フレキシブル環境) Cloud Run GCE(CoreOS)
無料枠 無料割り当てなし 毎月最初の 180,000 vCPU 秒は無料 無料割り当てなし

この時点でCloud Runのほうが良いですね。。。
そして、そのまま1ヶ月動かし続け、定常的にリクエストが来る場合、(GAEの場合は1インスタンスのままという考え)

GAE(フレキシブル環境) Cloud Run GCE(CoreOS)
1CPU/persec、256MBメモリ/persec 1CPU、256MBメモリ、(1インスタンスのリクエスト受付数50、1リクエスト 500ms、 1ヶ月でのリクエスト数 3百万(1秒1アクセスで259万なので切り上げ)) 1CPU、1GB(0.9GB以下に出来なかった)
値段 JPY 5,247.74 JPY 41.70 JPY 3,320 per 1 month、(30%ディスカウントがあれば JPY 2,423.79 per 1 month)

https://cloud.google.com/products/calculator/#id=4d2688d9-ddca-430b-9d3c-b6bdc4f4d291

定常的に動かすようなアプリケーションで簡単に Native Image を使う場合は完全に Cloud Runにしたほうが良さそうですね。
*ちなみにCloud Runのアクセス数を10倍(1秒10アクセスがずっと続く)にしても JPY 1,467.98 なので恐らくGAEの1インスタンスで起動し続けるよりは低コストになりそう、、、(見当違いなどあればご指摘ください)

まとめ

ちょっと上記までの結果(Native Imageを使う場合)でGAEとCloud RunとGCEを個人見解でまとめてみます

GAE(フレキシブル環境) Cloud Run GCE(CoreOS)
version管理
手軽さ
自動スケーリング
値段(上記の情報のもので) JPY 5,247.74 JPY 41.70 JPY 3,320

こうなると、Native ImageにするときにGAEを使う意味はあんまりなさそうですね。
餅は餅屋、GAEを使うならフレキシブルでもデフォルトのコンテナを使うなどしたほうが良さそうです。

個人的なGAEスタンダード環境でのアプリケーション開発では、deployも簡単だし、トレースなどもとってくれるので、起動時間は特に気にしないよーって人には、ぜひおすすめしたいです。

version管理における最低インスタンスの管理だけはきちんとしましょうね!(多額の金額が課金された人)