大量リクエストを低コストでさばく AWS Lambda 関数を JVM で実現

2021-10-05
デベロッパーのためのクラウド活用方法

Author : 立野 靖博 (Chatwork株式会社)

こんにちは、Chatwork 株式会社のソフトウェア開発者、立野と申します。

2021 年 7 月に アマゾン ウェブ サービス ジャパン (AWS) さん主催のイベント「そろそろマネージド、クラウドネイティブで行こう ! サーバーレスへのチャレンジ」にて、Chatwork におけるサーバーレス開発事例をお話しました。

弊社事例に対して「エンタープライズ開発でおなじみの Java/Scala で、高速・低コストの AWS Lambda 関数を実現する技術」が興味深い、という反応をいただきました。
この点を「ぜひ深堀りして紹介してみませんか」と AWS のソリューションアーキテクトの方々にお誘いを受けましたので、皆さんでも お試しいただけるソースコード つきの実践形式でご紹介いたします。

なお弊社事例の背景やアーキテクチャ詳細などにご興味のある方は、ぜひブログ記事や発表資料をご覧ください。

ご注意

今回示しました性能測定結果は、「今後も常にこの結果になる」ものではなく、参考であることをご理解ください。変動の影響を受けにくいように測定サンプル数を多くしておりますが、測定を行ったリージョンや日時、あるいは AWS Lambda 内部の改善など、さまざまな変動があるからです。

本記事で紹介する AWS サービスを起動する際には、料金がかかります。builders.flash メールメンバー特典の、クラウドレシピ向けクレジットコードプレゼントの入手をお勧めします。

builders.flash メールメンバーへの登録・特典の入手はこちら »

*ハンズオン記事およびソースコードにおける免責事項 »


サーバーレス関数の先駆者、AWS Lambda

クラウドを使った開発では近年、サーバーレスアーキテクチャーが人気トピックのひとつです。中でも、AWS Lambda を代表にしたサーバーレス関数が特に注目されていると思います。

サーバーレス関数とは、サーバーサイド処理をイベントハンドラ関数という小さな単位で実装、プロビジョニング、実行するモデルです。いくつも優れた点がありますが、なかでもぼくが気に入っている点は、

  • サーバー運用業務の大半をクラウド提供者に任せられるので、開発者はより多くの時間をプロダクトの価値向上に回せる
  • 非常に高いスケーラビリティが組み込まれているので、利用規模の大小を問わず適用できる
  • 「確保性能 x 処理時間」で課金されるので、ワークロードの負荷変動が大きいアプリでは高いコスト圧縮が見込める

というところです。

そんなサーバーレス関数という分野は、AWS が 2014 年 11 月にリリースした Lambda が切り開いたと言えるでしょう。
今では多くのクラウド提供者が AWS に追随してサーバーレス関数を提供しています。
また、自社データセンターなどで自前のサーバーレス関数を提供する OSS も登場しています。


ソフトウェア開発で広まる AWS Lambda

クラウドアプリのモニタリングを提供する DataDog の 2020 年の調査 によると、

  • AWS ユーザーの 50% 近くが AWS Lambda を採用
  • AWS 利用企業で Lambda を使っている割合は、スタートアップ~中小企業では40~55%、エンタープライズ領域では 90%

と、Lambda の高い普及率が示されております。
特に「エンタープライズ領域のほうが Lambda を使っている」というのは興味深いです。

サーバーレス関数は「一部の熱狂的ファンが使っている」という段階はとうに過ぎて、「エンタープライズを含むクラウド利用者全体に広がっている」と言えそうです。
先程紹介した「そろそろマネージド、クラウドネイティブで行こう ! サーバーレスへのチャレンジ」の事例は、その一例なのでしょう。


ソフトウェア開発で重要なポジションの Java 技術

ところで話は変わりますが、Java およびその周辺技術がソフトウェア開発で重要な位置を占めるのは言うまでもないことでしょう。

  • Java EE、Spring などのアプリケーション開発フレームワーク
  • Tomcat、Jetty、GlassFish などのアプリケーションサーバー
  • Hadoop、Spark、Elasticsearch などのデータの分析や検索
  • Eclipse、JetBrains IDE などの統合開発環境

