DigitalABOUTSERVICECASENEWSSPECIALISTRECRUITSPECIALIST多様な専門性を持つNRIデジタル社員のコラム、インタビューやインサイトをご紹介します。良太2023.3.13クラウドAWS技術検証Serverlessパフォーマンスチューニングこんにちは、NRIデジタルの島です。Lambda(以下Lambda)にコールドスタートの性能を大幅に改善し、初回起動を高速化する「SnapStart機能」のリリースが発表されました。SnapStartサーバレスアーキテクチャであるLambdaを使用してアプリケーションを構築する際、必ず課題となるのが「コールドスタートにおける処理遅延」です。Javaの場合フットプリントも大きく、またJIT(Just-In-Time)等アプリケーションアーキテクチャの特性上初動が遅いです。ひとたびコードが最適化されると他の言語よりも高速になりますが、初動時からスピードが求められるサーバレスアーキテクチャとの相性はあまり良くないです。これが原因で「LambdaにはJavaは向かない」と判断し、他言語実装への切り替えやコンテナベースの構築に舵を切る方々もいるのではないかと推測します。Javaはエンタープライズな業務アプリケーションやWebAPI等を実装する上でも優れた言語ですので、Lambda上でも高速に動作させていきたいところです。Lambda関数の高速化の可能性を探っていきたいと思います。Lambda関数のベストプラクティス」も提供されております。Java特に性能のチューニングについて現時点でも有用な情報が含まれておりますので、LambdaをJavaで実装しようとしている開発者やアーキテクトの方々は是非ご確認いただければと思います。Lambdaの課題Lambdaを使用してアプリケーションを構築する場合、意識しなければならないのがそのライフサイクルです。Lambda関数はリクエスト受信すると以下のライフサイクルで処理を実行します。lifecycleよりこのライフサイクル上の「INITフェーズが実行されるかどうか」が前述した「コールドスタートが実行されるかどうか」であり、処理実行速度に大きく影響します。起動された実行環境を再利用する場合はコールドスタートが実行されずINVOKEフェーズから実行される為、INITフェーズのオーバヘッドはありません。起動済みの実行環境が時間等により破棄された場合や、捌ききれない多重のリクエストが送信された場合などは新しく実行環境を起動する為コールドスタートとなり、INITフェーズが実行され処理遅延が発生します。このコールドスタート発生がJavaの場合、より表面化します。Javaは初期処理でクラスロードやDI(Spring等のDIベースのフレームワーク利用の場合)が発生すること、またJIT方式の特性上初動が非常に遅い為です。2022よりSnapStartの概要この課題を改善する為に登場したのが「SnapStart」です。SnapStartを利用することで、追加費用なしで最大90%(10倍)の待ち時間を削減可能とのこと。利用ニーズや上記課題を考慮し、現時点ではJava11(Corretto)のみが対象ですが、今後他言語にも展開されると思われます。Lambdaサービスは関数のコードを初期化(=INITフェーズを実行)し、階層化されたキャッシュに初期化済み実行環境のスナップショットを永続化することで、低レイテンシーでのアクセスを可能にします。2022より処理性能検証では、実際にサンプルアプリケーションを用いて処理性能の検証を実施したいと思います。DynamoDBに登録されている商品リスト(Items)を応答するというシンプルなアプリケーションです。Function処理性能(コールドスタートあり)まず、コールドスタートが発生した場合の実行処理性能について、SnapStartの有無でどのくらいの違いがあるか比較したいと思います。INITフェーズでの所要時間となり、実際の処理「所要時間」と合わせておよそ「12秒」もの時間がかかっています。duration」がスナップショットからのリストア時間です。実際の処理の「所要時間」と合わせて「5秒」以内にとなっており、初期処理がなくなっている分改善していることがわかります。実際の処理の「所要時間」は上記の通り遅いままです。これはJITコンパイルの仕組みによるもの(※1)だったり、Lambda上でJVMを実行する際に発生する諸々のオーバヘッド(※2)が要因かと思います。Client)利用時のオーバヘッドやクラス遅延読み込みによる大量I/Oなどが指摘されているJavaアプリケーションで、実際の処理の「所要時間」を含めた全体処理時間を改善するアプローチとして考えられるのは、アプリケーションをNative化してJVM上で実行することによるオーバヘッドを排除することです。Lambda関数のベストプラクティス」でもコールドスタートを回避し、高速化する為のアプローチとして提案されています。2019よりでは、上記サンプルアプリケーションをGraalVMにてNative化して処理性能を計測してみたいと思います。全体処理時間(初期所要時間+所要時間)がおよそ「600ミリ秒」に大幅に改善されました。この結果だけ見ると、SnapStartの有効化よりもNative化した方が断然効果が高いということになります。Native化に向けたアプリケーションの移行はかなり大変です。本検証のサンプルは規模が小さい為、比較的スムーズに移行できましたが、大規模なアプリケーションコードとなると簡単にはいきません。昨年のre:Inventの以下ページにおいても、Native化は「効果は絶大だが、労力が特大」というのが表されています。2022よりでは、他に方法はないでしょうか?もう一つのアプローチは、SnapStartの仕組みを利用して、「初期化処理内にJVMの暖機運転コードを埋め込む」方法です。前述の通りSnapStartは初期化処理済みのコードをスナップショットしますので、初期化処理内で事前に複数回コードを実行することで「リストア時に暖機運転済みの状態(=JVMがある程度適正化された状態)で処理が開始できるのでは?」という考え方です。実際に本サンプルにおいて、初期化処理内に暖機運転処理を実装します。暖機運転あり「WARMUP_COUNT=100」に設定して実行した結果です。所要時間がおよそ「700ミリ秒」に改善され、全体処理時間(リストア時間+所要時間)でも暖機運転なしの場合の「5秒」以内から「1秒」以内に改善しています。Native版には劣りますが、かなりの処理性能改善となりました。処理性能(コールドスタートなし)では、次にコールドスタートの発生がなかった場合、暖機運転ありなしで差があるか確認してみます。・暖機運転なし・暖機運転ありタイミングによるものかもしれないので確実なことは言えませんが、こちらもそれなりの差(つまり暖機運転効果)がありそうでした。(複数回実行してみましたが、「暖機運転なし」はしばらくの間「50〜100ミリ秒」の速度でした)なお、Native版は以下の通り、より高速でした。処理性能(ラッシュテスト)テスト実行にて効果が確認できたところで、以下の要領にてラッシュテストを実施してみたいと思います。ラッシュテストにはWebアプリケーション向けの負荷テストツールである「Gatling」を使用します。48ms本ラッシュテストの結果において、Mean(平均)、Max(最大)、Min(最小)、各パーセンタイル、どのレスポンスタイムからも予想通りNative版が最も良い結果となりましたが、暖機運転込みのSnapStartもかなり良い結果でした。性能チューニングを実施しようとした場合、移行コストや品質等とのバランスも考慮する必要があります。Native化は前述した通り移行コストが膨大で、品質的なリスクも大きいかと思いますが、SnapStartはアプリケーションの変更は少なく、追加コストもなくかなりの性能メリットを享受できます。その為、筆者は、まずはSnapStartを使用したチューニングのご検討をいただくのが良いのではないかと思います。その際、前述した暖機運転処理の導入も是非ご検討いただければと思います。Concurrency」機能も存在します。Concurrency」は事前プーリングにより処理時間短縮を実現します。実際に実行環境をプールしている為、SnapStartのようなリストアも不要でSnapStartよりも高速に処理出来るようです。以下がデメリットとなります。実際に実行環境をプールしている為、その実行環境稼働分のコストが課金される事前プール方式の為、アクセス数の事前予測が必要でプールする実行環境数の調整が難しい。→一定数にした場合、繁忙期等にはプール数が足りなくなりコールドスタートが発生、逆に繁忙期に合わせると通常時に余分なコストが発生する為、プール数をスケジューリングするような工夫が必要。この辺りはシステムの要件(コスト優先か性能優先か)や業務量・アクセス数の予測が可能か、等でどちらを利用するか考えていく必要があると思います。SnapStartを採用する場合は、いくつか考慮しなければならない事項がございますので、最後にそれらについて共有させていただきたいと思います。考慮事項SnapStart利用時には以下のような点を考慮する必要があります。ネットワークコネクションの切断INITフェーズ内でデータベース等へのコネクションを確立している場合、スナップショットからの復元時にはそのコネクションが維持されてない可能性が高いです。その為、コネクションが無効な場合は再接続するロジックが必須となります。サンプルコードに「RDBからユーザの名前を取得する」機能を追加してみます。コネクションの生成は、INITフェーズで実行されるように以下のようなローレベルコードで実装してみます。}}通常(SnapStartを利用しない)は正常に動作します。SnapStartを有効化し、RESTOREフェーズが発生する場合はやはりエラーになりました。回避方法は前述の通り再接続のロジックを実装することです。上記「getConnection」メソッドに再接続の処理を追加することで正常に動作しました。}・・・なお、コネクションの検証や再接続の実装場所は、以下公式ページになる「afterRestoreランタイムフック」を利用することも可能です。Lambda一意性の欠如SnapStartはINITフェーズ実行後のスナップショットをキャッシュする為、動的に生成したい値があったとしてもINITフェーズで生成した値が再利用されてしまいます。(実行環境が変わっても毎回同じ値)サンプルコードに「実行環境ごとに一意にしたい実行IDを生成する」機能を追加してみます。以下のようにStaticな変数を定義し、INITフェーズでUUID値を生成します。}}複数回実行してみます。結果の通り、コールドスタートが発生して実行環境が切り替わっても実行IDが同値になってしまいます。実行環境ごとに値を切り替えたい場合は、公式ページにあるように初回実行時に値を生成します。execId.toString());・・・こうすることで、以下の通り実行環境ごとに実行IDが別値となりました。上記は「実行環境ごと」に一意にしたい場合ですが、このような要件は稀で、どちらかというと「実行ごと」に一意にしたい要件の方が多いかと思います。そのような場合はSnapStart云々関係なく、コールドスタートによるINITフェーズが実行されなくとも、一意性が担保されるような実装の考慮が必要となります。(CSPRNG)」の利用を奨励しているようです。(CSPRNGs)アプリケーションの要件でランダム値の生成が必要な場合は、考慮いただければと思います。関数バージョンの非アクティブ化SnapStartは関数バージョンの作成で有効化されますが、呼び出しのない状態が14日続くとその関数のバージョンは非アクティブになりスナップショットが削除されます。この状態で関数バージョンの呼び出しが行われると、そのタイミングでINITフェーズを含んだコールドスタートが実行されてしまいます。INITフェーズに前述した暖機運転コード等時間がかかる処理を入れていると、長い時間処理遅延が発生することになってしまう為注意が必要です。詳細は以下公式ページをご参照ください。Lambdaさいごに冒頭でも述べた通り、これまでLambdaとJavaとの相性はあまり良くないと考えていましたので、今回のSnapStartのリリースは、筆者のようなJavaをメインに扱うアプリケーションアーキテクトには大変嬉しいものとなりました。システムをモダナイゼーションする際のアーキテクチャの選択肢として、コスト効率に優れたサーバレスアーキテクチャの採用は有力です。Lambda関数の高速化に向けた手応えを感じることができたので、引き続き実用化に向けた検証を進めてまいりたいと思います。