Amazon Web Services ブログ

SnapStart で AWS Lambda 関数の Java コールドスタートを削減する

この記事は Reducing Java cold starts on AWS Lambda functions with SnapStart (記事公開日: 2022 年 11 月 29 日) を翻訳したものです。

AWS re:Invent 2022 で、 AWS は Java 11 (Amazon Corretto) 上で実行される AWS Lambda 関数用の SnapStart を発表しました。この機能により、お客様は Java 関数のスタートアップパフォーマンスを最大 10 倍高速化でき、追加料金はかかりません。そして、通常はコードの変更を最小限に抑えることができます。

概要

今日、 Lambda の関数呼び出しにおいて、起動遅延の最大の原因は関数の初期化に費やされる時間です。これには、関数のコードの読み込みと依存関係の初期化が含まれます。起動遅延に影響を受けやすいインタラクティブなワークロードに対しては、これは最終的なエンドユーザーエクスペリエンスを損なう可能性があります。

この課題を解決するために、顧客はリソースを事前に準備するか、比較的複雑なパフォーマンス最適化を構築するよう努めます。これらの回避策は起動遅延を減少させるのですが、ユーザーはビジネスの価値を提供することに集中できません。 SnapStart は Java ベースの Lambda 関数に対するこれらの懸念に、より直接的な解決策を提供します。

SnapStart の仕組み

SnapStart を使用する場合、顧客が関数バージョンを発行すると、 Lambda サービスは関数のコードを初期化します。そして、初期化された実行環境を暗号化したスナップショットを取り、階層化されたキャッシュにスナップショットを永続化することで、低レイテンシーでのアクセスを可能にします。

関数が最初に呼び出され、その後スケーリングされるとき、 Lambda はゼロから初期化するのではなく、永続化されたスナップショットから実行環境を再開します。その結果、起動時のレイテンシーが低くなります。

SnapStart 設定をした関数バージョンが Active 状態となった後、 14 日間アイドル状態であると Inactive 状態に移行し、その後 Lambda はスナップショットを削除します。Inactive 状態の関数バージョンを呼び出そうとすると、呼び出しが失敗します。 Lambda は SnapStartNotReadyException を送信し、バックグラウンドで新しいスナップショットを初期化し始めます。この間、関数バージョンは Pending 状態のままです。関数が Active 状態に達するまで待ち、その後もう一度呼び出してください。このプロセスや関数の状態についてもっと知りたい場合は、ドキュメントを参照してください

SnapStart を使用する

Spring などのアプリケーションフレームワークは、共通タスクを実行するために書くボイラープレートコードを減らすことで、開発者にとって大きな生産性の向上をもたらします。フレームワークが作成された当初、アプリケーションサーバーで動作し、長期間実行されると考えられていたため、起動時間を考慮する必要はありませんでした。起動時間は、実行持続時間に比べてわずかです。通常、アプリケーションのバージョンが変更されたときにリスタートします。

これらのフレームワークがもたらす機能がランタイムで実装される場合、起動時間に対する遅延を引き起こすことがあります。 SnapStart を使用すると、 Spring のようなフレームワークを使用しても、テイルレイテンシーを損なわずに使用できます。(テイルレイテンシは高いパーセンタイルに位置するレイテンシであり、典型的には応答時間が 99パーセンタイル以上に位置するレイテンシのこと)

Lambda function lifecycle

Lambda function lifecycle

SnapStart をデモンストレーションするために、 Amazon DynamoDB にレコードを保存するサンプルアプリケーションを使用します。この Spring Boot アプリケーションは、 CRUD リクエストを処理する REST コントローラーを使用します。このサンプルには、 AWS Serverless Application Model (AWS SAM) を使用してアプリケーションをデプロイするためのインフラストラクチャ (IaC) が含まれています。この例をデプロイするには、 AWS SAM CLI をインストールする必要があります。

デプロイ方法:

  1. git リポジトリをクローンし、プロジェクトディレクトリに移動する:
    git clone https://github.com/aws-samples/serverless-patterns.git
    cd serverless-patterns/apigw-lambda-snapstart
  2. AWS SAM CLI を使用して、アプリケーションをビルドする:
    sam build
  3. AWS SAM CLI を使用して、リソースをAWSアカウントへデプロイする:
    sam deploy -g