など、開発者は多くの場面で Java を書くか、そうでないまでも Java 周辺技術を活用しています。
実際、統計的データの上でも Java 技術のビジネス需要は高そうです。


AWS Lambda 開発でも Java はよく使われ・・・ない !

そんなビジネス需要の高い Java は、AWS Lambda 開発でもやはり需要が高いのでしょうか ? 実は、そうではないようです。

ふたたび DataDog の大規模調査を引用すると、AWS Lambda のランタイム (実行環境) の利用率トップ 5 は次のとおりです。

1 位 Python、およそ 46%
2 位 Node.js、およそ 39%
3 位 Java、およそ 7%
4 位 .NET Core およそ 4%
5 位 Go およそ 3%
残り 1% は Ruby、その他。

img_jvm-lambda-function_01

なんと、Java の利用率は 3 位、それも 7% と、1 位・2 位の Python・Node.js と大きく離されています。
さきほどの「ソフトウェア開発者のうち Java 技術者は 28%」という推計に対して、すごいギャップがあります。

ここから「Java を第 1 言語とする開発者でも、AWS Lambda 開発では Java を避け、Python や Node.js を採用している」と推測できそうです。

実際、Java コミュニティをリードする Oracle の 2018 年の調査によると、Java 技術者も Node.js や Python など他の言語を使うケースがかなりあるようです。

img_jvm-lambda-function_02

この調査は AWS Lambda に限ったものではないので、少し強引かもしれません。
ですが、場面に応じて複数の言語を組み合わせて使うということはぼく自身行っているので、特に違和感なく感じます。


なぜ Lambda 開発では Java があまり選ばれないのか ?

Java 技術者が使い慣れた Java 言語やライブラリ、開発ツールを活用できると、Lambda 開発も捗りそうに思えます。ですが実際には Java があまり選ばれないのは、一体なぜでしょうか ?

いろいろ理由は考えられますが、ぼく自身の経験やサーバーレスコミュニティで見聞きする大きな理由は「JVM の特性と Lambda の特性の相性」だと思っています。

Eclipse、Elasticsearch、Minecraft、あるいは自身で開発したアプリなど、Java プログラムを起動されたことのある方なら、

「Java プログラムって処理はけっこう速いけど起動が遅いよなぁ」
「Java プログラムってメモリすごく必要だよなぁ」

と感じたことは多いと思います。
これは、JVM が起動に時間がかかったり、メモリをより多く消費するためです。
OS、CPU、メモリ管理などを抽象化してプログラミングやその実行をラクにしてくれる恩恵の裏側に、こうしたオーバーヘッドが生まれているのです。

一方、Lambda には次のような特徴があります。

  • ワークロードの同時リクエストが増えてきたら Lambda 関数がスケールアウトし、落ち着いてきたらスケールインする。つまり、大量インスタンスが頻繁に起動、停止を繰り返す
  • 使用料金は設定したメモリ確保量x処理時間で決まる。つまり、処理時間が同じならメモリ確保料が多ければ多いほど料金が高くなる

したがって、JVM と Lambda の組み合わせは、起動時間やサーバーコストの観点から少し考慮が必要です。


AWS Lambda の Java ランタイムと他のランタイムを比べてみる

Lambda 関数を Java で実装した場合とそれ以外で実装した場合で、処理時間や消費メモリがどう変わるか、具体的に見ていきましょう。
すぐに試せる環境が手に入るのがクラウドのすばらしいところですね。

Lambda ランタイムとして Java より人気の高い Python、Node.js と比較します。
Python、Node.js、Java いずれも、現時点 (2021 年 9 月 20 日現在) の最新バージョンを使用します。

  • Python ランタイム : Python 3.9 を使用
  • Node.js ランタイム * Node.js 14.x を使用。実装言語は TypeScript 4.3 で実装し、JS に変換
  • Java ランタイム : Java 11 (Amazon Corretto) を使用

Noop 関数、初期化時間の比較

まず比較の基準として、実質的に何も処理をしない (イベントが発生したら固定の JSON を返すだけ) 単純な Lambda 関数を作ってみます。
何もしないこの関数を Noop (no-operation) と呼びます。

Lambda での処理や初回起動にかかった時間は、CloudWatch Logs に格納される次のようなログで確認できます。

