java ランタイムの Lambda 関数で、関数を実行する毎に一意な ID を生成して使うには

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

こんにちは🐱 カスタマーサクセス部の山本です。

以下のブログ記事で、Lambda の Cold Start と Warm Start の違いを説明しつつ、SnapStart 機能について記述しました。
本記事を書くにあたり、前提となる知識であるため、掻い摘んで紹介します。

blog.serverworks.co.jp

SnapStart 機能とは(上記ブログからの引用):

Lambda を作成して最初の 1 回目の実行時には、VM を作成し、ランタイムを初期化し、Static コード と コンストラクタ を実行し、インスタンス化を行ないます。その後に、Lambda 側で指定したハンドラメソッドを実行します。 そのため、 1 回目の実行には非常に時間がかかります。 2 回目以降の実行時には、インスタンス化した状態で、ハンドラメソッドのみを実行します。 VM を作成し、インスタンス化を行なうまでの段階を「初期化フェーズ」と言います。 Lambda に複数のリクエストが同時にあった場合には、新しいVMを作成して処理するので、新たに初期化フェーズを実行することがあります。 初期化フェーズが走る場合を Cold Start と言い、インスタンス化している状態でハンドラメソッドのみ実行する場合は Warm Start と言います。 「初期化フェーズ (Cold Start)」が終わった状態のスナップショットを作成し、そのスナップショットからVMを復元して利用するのが、SnapStart という仕組みです。java のランタイム向けにある機能です。 VM を作成し、インスタンス化を行なうまでの「初期化フェーズ」に時間がかかりすぎるので、あらかじめスナップショットを取得しておきVMを復元する方が早い、という話ですね。 これで、Cold Start 問題がほぼほぼ解決する、というわけです。

参考:Cold Start と Warm Start (「Invocation」=「実行」。実行するまでにかかる時間が Cold Start が入ると長い。)

java ランタイムの Lambda 関数で、関数を実行する毎に一意な ID を生成して使うには

関数を実行する毎に一意な ID を生成する必要がある場合には、Lambda に設定しているハンドラ関数の中で行います。
それだけです。

初期化フェーズ(staticコード 、コンストラクタ)のコード内で一意な ID を作成していると、意図したとおりになりません。SnapStart を ON に設定している Lambda 関数バージョンでは、初期化フェーズを行ってインスタンス化した環境のスナップショットを最初に作り、使い回します。そのため、SnapStart が作ったスナップショットの中に埋め込まれている ID を使うようになってしまいます。 スナップショットは複数あるので、実際にはいくつかの ID を使い回します。ですので、最初のうちは気付かないかもしれません。SnapStart を設定していないときは、Warm Start になると、Cold Start 時に作成した一意な ID を使い回します。
そこで、「初期化フェーズの中で意図せずに一意な ID を作成していないか?」を確認するツール(SnapStart スキャンツール)があります。
このツールを本記事では試してみます。

参考までに、一意性に関する公式ドキュメントでの記載です。

SnapStart 関数での呼び出しがスケールアップすると、Lambda は単一の初期化されたスナップショットを使用して、複数の実行環境を再開します。スナップショットに包含される一意のコンテンツを初期化コードが生成する場合、そのコンテンツは、複数の実行環境で再利用されるときに一意にならない可能性があります。SnapStart の使用時に一意性を維持するには、初期化後に一意のコンテンツを生成する必要があります。これには、一意の ID、一意のシークレット、および疑似ランダム性を生成するために使用されるエントロピーが含まれます。

コードで一意性を維持できるように、以下のベストプラクティスをお勧めします。Lambda は、一意性を前提とするコードのチェックに役立つ、オープンソースの SnapStart スキャンツールも提供します。初期化フェーズ中に一意のデータを生成する場合は、ランタイムフックを使用して一意性を復元することができます。ランタイムフックを使用すると、Lambda がスナップショットを取得する直前、または Lambda がスナップショットから関数を再開した直後に、特定のコードを実行できます。

正しいコード例

初期化フェーズ(Static イニシャライザ や コンストラクター) では 一意な ID を生成しません。(9〜16行目)
その代わりに、Lambda 関数 の ハンドラーとして定義する handleRequest メソッドで一意な ID (handlerSandboxId) を生成しています。(21行目)
ID をハンドラー関数内で標準出力しています。(22行目)

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;
import java.util.UUID;


public class Handler implements RequestHandler<Map<String,Object>, String> {
    private UUID handlerSandboxId;

    // Static イニシャライザ
    static {
    System.out.println("static block");
    }