このプロジェクトは、すでにSnapStartが有効になっている状態でデプロイされます。AWS Management Consoleでこの機能を有効または無効にするには:

  1. Lambda 関数に移動します。
  2. [設定]タブを選択します。
  3. [編集]を選択して、 SnapStart 属性を[PublishedVersions]に変更します。
  4. [保存]を選択します。

Lambda Console confoguration

Lambda Console confoguration

  1. [バージョン]タブを選択して、[新しいバージョンを発行]を選択します。
  2. [発行]を選択します。

SnapStart を有効にすると、 Lambda はそれ以降のすべてのバージョンをスナップショット付きで発行します。発行バージョンの実行時間は、 初期化のコードに依存します。この機能では、最大 15 分まで初期処理を実行できます。

考慮すべき点

認証情報の陳腐化

SnapStart を使用してスナップショットから復元することは、関数を作成する方法を変えることがあります。オンデマンド関数の場合、初期化フェーズで 1 回限りのデータにアクセスし、それを将来の起動で再利用することがあります。このデータがエフェメラル(揮発性)である場合、例えばデータベースパスワードの場合、シークレットを取得して使用する間に、パスワードが変更されエラーが発生する可能性があります。このエラーケースを処理するコードを記述する必要があります。

SnapStart を使用すると、同じアプローチを踏むと、データベースパスワードが暗号化されたスナップショットに永続的に保存されます。すべての将来の実行環境は同じ状態です。この状態は、スナップショットが撮られてから数日、数週間、またはそれ以上の継続時間を要することがあります。これは、関数に不正なパスワードが保存される可能性が高くなることを意味します。これを改善するには、パスワードを取得する機能をスナップショット後のフックに移動することができます。どちらのアプローチも、アプリケーションのニーズを理解し、エラーが発生したときに処理することが重要です。

Demo application architecture

Demo application architecture

初期状態を共有する際の第二の課題は、ランダム性と一意性にあります。初期化段階でスナップショットに乱数のシードが格納されると、乱数が予測可能になってしまう可能性があります。

暗号化技術

AWSは、関数を復元するときに一意性とランダムさの影響を扱うために、マネージドランタイムを変更しました。

Lambdaはすでに、 Amazon Linux 2 に対するアップデートを組み込んでおり、よく使用される暗号ライブラリの 1 つである OpenSSL (1.0.2)もスナップショット操作に耐性を持つように更新されています。 AWS はまた、 Java ランタイムの標準的な RNG java.security.SecureRandomがスナップショットからの復帰時に一意性を維持することを検証しました。

常にオペレーティングシステムから乱数を取得するソフトウェア(例えば、/dev/randomや/dev/urandomから)はすでにスナップショット操作に耐性を持ちます。一意性を復元するためのアップデートは必要ありません。ただし、 Lambda 関数でカスタムコードを使用して一意性を実装することを好むお客様は、 SnapStart を使用しているときに一意性が復元されるかどうかを確認する必要があります。

詳細については、「Starting up faster with AWS Lambda SnapStart」をお読みください。また、SnapStart 一意性についての Lambda ドキュメントを参照してください。

ランタイムフックの使用

これらのプリ・ポストフックは、スナップショット化プロセスに対応する方法を開発者に提供します。

例えば、常に Amazon S3 から大量のデータをプリロードする必要のある関数は、 Lambda がスナップショットを取る前にこれを行うべきです。これにより、データがスナップショットに埋め込まれるので、繰り返し取得する必要はありません。ただし、一部の場合は、エフェメラル(揮発性)データを保持したくない場合もあります。データベースのパスワードは頻繁に更新され、不必要なエラーを引き起こす可能性があります。これについては後のセクションで詳しく説明します。

Javaマネージドランタイムは、フックサポートを提供するためにオープンソースの Coordinated Restore at Checkpoint (CRaC)プロジェクトを使用します。マネージド Java ランタイムには、カスタマイズされた CRaC コンテキスト実装が含まれており、スナップショット作成を完了する前に Lambda 関数のランタイムフックを呼び出し、スナップショットから実行環境を復元するときにもランタイムフックを呼び出します。