REPORT RequestId: 1a7a9da5-5e7a-4a5b-ad9f-552b60323618	Duration: 104.96 ms	Billed Duration: 105 ms	Memory Size: 256 MB	Max Memory Used: 90 MB	Init Duration: 636.35 ms

各項目は次のような意味です。

  • Duration はイベントの処理にかかった時間です。
  • Billed Duration は「処理時間を 1 ミリ秒単位に切り上げたもの」です。Billed Duration は使用料金の計算に使われます。
  • Init Duration はランタイムやイベントハンドラ関数外の初期化にかかった時間で、Duration とは別物です。Init Duration も料金計算に使われます。Lambda 関数の新たなインスタンスが起動するときに発生します。

今回は Node.js、Java そして Python のランタイムについて、128MB、256MB、512MB とメモリを増やしながら

  • 起動時間 (Init Duration)
  • 処理時間 (Billed Duration)

ともに 300 回ずつ計測しました。
結果は次のとおりです。

img_jvs-lambda-function_03

クリックすると拡大します

今回の箱ひげ図は、次のように読みます。

img_jvs-lambda-function_04

通常、箱ひげ図には中央値 (50%ile) や平均値が分かるようにしますが、今回は図を簡略にするため、省略しました。25%ile~75%ile の区間をボリュームゾーンと考え、これを比較していくことにしましょう。

Node.js ランタイムが明らかに速く、どのメモリ量でもおよそ140~160ミリ秒にボリュームゾーンが見られます。
Java と Python ランタイムでは、およそ 330~380ミリ秒程にボリュームゾーンが見られます。

全体としては、メモリ量を増やしてもボリュームゾーンはほとんど動きませんでした。
Lambda ではメモリ量に比例して CPU 性能も高まるのですが、初回起動時間については大きな影響はないようです。
したがって、Lambda ランタイムの初期化時間はメモリ量 (と CPU 性能) によらずほぼ一定と言えそうです。

次に、Node.js ランタイムが他よりも初期化時間が短い理由は何なのでしょうか。
ひとつ考えられるのは、Lambda 関数のパッケージサイズです。
関数のパッケージサイズが大きくなると、Lambda の起動時間も長くなることがあります。

今回のパッケージサイズは下表のようになりました。

Node.js

Java

Python

458 Byte

973 KB

461 Byte

Java だけサイズが突出して大きいですが、これは aws-lambda-java-core やその遷移先依存である joda-time などのライブラリが同梱されるためです。
Node.js や Python は Noop 実装では特にライブラリを必要としないので、パッケージサイズは Lambda 関数そのもののソースコードだけとなり、とても軽量です。
ただし、Node.js と Python のパッケージサイズはほぼ同じなので、今回の両者の起動時間の違いはパッケージサイズはあまり関係なさそうです。

他に考えられそうなのは、各ランタイムに含まれる組み込みライブラリの初期化です。
ただ、組み込みライブラリの起動時間のどれくらいが初期化時間なのかを Lambda 上で記録するのは難しそうです。

以上から、Node.js 14 の起動は JVM や Python よりも純粋に速い、と理解しておくことにしましょう。


Noop 関数、処理時間の比較

次に、処理時間を見ていきましょう。

遅い Java 11@ 128MB に引きづられてしまい、ちょっと傾向が分かりにくいですね。

img_jvs-lambda-function_05

クリックすると拡大します

対数表示を使うと分かりやすくなりました。日常的な線形表示では目盛りは 20 ミリ秒など「一定量」ずつきざみますが、対数表示では目盛りごとに 10 倍のような「一定倍率」できざみます。対数表示は、桁数が大きく変わるようなデータに便利で、身近なところでは音の強さ (デシベル) やカメラレンズの明るさ (F 値) などに使われています。

以降、対数表示を使っていきます。

img_jvs-lambda-function_06_new

クリックすると拡大します

どのランタイムでも、メモリを増やしていくとボリュームゾーンがやや下に改善していきます。

いずれのランタイムでも、128MB のときのボリュームゾーンの振れ幅が 256MB以降よりも大きいようです。128MB と 256MB の間になにか壁があるようです。

さきほどご紹介したように Lambda では CPU 性能はメモリ量に比例するとされていますが、直線的ではなく、128MB / 256MB のどこかで CPU 性能がガクッと変化するのかもしれません。

