[アップデート] JavaのLambda関数の実行を高速化するLambda SnapStartがリリースされました #reinvent

2022.11.29

Lambda SnapStartというLambdaの新機能が発表されました。関数のInit(初期化)フェーズの処理時間を最大10倍短縮させることができます。

どういう機能?

  1. 関数Init処理を予め実行し、出来上がった実行環境をスナップショットとして暗号化して保存しておきます。
  2. 初回関数実行時もしくは新規実行環境が必要になった際に、関数Init処理を実行するかわりに上記スナップショットを復元することで、処理時間の短縮を図ります。

まずLambdaの実行環境についておさらいを。Lambda関数は実行リクエストを受けた際に初めてコードをダウンロードして、ランタイムをインストールして、関数内の初期化処理を行なって実行環境を作成します。その後関数が実行されます。この「コードをダウンロードして、ランタイムをインストールして、関数内の初期化処理を行なう」フェーズのことを関数初期化フェーズやinitフェーズと言います。以下図左側の「INIT」の部分ですね。そしてInitフェーズを伴う関数実行のことをコールドスタートと呼んだりします。

Overview-Successful-Invokes Lambda execution environment lifecycle - Lambda execution environment - AWS Lambdaより引用

2回目以降の実行リクエストを受けた際には、Initフェーズはスキップして次のInvokeフェーズだけが実行されます。ただし同時リクエスト数が多くて1つの実行環境だけではリクエストをさばけない場合は、さばけるだけ実行環境がスケールアウトしてそれぞれでInitフェーズが走ります。

そしてしばらくリクエストが来なくなったら、使わなくなった実行環境は破棄され(SHUTDOWNフェーズ)、次のリクエストの際に再びInitフェーズから実行されます。

このInitフェーズが関数の性質によってはとても長くなってしまい、要件を満たさない場合があります。久しぶりのアクセス時に遅くなったり、たくさんリクエストが来た際に一部リクエストだけInitフェーズが乗っかってハズレ値みたいな長い実行時間になったり。この原因は、特定の言語(Javaって言いたいんでしょう)はランタイム初期化に時間がかかるからであったり、機械学習のモデルなどすごく大きいファイルをダウンロードする必要があったりです。

SnapStartはこのInitフェーズに時間がかかることを解決します。冒頭に書きましたが、予め(関数デプロイ時)にInitフェーズを実行しておき、実行環境のメモリとディスク状況をスナップショットとして暗号化して保存しておきます。関数実行時にはInitフェーズの代わりにRestoreフェーズという処理を実行します。これは先程のスナップショットから実行環境を復元する処理です。復元処理の方がInitフェーズよりも高速になるので、処理時間の短縮が見込めます。

初期化処理がメチャ重な場合にも使える

通常のInitフェーズは10秒以内に処理完了する必要があります。が、SnapStartを有効化した関数でInitフェーズが実行される際、つまり関数がデプロイされたタイミングですが、このときは最大15分処理を行なえます。 Springフレームワークなどの初期化処理がヘビーなものもLambdaに乗せることができるようになるのではないでしょうか。

料金

無料です!

SnapStartを使わない場合と同様、関数実行のリクエスト回数、実行処理時間、設定メモリによって課金されます。

対応ランタイム

Java11のみ。他のランタイムのLambda関数のコンソールでは該当設定項目箇所がdisabledになっていました。

only-java11

Provisioned Concurrencyとどう違うの?

Initフェーズの処理時間短縮を狙った機能としてすでにProvisioned Concurrency(プロビジョニング済み同時実行数)があります。ちょうど三年前のre:Inventのタイミングでリリースされた機能ですね。

この機能は、Initフェーズを実行済のLambda関数実行環境を、指定した数だけプールしておけるという機能です。ですので、プールしている実行環境数内でリクスエストがさばききれるのであれば、Initフェーズが発生しないので、一貫した処理時間が期待できます。

このProvisioned Concurrencyと今回リリースされたSnapStart、どう使い分ければ良いでしょう?Provisioned ConcurrencyとSnapStartは併用できません。

リリースブログ内では、まずSnapStartを使うことを考えて、レイテンシについて非常に厳しい要件がある場合はProvisioned Concurrencyを使うと良いと書かれています。

これは料金に関して、SnapStartは無料で利用できる反面、Provisioned Concurrencyはプールする実行環境環境数に応じて時間課金が発生するからというところによるのではと思います。

また、SnapStartはInitフェーズの代わりにRestoreフェーズが発生しますが、Provisioned ConcurrencyはRestoreフェーズすらなく即Invokeフェーズに入れるので、Provisioned Concurrencyの方がより高速に処理できるでしょう。ただし速度アップが期待できるのはプールした実行環境数内でリクエストがさばける時だけです。それ以上のリクエストが発生した場合はInitフェーズが発生してしまいます。そういう意味では、SnapStartはよりアクセス状況が読めない、スパイクアクセスが発生するようなワークロードに適しているんじゃないかと思います。

注意が必要な点

一意性

1つのスナップショットを実行環境が必要になる度に復元する機能なので、Init処理内にて何かしら一意のキーを発行していた場合、すべての実行環境で同じキーが使われることになります。これが許容できない場合は以下いずれかの方法を検討しましょう。

  • 関数Init時に一意キーを発行するのではなく、実行時に(=ハンドラー関数内で)発行する。もしくはRuntime hookでスナップショットから復元したあとに処理を差し込む
  • cryptographically secure pseudorandom number generators (CSPRNGs)を使う
    Javaよくわからないのですが、こいつを使うとSnapStartを使った場合でも実行環境毎に異なる乱数を使えるそうです。

ネットワークコネクション

Init処理内で外部サービスに対してコネクションを貼った場合、それが関数実行時まで残っている保証がありません。ハンドラー関数内もしくはafterRestoreRuntime hookを使って実行時に再接続することを検討してください。

揮発性データ

Init処理内で作成orダウンロードしたファイルが関数実行時に古くなっていないか確認してください。

対応リージョン

東京リージョンは使えます。大阪は未対応です。

  • US East (Ohio, N. Virginia)
  • US West (Oregon)
  • Asia Pacific (Singapore, Sydney, Tokyo)
  • Europe (Frankfurt, Ireland, Stockholm)

まとめ

どうしてもコールドスタート時の処理時間を許容できないから、Lambdaは諦めてコンテナ常駐させるかー」という状況に刺さるアップデートですね。こういう状況においてLambdaを選択できる可能性が上がります。Java11のLambda関数を持っていて前項の注意点が大丈夫そうなのであれば、とりあえず有効化してみるのが良いのではないでしょうか。またInitフェーズ処理が最大15分まで実行可能ということで、初期化処理の重いコード、フレームワークをLambdaで実行するという選択肢も取り得るかと思います。

参考情報