🙂

KotlinでAWS Lambda SnapStartを利用しコールドスタートの改善を検証してみた

に公開

はじめに

AWS LambdaでKotlinを利用しています。
LambdaでKotlinを利用する場合、気になるのがコールドスタートです。
今回は、この課題を解決するLambda SnapStartを実際に試し、以下の2点を検証しました。

  1. SnapStart で、起動速度と挙動はどう変わるのか?
  2. 実務での実装指針:初期化ブロック(init)には何を書くべきなのか?

検証には Windows (WSL) × Terraform 環境を使用し、実際にAWSリソースを構築して確認しました。

SnapStartとは?

通常、Lambdaはリクエストが来てから初期化を行いますが、SnapStartは、Lambdaの初期化フェーズ完了直後の状態をバージョン公開時にスナップショットとして保存し、実行時はその状態から復元する機能です。

検証コード

今回は、初期化処理の重さをシミュレーションするため、「S3クライアントの生成」と「バケット一覧の取得」を行うコードで検証しました。
また、SnapStartの挙動を確認するためにinitブロックとhandleRequestの両方でUUIDやバケット数を取得しています。

class App : RequestHandler<Map<String, String>, Map<String, String>> {

    private lateinit var cachedUuid: String
    private val s3Client: S3Client
    private val cachedBucketCount: Int

    init {
        println("========== 初期化フェーズ 開始 ==========")
        cachedUuid = UUID.randomUUID().toString()
        // S3クライアント生成とAPIコール
        s3Client = S3Client.builder()
            .region(Region.AP_NORTHEAST_1)
            .build()

        val buckets = s3Client.listBuckets().buckets()
        cachedBucketCount = buckets.size

        println("========== 初期化フェーズ 終了 ==========")
    }

    override fun handleRequest(input: Map<String, String>, context: Context): Map<String, String> {
        val freshUuid = UUID.randomUUID().toString()
        val freshBucketCount = s3Client.listBuckets().buckets().size

        return mapOf(
            "cachedUuid" to cachedUuid,
            "freshUuid" to freshUuid,
            "cachedBucketCount" to cachedBucketCount.toString(),
            "freshBucketCount" to freshBucketCount.toString()
        )
    }
}

検証結果 1:起動速度の比較

Terraformで環境を構築し、SnapStartの有効と無効で、コールドスタート時のログを比較しました。

実行ログの比較

項目 SnapStart 無効 SnapStart 有効
計測対象 Init Duration Restore Duration
時間 (ms) 約 2,478 ms 約 704 ms

結果

SnapStart無効時は、init ブロック内で行っている「S3クライアントの初期化とリスト取得」の時間がそのまま待ち時間に含まれています。
一方、SnapStart有効時は、初期化ブロック内の処理はバージョン公開時に完了しているため、コールドスタート時の待ち時間には含まれていません。

検証結果 2:挙動の違い(値のキャッシュ)

次に、複数回リクエストを送った際の cachedUuid(初期化時に生成)と freshUuid(実行時に生成)の値を確認しました。

比較表:変数のライフサイクル

変数の種類 SnapStart 無効 SnapStart 有効
cachedUuid
(initで生成)
コンテナ再作成のたびに変わる バージョンを更新しない限り変わらない
(全実行環境で同一の値になる)
freshUuid
(handleRequestで生成)
毎回変わる 毎回変わる
cachedBucketCount
(initで取得)
比較的最新の値 バージョン公開時の値で固定される

実際の挙動

SnapStart有効環境では、15分経過しようが、別の実行環境が立ち上がろうが、cachedUuidは常に同じ値が返ってきました。

初期化ブロック(init)に書くべき処理

「重くて、かつ変更頻度が低い(静的な)もの」です。以下の処理は初期化ブロック(init)に記載すればいいと思いました。

  • AWS SDK クライアントの生成 (S3, DynamoDBなど)
  • 設定ファイルの読み込み
  • 重い計算処理による定数生成

ハンドラーに書くべき処理

「リクエストごとに異なる(動的な)もの」です。以下のような処理を初期化ブロック(init)に書くと値が固定化されてしまうので注意が必要だと思います。

  • UUIDや乱数の生成(異なる実行環境でも同じ値が使われてしまう)
  • 現在時刻の取得(デプロイした時刻で固定されてしまう)
  • 頻繁に変わる外部データの取得(再デプロイするまで更新されない)

おわりに

Kotlinでも、SnapStartを活用することでコールドスタートの影響を軽減できました。

Discussion