AWS Lambda で Java 17 のランタイムを試しつつ、SnapStart を試してみた。前編:AWS Lambda で Java 17 を試してみる

記事タイトルとURLをコピーする

カスタマーサクセス部の山本です。

AWS Lambda で Java 17 のランタイムを試しつつ、SnapStart を試してみました。
SnapStart については、以下の記事で概要については書きました。

blog.serverworks.co.jp

Lambda を作成して最初の 1 回目の実行時には、VM を作成し、ランタイムを初期化し、Static コード と コンストラクタ を実行し、インスタンス化を行ないます。その後に、Lambda 側で指定したハンドラメソッドを実行します。
そのため、 1 回目の実行には非常に時間がかかります。
2 回目以降の実行時には、インスタンス化した状態で、ハンドラメソッドのみを実行します。
VM を作成し、インスタンス化を行なうまでの段階を「初期化フェーズ」と言います。
Lambda に複数のリクエストが同時にあった場合には、新しいVMを作成して処理するので、新たに初期化フェーズを実行することがあります。
初期化フェーズが走る場合を Cold Start と言い、インスタンス化している状態でハンドラメソッドのみ実行する場合は Warm Start と言います。

AWS re:Invent 2021 - Best practices of advanced serverless developers より

該当箇所を公式ドキュメントから抜粋します。

参考:初期化コード

Lambda は、関数を初めて呼び出す前の初期化フェーズで静的コードとクラスコンストラクターを実行します。初期化中に作成されたリソースは、呼び出しと呼び出しの間でメモリ内に保持され、ハンドラーによって何千回も再利用できます。したがって、メインハンドラーメソッドの外側に初期化コードを追加することで、計算時間を節約し、複数の呼び出し間でリソースを再利用することができます。

「初期化フェーズ (Cold Start)」が終わった状態のスナップショットを作成し、そのスナップショットからVMを復元して利用するのが、SnapStart という仕組みです。java のランタイム向けにある機能です。
VM を作成し、インスタンス化を行なうまでの「初期化フェーズ」に時間がかかりすぎるので、あらかじめスナップショットを取得しておきVMを復元する方が早い、という話ですね。
これで、Cold Start 問題がほぼほぼ解決する、というわけです。
あらかじめ取得したスナップショットからVMを復元するのは、試したところ、 150〜200ms 程度でした。

前編:AWS Lambda で Java 17 を試してみる

前編(本記事)では、AWS Lambda で Java 17 を試してみるところまで実施します。
後編では、SnapStart を試してみる予定です。
正直なところを申し上げますと、私はほとんど業務で Java を触った経験がないので、社内の詳しい方々に手取り足取り教えてもらいました。

参考にしたドキュメント

試してみた

Eclipse でのプロジェクト作成

以下のリンクから、Eclipse をダウンロードします。

www.eclipse.org

Java 開発者向けのエディションを選択します。

Launch したら、プロジェクトを作成します。(Package Explorer の空白を右クリックしました。)
Maven プロジェクトを選択します。

「Create a simple project...」にチェックを入れました。

[Group Id:] と [Artifact Id:] を適当に入力しました。

src/main/java を右クリックし、フォルダーを作ります。

example という名前にします。

example フォルダーを右クリックし、ファイルを作ります。

Handler.java という名前にします。

こんな構成になりました。

pom.xml の編集

プロジェクトディレクトリ直下にある pom.xml を以下のように変更しました。
Lambda 実行用に依存関係を追加し、shade プラグインを入れています。

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>doc-examples</groupId>
  <artifactId>lambda-java-example</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <name>lambda-java-example</name>
  <dependencies>
    <dependency>
        <groupId>com.amazonaws</groupId>
        <artifactId>aws-lambda-java-core</artifactId>
        <version>1.2.2</version>
    </dependency>
  </dependencies>
  <build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-shade-plugin</artifactId>
            <version>3.2.2</version>
        </plugin>
    </plugins>
  </build>