次の関数例では、ランタイムフックを使用して関数ハンドラーを作成する方法を示しています。ハンドラーは CRaC Resource と Lambda RequestHandler インターフェースを実装しています。

...
import org.crac.Resource;
import org.crac.Core;
...

public class HelloHandler implements RequestHandler<String, String>, Resource {

    public HelloHandler() {
        Core.getGlobalContext().register(this);
    }

    public String handleRequest(String name, Context context) throws IOException {
        System.out.println("Handler execution");
        return "Hello " + name;
    }

    @Override
    public void beforeCheckpoint(org.crac.Context<? extends Resource> context) throws Exception {
        System.out.println("Before Checkpoint");
    }

    @Override
    public void afterRestore(org.crac.Context<? extends Resource> context) throws Exception {
        System.out.println("After Restore");
    }
}

ランタイムフックを記述するために必要なクラスについては、以下の依存関係をプロジェクトに追加してください:

Maven

<dependency>
  <groupId>io.github.crac</groupId>
  <artifactId>org-crac</artifactId>
  <version>0.1.3</version>
</dependency>

Gradle

implementation 'io.github.crac:org-crac:0.1.3'

プライミング

SnapStart とランタイムフックは、起動時のレイテンシを抑えるために Lambda 関数を構築する新しい方法を提供します。 Java アプリケーションを最初に呼び出す準備をできるだけ整えるために、プリスナップショットフックを使用できます。スナップショットが取られる前に、関数内でできるだけ多くのことを行います。これをプライミングと呼びます。

Java コードの zip ファイルを Lambda にアップロードすると、 zip にはバイトコードの .class ファイルが含まれます。これは、 JVM を持つあらゆるマシンで実行できます。 JVM がバイトコードを実行すると、最初にインタプリットされ、次にネイティブマシンコードにコンパイルされます。このコンパイルステージは、 CPU を比較的多く使用します(JIT コンパイラー)。

スナップショットが取られる前にコードパスを実行するには、 before snapshot フックを使用できます。 JVM はこれらのコードパスをコンパイルし、将来の復元に対する最適化を保持します。たとえば、DynamoDB と統合する関数を持っている場合は、before snapshot フック内で読み取り操作を行うことができます。

これは、関数コード、AWS SDK for Java、およびそのアクションで使用されるその他のライブラリがスナップショット内にコンパイルされ保持されることを意味します。そのため、関数が呼び出されるときに JVM がこのコードをコンパイルする必要がなくなり、実行環境が最初に呼び出されるときのレイテンシが低くなります。

プライミングには、アプリケーションコードとその実行に伴う結果を理解する必要があります。サンプルアプリケーションには、before snapshot フックが含まれており、DynamoDB からの読み取り操作を行うことでアプリケーションをプライムするものです。

メトリクス

次のグラフは、サンプル アプリケーションの Lambda 関数を 1 秒あたり 100 回、10 分間呼び出した場合を示しています。このテストは、SnapStart を使用する場合と使用しない場合の両方について行われます。

 

p50 p99.9
On-demand 7.87ms 5,114ms
SnapStart 7.87ms 488ms

まとめ

このブログでは、Java ベースの Lambda 関数のスタートアップ(コールドスタート)レイテンシーを縮小する SnapStart の使い方を紹介しています。SnapStart を AWS SDKAWS CloudFormationAWS SAM、および CDK を使用して設定できます。

詳細については、AWS のドキュメントから「関数オプションの設定」を参照してください。この機能では、一部のコード変更が必要になる場合があります。ほとんどの場合、既存のコードはすでに SnapStart と互換性があります。これより、レイテンシーに影響を受けやすい Java ベースのワークロードを Lambda に導入し、改善されたテイルレイテンシーで実行することができるようになりました。

この機能を使用することで、開発者は追加コストなしで、低レイテンシーのレスポンスタイムで Lambda のオンデマンドモデルを使用できます。SnapStart をパートナーフレームワーク(QuarkusMicronaut)と使用する方法については、Serverless Land で詳しく知ることができます。この機能やその他の機能については、Serverless Land を訪問してください。

翻訳はプロフェッショナルサービスの松岡が担当しました。原文はこちらです。