サーバーレス コードを操作する場合の非常に一般的なアプローチは、コールド スタートが非常に速いという評判を考慮して、Python、Node、または Go アプリケーションとしてコードを記述することです。
しかし、AWS Lambda などのサーバーレス環境をターゲットとする既存の Java アプリケーションに直面したらどうなるでしょうか?おそらくコード ベースの大部分は Java をホストしており、再利用したいツールとライブラリの豊富なエコシステムを開発しました。このようなアプリケーション全体を別の言語で書き直すにはコストがかかり、言うまでもなく、静的タイプセーフやコンパイル時間の最適化などの機能を放棄することになります。
少し前、私はまさにこのシナリオに直面しました。Java で書かれた 9 つの AWS Lambda アプリは、コールド スタートで非常に遅くなり、一部のアプリが時折タイムアウトするほどでした。
問題の Lambda は API Gateway の背後に配置され、対応する REST API を呼び出すことで管理タスクに使用されました。この機能はあまり頻繁に使用されていなかったため、コールド スタートが発生するのは避けられませんでした。ただし、これは重要なサービスではなかったため、これらの Lambda を回収できるかどうかを判断するための実験には絶好の機会でした。
この問題に対処するために GraalVM や Quarkus などのフレームワークをうまく使用している開発者に関する他のいくつかのブログ投稿に出会うまで、それほど時間はかかりませんでした。そこで私は自分で試してみることにしました。
しかし、そもそもこれらのツールは何なのでしょうか?
要するに、GraalVM は、Java をネイティブ イメージにコンパイルし、Graal JVM を使用して実行できるツールセットが付属する Java 仮想マシンです。
通常、Java は「Just In Time」(JIT) コンパイラーを利用します。これは、名前が示すように、コードの実行中に最適化とコンパイルを実行します。 JVM オプティマイザーがプログラムの実行を常に監視し、時間の経過とともにパフォーマンスの向上につながる微調整を実行するため、長時間実行されるアプリケーションはこの恩恵を受けます。
これは、アプリケーションが一度インスタンス化され、数時間以上実行されることが予想される場合には最適ですが、Kubernetes、AWS Lambda、および Java アプリケーションを迅速に起動し、実行することを期待するバッチ ジョブを扱う場合には、あまり良くありません。時間に敏感な操作と需要に応じた規模 - 自動車愛好家にとってターボラグといえば。
ここで、GraalVM のネイティブ イメージ機能が役に立ちます。 JIT コンパイラーを使用する代わりに、コードを事前にコンパイルする (AOT) という非常に異なるアプローチを選択します。静的コード分析を使用してパイを事前にベイクし、ビルド時に特定のクラスを事前初期化して、アプリケーション コードが実行されるときにいつでも起動できるようにします。
結果は?非常に高速なコールド スタートにより、アプリの有効期間が短く、すぐに起動する必要があるサーバーレス ドメインでネイティブ イメージが非常に機能します。
注意すべき点の 1 つは、GraalVM は AOT に対応していますが、Java で書かれた GraalVM の新しい JIT コンパイラを考慮すると、既存の JVM のドロップイン置き換えとしても機能し、パフォーマンスが向上するということです。
しかし、待ってください、それだけではありません!ネイティブ イメージには既知の実行パス上にあるコードのみが含まれるため、脂肪は削除され、保持することが明示的に宣言されていないすべての Java クラスは使用できなくなります。実行が予想されるビットのみを保持するため、アプリケーションのセキュリティが向上します。
ホストを侵害する手段としてリモート コード実行を使用する悪名高い Log4J 脆弱性を例に考えてみましょう。ネイティブ イメージでは、攻撃を伝達するために必要なライブラリ コードの部分にさえ到達できないため、ガジェット チェーンが成功する可能性は非常に低いです。
Quarkus は、サーバーレス アプリケーション用に最適化された Java フレームワークで、AWS Lambda をネイティブ実行可能ファイルとして特別に設定および構築するための拡張機能を提供することで、ネイティブ イメージの構築を容易にするツールボックスが付属しています。
Lambda の最適化の過程で、別の最適化手法にも遭遇しました。そのような最適化の 1 つは、Lambda の実行中に C1 コンパイラーを排他的に使用する提案であり、これによりコールド スタートの高速化が約束されました。通常、JVM 内で実行される Java アプリケーションは、高速ではあるが最適性が低い C1 と、その後に低速ではあるが長時間実行される Java アプリケーションに最適なパフォーマンスを提供する C2 で構成される階層型コンパイルを使用します。 Lambda の寿命が短いことを考えると、C2 コンパイルのメリットはごくわずかです。
AWS Lambda の C1 コンパイルを構成するプロセスを説明するガイドは、ここから入手できます。
もちろん、私が実施している GraalVM マスタープランと比較して、この手法がどの程度の改善をもたらすかを知りたかったので、以下の調査結果にも含めました。
JVM の階層型コンパイルと GraalVM の新しい JIT コンパイラーの詳細については、この Baeldung の記事を参照してください。
皮肉なことに、私が変更を本番環境に出荷してから数か月後、AWS は最新の SnapStart 機能を考案しました。これは、実行中の Lambda のスナップショットを取得し、最初から再初期化するのではなく、スナップショット イメージを使用します。復元ポイントはコールド スタートの高速化を約束します。 GraalVM の使用が無駄な労力であったかどうかを確認するために試してみる必要があり、それも調査結果に含めました。
SnapStart を最大限に活用するには、beforeCheckpoint フックと afterRestore フックを利用するためにコードのリファクタリングが必要になることに注意してください (詳細はこちら)。可能であれば大きなコード変更を避けたかったため、これらのメソッドを実装したりコードを再配置したりすることなく、この機能を「そのまま」使用しました。
GraalVM に戻りましょう。驚いたことに、このソリューションを組み込んだ後は、ビルド構成ファイルといくつかの必要なメタデータの追加と調整を除いて、Java コードの変更はまったく必要ありませんでした。
うますぎる話ですね?
少しかもしれない。 Java の世界では AOT コンパイルを使用していることを考えると、多くのライブラリが依存するリフレクション、プロキシ、インターフェイス、サービス レジストリなどの言語機能の使用に関しては、一定の課題が生じます。このため、GraalVM コンパイラでは、特定のクラスとサービスを最終アーティファクトに含めることができるように、明示的に登録する追加の構成メタデータを宣言する必要があります。 GraalVM は、実行可能ファイルと一緒に実行するために使用できるいわゆるエージェントを提供し、必要な構成を自動的に識別することで、このプロセスを容易にします。
Quarkus は、よく知られているライブラリを「ネイティブ イメージ フレンドリー」にするための拡張機能をいくつか提供していますが、私が既存のコード ベースで作業していたことを考えると、私の目標は、大きなリファクタリング (または、さらに言えばコードの変更) を避けることでした。 )、ネイティブ イメージを正常に生成するために既存のライブラリに必要な構成ファイルを作成することにしました。
ネイティブ イメージのコンパイルはリソースを大量に消費し、標準の JVM ランタイムを対象としたバイトコードのコンパイルに比べてかなり長い時間がかかることに注意してください。メモリ不足の問題を回避するために、ビルド ノードにより多くの RAM を割り当てる必要がある可能性があります。これは取引を妨げるものではありませんが、必ず念頭に置いておく必要があります。
ネイティブ イメージ Lambda をコンパイルしてパッケージ化したので、次はそれらをテスト環境にデプロイします。通常、Java Lambda は AWS の Java ランタイムを利用して実行します。ただし、Graal JVM 内にラップされたアプリコードを含むバイナリアーティファクトであるネイティブイメージを使用しようとしている場合、AWS が提供する「カスタム」Amazon Linux 環境の 1 つを選択する必要があります。
Postman API コレクションを使用して 9 個の Lambda すべてにリクエストを送信し、上記の各手法のコールド スタートの応答時間を測定しました。常にコールド スタートが発生することを確認するために、ターゲット Lambda の設定をリロードしました。これにより、次の呼び出しではすでにウォーム状態になっている可能性のあるインスタンスが使用されなくなります。すべての Lambda は 1GB の RAM で構成されました。また、プロセスに時間がかかることを考慮して、構成ごとに 1 回の呼び出しを測定しました。ただし、観察された応答時間は非常に明確な状況を示しています。
それで、うまくいきましたか?絶対に!結果は次のとおりです:
そして、明らかな勝者は次のとおりです: GraalVM Native Images - 変更されていない Java Lambda と比較して、平均して 3 倍の高速化が実現しました - タイムアウトがなくなり、応答時間が大幅に向上しました。これはまさに私が望んでいたものです。達成します。
コードを変更しないと、SnapStart は思ったほどパフォーマンスが良くありませんでした。 SnapStart 機能に加えて C1 コンパイラを採用すると、コールド スタート時間はさらに短縮されましたが、それでも GraalVM のネイティブ イメージを上回ることはできませんでした。だからといって、これが迅速かつ簡単に実装できる改善策として実行可能な選択肢ではないというわけではありません。ただし、Lambda を可能な限り最適化する必要があり、構成とビルド プロセスを調整するための時間とリソースがある場合は、パフォーマンスとセキュリティの点で GraalVM が間違いなく優れています。
GraalVM が主張しているように、ネイティブ イメージは、通常の JVM に比べて効率的に実行するために必要なリソースが少なくなります。これらの Lambda が動作する必要がある RAM の量を減らした場合に、コールド スタートとウォーム スタートのパフォーマンスがどのように維持されるかを確認したかったのです。今回は、このテストを実行するために 1 つの Lambda アプリのみを選択しました。結果は次のとおりです:
そして彼らは約束を果たしました!通常の JVM Lambda は 256 MB 以下の構成を試行するとメモリ不足になりましたが、ネイティブ イメージは段階的ではなく実行を継続しているように見えました。 128 MB が利用可能な最小メモリ オプションでなかったら、どれだけメモリを減らすことができたのだろうか。ネイティブ イメージは、コールド スタートで高速なだけでなく、限られたリソースで作業する場合でも一貫したパフォーマンスを提供し、運用コストの削減につながります。
Java のエコシステムは豊富かつ広大で、サーバーレス アプリケーションに関して Java を常に活用し続ける多くの新しいテクノロジと機能強化が日々登場しています。そのような新興テクノロジーの 1 つが GraalVM です。研究プロジェクトとして始まったものは、現在では徐々に採用されており、HotSpot などの標準 JVM の実行可能な代替手段として浮上しています。このブログ投稿では、GraalVM が提供する機能のほんの表面をなぞっただけなので、読者にはさらに詳しく調べていただくことをお勧めします。 Adyen (記事リンク) や Facebook (記事リンク) などの企業からは、GraalVM を利用して時間とお金を節約できた成功事例がいくつかあります。
次に Java をオプションとして無視する場合は、GraalVM を試してみてください。 Spring Boot 3 がすぐに使える GraalVM ネイティブ イメージをサポートするようになったことで、サーバーレス ワークロードにこれまで以上に簡単にネイティブ イメージを採用して、GraalVM が提供するパフォーマンス、低リソース消費、追加されたセキュリティを活用できるようになりました。
以上がJava もサーバーレスにできる: GraalVM を使用した高速コールド スタートの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。