KotlinでAWS Lambda SnapStartを利用しコールドスタートの改善を検証してみた
はじめに
AWS LambdaでKotlinを利用しています。
LambdaでKotlinを利用する場合、気になるのがコールドスタートです。
今回は、この課題を解決するLambda SnapStartを実際に試し、以下の2点を検証しました。
- SnapStart で、起動速度と挙動はどう変わるのか?
- 実務での実装指針:初期化ブロック(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