    // コンストラクター
    public Handler(){
    System.out.println("constructor");
    }

    // Lambda 関数ハンドラー
    @Override
    public String handleRequest(Map<String,Object> event, Context context) {
    System.out.println("override handle request");
    handlerSandboxId = UUID.randomUUID(); // <-- unique content created
    System.out.println("handler Sandbox Id: " + handlerSandboxId);
    return "Hello, World!";
    }

    // SnapStart プラグイン以外の SpotBugs のバグ検知を発生させるためのクラス
    private class SpotBugsCheck {

    }
}

Lambda 側では example パッケージの Handler クラスにある、 handleRequest メソッドをハンドラに設定しています。

  • example.Handler::handleRequest

正しいコードでの実行結果

Lambda のサービス画面から何回かテスト実行してみます。
実行毎に一意な ID (handlerSandboxId) を発行できています。

誤ったコード例

初期化フェーズ(Static イニシャライザ や コンストラクター) で 一意な ID を生成します。(12〜22行目)
Lambda 関数 の ハンドラーとして定義する handleRequest メソッドでは一意な ID (handlerSandboxId) を生成していません。(23〜30行目)
ID をハンドラー関数内で標準出力しています。(27〜28行目)

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;
import java.util.UUID;


public class Handler implements RequestHandler<Map<String,Object>, String> {
    private static UUID staticSandboxId;
    private final UUID finalSandboxId;
    private UUID handlerSandboxId;

    // Static Initializer
    static {
    System.out.println("static block");
    staticSandboxId = UUID.randomUUID(); // <-- unique content created
    System.out.println("Static Sandbox Id: " + staticSandboxId);
    }

    // Constructor
    public Handler(){
    System.out.println("constructor");
    finalSandboxId = UUID.randomUUID(); // <-- unique content created
    System.out.println("Final Sandbox Id: " + finalSandboxId);
    }

    // Lambda 関数ハンドラー
    @Override
    public String handleRequest(Map<String,Object> event, Context context) {
    System.out.println("override handle request");
    System.out.println("Static Sandbox Id: " + staticSandboxId);
    System.out.println("Final Sandbox Id: " + finalSandboxId);
    return "Hello, World!";
    }

    // SnapStart プラグイン以外の SpotBugs のバグ検知を発生させるためのクラス
    private class SpotBugsCheck {

    }
}

誤ったコードでの実行結果

Lambda のサービス画面から何回かテスト実行してみます。
実行毎に一意な ID (handlerSandboxId) を発行できていません





「初期化フェーズの中で意図せずに一意な ID を作成していないか?」を確認するツール:SnapStart スキャンツール

次に、SnapStart スキャンツール を試してみましょう。
SnapStart スキャンツールは、SpotBugs のプラグインです。
Github リポジトリ: GitHub - aws/aws-lambda-snapstart-java-rules

Maven で試す

SpotBugs プラグインの Git リポジトリに沿ってpom.xmlを編集します。
<!-- SnapStart スキャンツール --> で囲まれた範囲にプラグインの情報を追加しています。
README では version が0.2.1 になっていますが、0.2.0でないと使えなかったので修正しています。(88行目)
参考:https://repo.maven.apache.org/maven2/software/amazon/lambda/snapstart/aws-lambda-snapstart-java-rules/

<?xml version="1.0" encoding="UTF-8"?>