ただ、これら 2 つの違いは、この関数はあくまでも Noop であり本稿では重要ではないので、特に検討しないでおきます。


GetItem 関数、初期化時間の比較

さて、Noop 関数だけを見ると、Node.js の起動時間は速いものの、Java ランタイムもなかなか健闘しているように見えました。
ですが、実際のアプリケーションに近い処理をさせてみるとどうなるのでしょうか ?

実際のアプリでは、何らかのデータをファイルやデータベースに書き込んだり、読み込んだりするのが常です。
そこで、KVS 型データベースである Amazon DynamoDB から、1 つのアイテム (リレーショナルデータベースでいうレコード) を読み取る Lambda 関数「GetItem」を実装してみます。

Lambda から DynamoDB などの AWS にアクセスするには、クライアントが必要です。今回は AWS 公式の AWS SDK を使用します。

GetItem 関数の起動時間 (対数表示) は次のようになりました。

img_jvs-lambda-function_06

クリックすると拡大します

処理時間の大小は、Java (1860~1950ミリ秒) > Python (600~640ミリ秒) >Node.js (250~290ミリ秒) となりました。

なんと Java @ 128MB では、メモリ不足 (OutOfMemory) が多発し、何度か Lambda が再起動して運良く成功することがある・・・という状況でした。

これでは不安定で本番運用は難しいので、今回は Java @ 128MB の結果を除外しました。

各ランタイムを、Noop 関数のときの初期化時間と比べてみましょう。

img_jvs-lambda-function_07

クリックすると拡大します

Noop に比べて、GetItem では次のように起動時間が伸びています。

  • Node.js では + 120 ミリ秒
  • Java では +1500 ミリ秒
  • Python では +300 ミリ秒

これはさきほど紹介した「関数のパッケージサイズが大きくなると、Lambda の起動時間も長くなることがある」との典型的な例です。

  • 大きなパッケージを読み込む時間
  • パッケージに含まれるライブラリやアプリの初期化時間 (何らかのオブジェクトのインスタンス作成など)

が伸びるためです。

今回、GetItem では DynamoDB クライアントライブラリが必要でした。互換ライブラリもいくつかありますが、AWS 公式の AWS SDK を使用しました。これにより、パッケージサイズは次のように増加していました。

Node.js

Java

Python

121 KB

10.4 MB

 8.8 MB

  • Java では DynamoDB 専用 SDK だけでも 10MB にもなりました。AWS SDK のコアライブラリやいくつもの HTTP クライアントなどが比較的大きいためです。他のランタイムに比べて初期化に 2 秒近くもかかってしまっていました。
  • Python では、boto3 という AWS 全部入りの SDK にしました。boto3 が Java や Node.js の AWS SDK と API がほぼ共通で、Python に詳しくないぼくでもすぐ扱えるためです。おもしろいことに、boto3 は AWS 全部入りで 9 MB 近くあるにもかかわらず、初期化時間の増加は Java ほどではありません。ライブラリの初期化が少なく早く終るのかもしれません。
  • TypeScript では、2021 年に正式リリースされた AWS-SDK-JS-v3 を使うことで、0.1MB と他の 100 分の 1 程度で済みました。パッケージサイズは小さく、また初期化処理も少ないとみえ、初期化時間の増加も +120 ミリ秒程度に収まりました。

AWS SDK を追加するだけでも、ランタイムによって初期化時間がずいぶん異なることが分かりました。

少し補足しますと、Python や Node.js などのランタイムでは、各言語の AWS SDK が事前にインストールされています。デフォルトの SDK で十分であれば、Lambda のパッケージから AWS SDK を除外してパッケージサイズを小さくできます。

ただし、事前インストール済み SDK のバージョンは必ずしも最新ではありません。新機能やバグ修正が含まれる新しい SDK が必要な場合は、今回の実験のように SDK を Lambda パッケージに含める必要があります。
今回の実験では「各言語の AWS SDK を明示的に追加する」という条件を揃えることにしました。


GetItem 関数、処理時間の比較

続いて GetItem の処理時間を見てみましょう。
今回は、ごく単純な JSON 構造の、1 アイテム 172 バイトのアイテムを取得させます。

img_jvs-lambda-function_08

クリックすると拡大します

