【Micronaut 2.x on AWS Lambda】ランタイム別速度比較

結論から言うと、Micronaut アプリをAWS Lambdaで動かす場合には、GraalVM Native Image を用いてネイティブコンパイルしてカスタムランタイムで動かすのが、処理速度とコストの両方の面でおすすめです。

但し、 Native Image 化には制約(※後述)があるため、依存ライブラリが多い場合は特に注意が必要。ライブラリ1つ1つについて利用可否の確認や置き換えを検討する必要があるので、動作しているアプリケーションをあとから Native Image するのはそれなりに大変と思われます。もちろん規模に依ります。

目次

Micronaut やその周辺のこと

Micronaut

Micronaut は JVM で動作するマイクロサービス向けのフレームワークです。2018年5月にOSSとして公開され、2020年6月にバージョン2.0がリリースされました。

マイクロサービス向けのフレームワークというと、Spring Boot がまず挙げられると思いますが、Micronaut は Spring Boot よりも軽量です。同様に軽量なマイクロサービスフレームワークとしては、 quarkus や Helidon がありますね。機会があれば quarkus や Helidon も触ってみようと思います。

GraalVM Native Image

Micronaut は GraalVM Native Image を用いたネイティブコンパイルをサポートしています。これにより、超高速起動するフットプリントの小さいアプリケーションを開発することが出来ます。

GraalVM Native Imageは良い点ばかり悪い点もあります。

まず、 GraalVM の Native Image 化には、制約があります。
https://github.com/oracle/graal/blob/master/substratevm/Limitations.md

  • 動的なクラスロードが出来ない
  • リフレクションを使えない

など。

また、ネイティブコンパイルには時間がかかります。私が Micronaut で試した時は5分以上かかりました。
その上、いざ実行してみるとランタイムエラーということも多々発生するので、アプリケーションのベースが出来てきて安定するまでは試行錯誤する可能性があります。

AWS Lambda

(AWS Lambda 自体の説明は割愛します。公式等を確認してください。)

Micronaut は Lambda もサポートしています。

API Gateway 経由でHTTPリクエストを処理する場合と、ALB (Application Load Balancer) 経由でHTTPリクエストを処理する場合の両方に対応しています。

Azure Functions や Google Cloud Functions にも Micronaut 2.0 から対応しているはずですが未調査です。

検証内容

Lambda のランタイムとして、Java11 (Corretto), Java8, カスタムランタイム (AmazonLinux2) を選択して動作させた場合の、それぞれの処理時間や使用メモリを確認します。

ALB からHTTPリクエストを受け取る想定で、Lambdaを実行 (Invoke) します。

検証に利用するツールやフレームワークのバージョンは以下の通りです。

Micronaut2.3.1
GraalVMCE (Community Edition) 21.0.0
Micronaut / GraalVM のバージョン

Java11 / Java8 ランタイム用Jarは、以下の公式ガイドを参考に行いました。

カスタムランタイム用Native Imageの作成は、以下の公式ガイドを参考に行いました。

検証結果

コールドスタート時の処理時間とメモリ使用量

処理時間

メモリ上限Java 11 (Corretto)Java 8カスタムランタイム
128m(Out of Memory)(Out of Memory)846 ms
256m(Out of Memory)(Out of Memory)510 ms
512m5,488 ms5,518 ms436 ms
1024m4,795 ms4,806 ms400 ms

初回起動時に関して、カスタムランタイムのほうが圧倒的に処理時間が短いようです。
また、Lambdaのメモリ上限を増やすとvCPUが増えるという話があり、メモリが多いほうが処理時間は短くなっています。

メモリ使用量

メモリ上限Java 11 (Corretto)Java 8カスタムランタイム
128m(Out of Memory)(Out of Memory)105 MB
256m(Out of Memory)(Out of Memory)104 MB
512m179 MB173 MB102 MB
1024m182 MB174 MB103 MB

Native Imageをカスタムランタイムで動かしたほうが、メモリ使用量も少ないようです。
Javaで実行した場合ですが、メモリ使用量が 180 MB程度のところ 256 MB分割り当てていたのですが、何故か Out Of Memory になりました。

ウォームスタート時の処理時間とメモリ使用量

処理時間

メモリ上限Java 11 (Corretto)Java 8カスタムランタイム
128m(確認不可)(確認不可)41 ms
256m(確認不可)(確認不可)9 ms
512m7 ms6 ms5 ms
1024m7 ms6 ms4 ms

2回目以降については、配備済のコンテナを再利用するので処理時間が総じて短くなります。
JavaはJITコンパイルによって実行環境に最適化されるので、Native Image より高速に処理できているのだと思われます。

メモリ使用量

メモリ上限Java 11 (Corretto)Java 8カスタムランタイム
128m(確認不可)(確認不可)105 MB
256m(確認不可)(確認不可)104 MB
512m180 MB173 MB105 MB
1024m182 MB174 MB105 MB

メモリ使用量については、初回起動時とほぼ一緒ですね。

まとめ

リアルタイム性が求められるWebアプリケーション等で、LambdaのランタイムとしてJavaを選択する場合は、コールドスタート対策が必須になると思われます。

コールドスタート対策としては、2019年末に発表された Provisioned Concurrency や 昔からある定期実行する方法が考えられます。Provisioned Concurrencyはそこそこ高いのと、どちらの方法も急なスパイクには弱いです。

例えば既存のコード資産がJavaであるといった理由等でJavaで開発したいのであれば、Native Image化してカスタムランタイムで動かしたほうが良いと思います。

ただ、JavaがNative Imageに対して必ず遅いのかというとそういうことではなくて、重いループ処理を行うケースでは実行環境に最適化して実行できるJVMのほうが早い場合もあると思います。

この記事が気に入ったら
いいね または フォローしてね!

よかったらシェアしてね!
目次