<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>uniqueness</groupId>
  <artifactId>uniqueness</artifactId>
  <version>1.0-SNAPSHOT</version>

  <name>uniqueness</name>
  <!-- FIXME change it to the project's website -->
  <url>http://www.example.com</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
  </properties>

  <dependencies>
    <dependency>
      <groupId>com.amazonaws</groupId>
      <artifactId>aws-lambda-java-core</artifactId>
      <version>1.2.2</version>
    </dependency>
    <dependency>
      <groupId>com.amazonaws</groupId>
      <artifactId>aws-lambda-java-events</artifactId>
      <version>3.11.1</version>
    </dependency>
    <dependency>
      <groupId>com.amazonaws</groupId>
      <artifactId>aws-lambda-java-log4j2</artifactId>
      <version>1.5.1</version>
    </dependency>
  </dependencies>

  <build>
    <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
      <plugins>
        <!-- clean lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#clean_Lifecycle -->
        <plugin>
          <artifactId>maven-clean-plugin</artifactId>
          <version>3.1.0</version>
        </plugin>
        <!-- default lifecycle, jar packaging: see https://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_jar_packaging -->
        <plugin>
          <artifactId>maven-resources-plugin</artifactId>
          <version>3.0.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-compiler-plugin</artifactId>
          <version>3.8.0</version>
        </plugin>
        <plugin>
          <artifactId>maven-surefire-plugin</artifactId>
          <version>2.22.1</version>
        </plugin>
        <plugin>
          <artifactId>maven-jar-plugin</artifactId>
          <version>3.0.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-install-plugin</artifactId>
          <version>2.5.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-deploy-plugin</artifactId>
          <version>2.8.2</version>
        </plugin>
        <!-- site lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#site_Lifecycle -->
        <plugin>
          <artifactId>maven-site-plugin</artifactId>
          <version>3.7.1</version>
        </plugin>
        <plugin>
          <artifactId>maven-project-info-reports-plugin</artifactId>
          <version>3.0.0</version>
        </plugin>
    <!-- SnapStart スキャンツール -->
        <plugin>
          <groupId>com.github.spotbugs</groupId>
          <artifactId>spotbugs-maven-plugin</artifactId>
          <version>4.7.3.0</version>
          <configuration>
            <effort>Max</effort>
            <threshold>medium</threshold>
            <failOnError>true</failOnError>
            <plugins>
              <plugin>
                <groupId>software.amazon.lambda.snapstart</groupId>
                <artifactId>aws-lambda-snapstart-java-rules</artifactId>
                <version>0.2.0</version>
              </plugin>
            </plugins>
          </configuration>
        </plugin>
    <!-- SnapStart スキャンツール -->
      </plugins>
    </pluginManagement>
  </build>
</project>

確認手順

まず、compile か package を行います。 例では package で行います。

mvn package



次に、スキャンを行います。 スキャンをすると、targets 配下に結果ファイル spotbugsXml.xml ができています。

mvn spotbugs:spotbugs



最後に、スキャン結果の spotbugsXml.xml の中身を GUI ツールで見てみます。
「誤ったコード例」に記したコードで試したところ、「初期化フェーズで一意な ID を生成しているので、意図しない動作になる可能性がある」というエラーが出ていました。

mvn spotbugs:gui

別の画面が起動する。

エラー本文

Detected handler state that is potentially not resilient to VM snapshot and restore operations. Our analysis shows that AWS Lambda handler class initialization creates state that may not remain unique for the function when it uses SnapStart. Lambda functions that use SnapStart are snapshotted at their initialized state and all execution environments created afterwards share the same initial state. This means that if the Lambda function relies on state that is not resilient to snapshot and restore operations, it might manifest an unexpected behavior by using SnapStart. This tool helps provide an insight on possible cases where your code may not be fully compatible with snapstart enabled. Please verify that your code maintains uniqueness with SnapStart. For best practices, follow the guidelines outlined in the SnapStart documentation.

Bug kind and pattern: SNAP_START - AWS_LAMBDA_SNAP_START_BUG



コマンドラインで GUI ツールと同じ確認をすることもできます。

mvn spotbugs:check

SnapStart スキャンツール に関する補足

「誤ったコード例」に記した UUID (java.util.UUID)の他にも、 Random (java.util.Random)で生成した疑似乱数や、System.currentTimeMillis で生成した時刻(timestamp)などが初期化フェーズに含まれていないか確認できるようです。

github.com

また、何を検知できるのかを全部確認するのは難しそうでした。 以下フォルダにテストコードがあり、探すことはできそうです。

aws-lambda-snapstart-java-rules/src/test/java/software/amazon/lambda/snapstart/LambdaHandlerInitedWithRandomValueTest.java at main · aws/aws-lambda-snapstart-java-rules · GitHub

aws-lambda-snapstart-java-rules/src/test/java/software/amazon/lambda/snapstart/lambdaexamples at main · aws/aws-lambda-snapstart-java-rules · GitHub

Snap Start 概要編

blog.serverworks.co.jp

Snap Start 試してみた前編:

blog.serverworks.co.jp

Snap Start 試してみた後編:

blog.serverworks.co.jp

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

余談

最近はバス釣りにハマっています。
週末は野尻湖でスモールマウスバスを釣っていました。

山本 哲也 (記事一覧)

カスタマーサクセス部のエンジニア。2024 Japan AWS Top Engineers に選んでもらいました。

今年の目標は Advanced Networking – Specialty と Machine Learning - Specialty を取得することです。

山を走るのが趣味です。今年の目標は 100 km と 100 mile を完走することです。 100 km は Gran Trail みなかみで完走しました。残すは OSJ koumi 100 で 100 mile 走ります。実際には 175 km らしいです。「草 100 km / mile」 もたまに企画します。

基本的にのんびりした性格です。座右の銘は「いつか着く」