Node.js と Python では、メモリを増やしていくにつれて処理時間が短くなっていきますが、512 MB から 1024 MB に増やしてもほとんど変化が見られません。このような「変数がある値を超えると結果がほとんど変化しなくなる」ことを「飽和 (サチュレーション) している」、俗に「サチる」と言うことがあります。実際は 512 MB より手前のどこかで飽和しているのでしょう。
Java でも処理時間は短くなっていきますが、今回の範囲では飽和しませんでした。
 
繰り返しますが、Lambda ではメモリ量とともに CPU 性能が高まります。
CPU性能が高まったことで、Lambda ⇔ DynamoDB のサービス間通信にかかる時間や受け取ったレスポンスの処理にかかる時間が短くなったのでしょう。
ただしどんなに CPU 性能が高くても、通信に最低限かかる時間はあり、そこで飽和したたようです。
 
以上から、この実験では次のことが言えそうです。
 
  • DynamoDB への GetItem はメモリを増やしていくとある程度速くなる。AWS 間通信に関する処理が、メモリ量 ≒ CPU 性能の向上で高速化するためと考えられる。
  • ただし通信に最低限かかる時間はあるので、どこかで飽和する。したがって、最短の処理時間実現に十分なメモリ量を見極めることで、コストを最適化できる
  • Java は 1024MB でも 12~30 ミリ秒かかる。他のランタイムが 512MB で下限の 6 ~ 10 ミリ秒に到達しているので、Java は性能・コスパ観点で見劣りする

Query 関数、初期化時間の比較

GetItem では Java がやや不利になってきましたが、もう少し見ていきましょう。
 
実際のアプリでは、DB から連続する複数のアイテムを一度に取り出すことが多々あります。DynamoDB では、このような操作は Query と呼ばれます。

そこで、Query を実行する Query 関数を実験してみましょう。

初期化時間はこうなりました。

img_jvs-lambda-function_09

クリックすると拡大します

これは、同じく DynamoDB クライアントを使う GeItem 関数 とほぼ一致です。
 
初期化の内容が同じであれば、初期化後に実際に行う処理の内容が違っても、初期化時間は変わらないようです。それはそうだよな、という感じですね。

Query 関数、処理時間の比較

次に Query 関数の処理時間を見ていきましょう。
今回は、1 アイテム 172 バイトを 1,000 アイテム、合計 172KB を Query させてみます。

img_jvs-lambda-function_10

クリックすると拡大します

さすがに 1,000 アイテム、172KB も Query するとなかなか時間がかかりますね。
いずれのランタイムでもメモリ量を倍、倍に増やしていくと、処理時間のボリュームゾーンもほぼ半減、半減していきます。

さきほど GetItem では 512MB あたりで飽和するのを観察できました。Query でも同様なのか、さらにメモリを増やして見てみましょう。

img_jvs-lambda-function_11

クリックすると拡大します

今回も Node.js と Python では 1024MB / 2048MB のあたりで飽和するようです。Java ではメモリを更に増やせばさらに改善しそうですが、それでも他のランタイムより遅いのは変わりません。
 
Node.js と Python を比べると、GetItem では Python のほうが Node.js よりもやや高速でした。しかし Query では Node.js のほうが Python より高速になりました。これはおそらく、Query で取れてきた複数要素(Array)を加工する処理が Node.js のほうが高速だったと考えられます。
 
ということで、Query でも GetItem とほぼ同様の結論が言えそうです。

  • メモリを増やしていくとある程度速くなるが、どこかで飽和する。なので達成したいレイテンシに必要なメモリ量を見極めることで、コストを最適化できる
  • Java は性能・コスパ観点では Node.js や Python ランタイムには見劣りする
  • GetItem と違い、Node.js のほうが Python より高速であった。これはNode.js が Python よりも Array 処理が速いということだと思われる

つまり、低レイテンシや低コストを実現したい領域では Java ランタイムは不利と言えるでしょう。


それでも AWS Lambda で Java を使いたい

さて、残念ながら Java ランタイムにはキビシイ結果となりました。
さきほどの Datadog の Lambda ランタイム利用の調査で Python と Node.js 合わせたシェアが 85%、Java がわずか 7% になるのもムリはない気がします。

Java 技術者 / Scala 技術者でも、Lambda 開発では Python や Node.js を使えばいいのでしょうか?