</project>

Handler.java ファイルの編集

Handler クラスを作成しました。(7行目〜)
Hello World! と標準出力し、 3 秒待ってから "Hello" と返す handleRequest メソッドを作成しました。(9行目〜)
Staticイニシャライザは 10 秒待ってから static! と標準出力します。(22行目〜)ここが、「初期化フェーズ」でインスタンス化の前に実行する想定です。Cold Start の場合に実行されます。

package example;

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.LambdaLogger;
import com.amazonaws.services.lambda.runtime.RequestHandler;

import java.util.Map;

// Handler value: example.Handler
public class Handler implements RequestHandler<Map<String,String>, String>{

  @Override
  public String handleRequest(Map<String,String> event, Context context)
  {
    System.out.println("Hello World!");
    LambdaLogger logger = context.getLogger();
    logger.log("EVENT TYPE: " + event.getClass());
    try {
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
    return "Hello";
  }

  static {
      try {
        Thread.sleep(10000);
        System.out.println("static!");
    } catch (InterruptedException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
    } 
}

ビルドしてパッケージ化する

プロジェクトを選択して右クリックし、「Maven ビルド...」をクリックします。

「ゴール」には、「package shade:shade」と入力します。

「BUILD SUCCESS」と出ました。

パッケージ化した jar ファイルを AWS Lambda にアップロードする

target ディレクトリを右クリックし、Property をクリックすると、場所がわかります。

ボタンを押すと ファインダーが開きます。(Windows ならおそらく Explorer)

target ディレクトリに jar ができています。

AWS Lambda の関数を作成します。 java 17 のランタイムを選択し、適当に名前をつけて作成します。

jar をアップロードします。

ハンドラーの名前を「example.Handler::handleRequest」に変更します。

デフォルトだと Lambda 関数のタイムアウトが 15 秒になっているので、伸ばしておきます。(Edit から伸ばせます。)

<変更後>

Lambda のテスト実行

Lambda をテスト実行したところ、1 回目は、14 秒で終わっていました。
1 回目なので、「初期化フェーズ」がある Cold Start パターンです。

「初期化フェーズ」の static イニシャライザーが 10 秒待ち、ハンドラメソッドが 3秒待つので、想定した通りの動きです。 static イニシャライザーが動いた証拠に、static! と Log output に出ています。

2 回目は、3 秒で終わっていました。
2 回目なので、「初期化フェーズ」がない Warm Start パターンです。
インスタンス化が終わっており、ハンドラメソッドが実行されるのみです。 3秒待って、 "Hello" と返却します。
Log output をみると、ハンドラメソッドのみ動いて Hello World を出力しています。

再掲:
Lambda を作成して最初の 1 回目の実行時には、VM を作成し、ランタイムを初期化し、Static コード と コンストラクタ を実行し、インスタンス化を行ないます。その後に、Lambda 側で指定したハンドラメソッドを実行します。(Cold Start)
2 回目以降の実行時には、インスタンス化した状態で、ハンドラメソッドのみを実行します。(Warm Start)

AWS re:Invent 2021 - Best practices of advanced serverless developers より

まとめ

AWS Lambda で java 17 を動かしてみました。
Cold Start と Warm Start の違いを実際に確認できました。

以下、後編です。

blog.serverworks.co.jp

以下、一意性チェックツールを試してみた編です。

blog.serverworks.co.jp

以下、スナップショットの保存期間を超過させてみた編です。
blog.serverworks.co.jp

以下、概要編です。
blog.serverworks.co.jp

余談

休暇を取って、地元の静岡にある赤石山脈の山々を歩いてきました。
見事な景色に見惚れ、帰りたくなくなりました。

山本 哲也 (記事一覧)

カスタマーサクセス部のエンジニア(一応)

好きなサービス:ECS、ALB

趣味:トレラン、登山(たまに)