ですが、開発の事情によってはそうも言えないでしょう。
弊社 Chatwork でも同様で、次のような事情がありました。

  • 組織として Scala に投資しており、開発者は Java / Scala の専門家であるため、Node.js や Python のノウハウが組織にない。
  • 短期間で性能や保守性よく開発する必要があり、不慣れな Node.js や Python だと不安がある。
  • 開発予定の Lambda 関数はわずか 2 つで、今後 1~2 年は増やす予定はない。せっかく Node.js や Python の技術スタックを習得しても定着しない可能性が高い。
  • 今後も長期的にメンテナンスするので、Java / Scala 技術が望ましい。

こうした事情がある中でも「膨大なトラフィックを省メモリ (=低コスト) で高速に処理」するという非機能要件をなんとか両立できないでしょうか。
弊社では Java でも省メモリ・高速を実現する道を探ることにしました。


Java とスケーラビリティの共通課題

Java アプリを開発してきた方なら、これまで見てきた Lambda x Java ランタイムの問題にはどこか見覚えがあるのではないでしょうか。

  • JVM の起動や JIT の暖機が十分に進むまで時間がかかるため、ワークロードの変動が大きい場合にスケールアウトがもたついてしまう
  • スケールアウトに余裕をもたせておくとアイドルなサーバーが増えてしまい、高コストになってしまう
  • 比較的単純な Java アプリでも数百 MB 以上のメモリを必要とし、サーバーコストの最低ラインが高い

これはサーバー「フル」、つまり従来型の Java アプリケーションを物理サーバーや何らかのコンテナランタイムで起動する場合であっても、つきまとってくる問題です。


Java アプリのネイティブ化で省メモリ、高速を両立する

こうした課題に対して、Oracle が打ち出したのが GraalVM と総称される一連の技術です。GraalVM は 2018 年にバージョン 1.0 がリリースされ、2019 年にはプロダクションレディとうたわれるバージョンがリリースされた、比較的新しい技術です。

GraalVM にはいくつかの技術が含まれるのですが、今回重要なのは native-image と呼ばれる技術です。native-image をカンタンに説明すると「Java プログラムを事前コンパイル (AOT、Ahead-Of-Time) して、単独実行可能なネイティブバイナリを生成するツール」です。C 言語や C++ 言語などで開発したときと似ていますね。

ネイティブという言葉から想像しやすいように、native-image で生成されたネイティブバイナリは、次のメリットがあります。

  • 非常に高速に起動する
  • 必要なメモリが少なく済む

これはまさに、今回の実験でお見せしました Java ランタイムの課題にピッタリですね。


Lambda カスタムランタイムで native-image を使う

これまでの実験では、AWS が提供するランタイムを使用してきました。
Lambda ではさらに、AWS ユーザー自身で実装するカスタムランタイムを使うことができます。これにより、既存のランタイムで満たせないニーズを満たすことができます。

カスタムランタイムは、Lambda のインターフェース仕様を満たしていれば、どんなプログラミング言語でも開発できます。native-image で生成したネイティブバイナリももちろん使用できます。


native-image で Lambda 関数を開発すると

実際に native-image を Lambda 関数に適用してみるとどうなるか、実験してみましょう。

ロジックのない Noop 関数は省略し、DynamoDB を扱う GetItem 関数と Query 関数だけを見てみます。

まず GetItem の起動時間です。

img_jvs-lambda-function_12

クリックすると拡大します

native-image で実装した関数は、「GraalVM」としては図の右側に追加しました。

img_jvs-lambda-function_20

クリックすると拡大します

もともとの Java ランタイムより圧倒的に速くなり、300〜340 ミリ秒台でした。最速の Node.js ランタイムの 250〜280 ミリ秒台に肉薄しています。

続いて GetItem の処理時間です。
最速の Python とほぼ同等か、若干速いくらいです。

img_jvs-lambda-function_13

クリックすると拡大します

最後に、Query の処理時間です。起動時間は GetItem と同様なので省略します。
Python より速く、Node.js よりは遅い、という結果になりました。

img_jvs-lambda-function_14

クリックすると拡大します

以上から、native-image を使ってビルドした Lambda だと、

  • 起動時間は Node.js に迫る
  • データ量が少ない GetItem 関数では最速
  • データ量が多い Query 関数では最速の Node.js より若干遅いが、次点の Python より速い

ということが言えそうです。
これにて、本題の「大量リクエストを低コストでさばく AWS Lambda 関数を JVM で実現」できたと結論づけたいと思います。


GraalVM + JVM 言語、弊社事例での効果

事例紹介しましたとおり、Chatwork では、GraalVM native-image を用いて Java+Scala で開発した AWS Lambda を本番運用しています。

Java+Scala、Lambda で開発できた恩恵は大きなものがありました。

  • Scala 技術者が慣れ親しんだ Scala+Java 技術スタックで開発でき、社内に豊富な Scala 技術者がメンテナンスできるようになった
  • コンテナベースの開発に比べて、
    • サーバー運用コストを 50〜60% 削減
    • サーバー開発工数を 56%削減 (4.5 人月→ 2 人月)
    • スケーリング計画やコンテナ環境のバージョンアップなどの DevOps 工数 : 毎月 6 時間削減

もし JVM 言語を使って Lambda 開発するのであれば、GraalVM native-image の使用を第一に検討するのをオススメします。


GraalVM と Lambda 開発を助けるツール

今回は、カスタムランタイムと GraalVM native-image の仕組みを理解するため、あえて自前で実装してみました。ただ、こうした低レイヤーを自前でメンテナンスしていくのは面倒そうです。

実は、Java コミュニティにはすでに、GraalVM を活用して Lambda 開発を助けるフレームワークがいくつか登場しています。有名なところでは、Spring Cloud FunctionQUARKUSMICRONAUT の 3 つです。

Spring Cloud Function は、Java 開発で一世を風靡した歴史ある Spring フレームワークのファミリーです。Quarkus や Micronaut は、後発のフレームワークで、どちらもクラウドネイティブを強く志向した Java 開発フレームワークです。

こうしたフレームワークを使えば、GraalVM と Java を利用した Lambda 開発もやりやすくなると思います。


Lambda 全般で使えるチューニング方法

この寄稿では、Java を使った Lambda 開発については GraalVM native-image を使うことで大きくチューニングできる可能性を示すことができました。

Lambda では他にもさまざまなチューニング方法があり、Java 以外でも広く利用できます。Amazon Web Services ブログの “Optimizing Lambda” という Lambda チューニングの連載記事 はそうしたチューニングについて広くカバーしており、実践的で、すぐに試してみたくなる内容が満載です。ご一読をおすすめいたします ! また、以下のような記事も公開されていますので、おすすめです !


まとめ

  • いずれのランタイムでも AWS SDK のような大型ライブラリを追加すると、起動時間が大きく伸びることを確認しました。Lambda 関数で使うライブラリサイズを最小化することは起動時間短縮の有効手段のひとつです。
  • 通常の Java 11 ランタイムでの処理時間は、Noop 関数と GetItem 関数では Python 3.9 や Node.js 14.x ランタイムより少し遅いほどでした。しかし Query 関数では、Java は数百ミリ秒前後遅くなりました。
  • どのランタイムでも、Lambda 関数に与えるメモリ量を増やしていくと、DynamoDB アクセスの処理時間が短く改善されました。また、十分なメモリ量に達すると処理時間の改善が飽和しました。
  • GraalVM native-image を使うことで、Java で実装した Lambda 関数は Node.js や Python ランタイムと同程度のメモリ量で同程度か少し速くなりました。

builders.flash メールメンバーへ登録することで
AWS のベストプラクティスを毎月無料でお試しいただけます

筆者プロフィール

photo_tatsuno-yasuhiro

立野 靖博 (たつの やすひろ)
Chatwork株式会社 プロダクト基盤開発部
サーバーレスエンジニア

長野県で在宅勤務するソフトウェア技術者。2017 年から AWS Lambda を中心としたサーバーレス大好き人間。それが高じて Serverless Framework のメンテナ。最近は Java や Scala や TypeScript ではなく、Rust で Lambda 関数や ECS タスクを開発中。

メンバー登録で毎月抽選で無料クーポンを入手できます

さらに最新記事・デベロッパー向けイベントを検索

AWS を無料でお試しいただけます

AWS 無料利用枠の詳細はこちら ≫
5 ステップでアカウント作成できます
無料サインアップ ≫
ご不明な点がおありですか?
日本担当チームへ相談する