こんにちは。
アプリケーションサービス部、DevOps担当の兼安です。
今回は、先日AWS Lambdaのコールドスタートを短縮するAWS Lambda SnapStartがPythonのサポートを開始したということで、これについてご紹介します。
- 本記事の対象者
- AWS Lambda SnapStartとは
- AWS Lambdaのコールドスタートとウォームスタート
- AWS Lambdaのコールドスタートを起こしてみる
- AWS Lambda SnapStartを使ってコールドスタートを改善
- まとめ
本記事の対象者
本記事はAWS Lambdaの基礎知識がある方、そして需要増に備えてチューニングの必要性を感じておられる方を対象としています。
AWS Lambda SnapStartとは
AWS Lambda SnapStartは、AWS Lambda関数のコールドスタートの時間を短縮する機能です。
これまでは、Javaのみで利用可能でしたが、Pythonと.NETでも利用可能になりました。
AWS Lambda SnapStartについては、AWS re:Invent 2024のセッションでも取り上げられており、動画も公開されています。
併せて視聴いただけると、より理解が深まるかと思います。
AWS Lambdaのコールドスタートとウォームスタート
AWS Lambda SnapStartの紹介の前に、まずはAWS Lambdaのコールドスタートとウォームスタートについて簡単に説明します。
コールドスタートとは、Lambda関数が初めて呼び出された際に、Lambdaランタイムが新しいコンテナを起動し、関数のコードを読み込んで初期化することを指します。
この初期化処理には、Lambda関数のコードを読み込むだけでなく、依存関係の解決や初期化処理などが含まれるため、コールドスタートの時間はウォームスタートに比べて長くなります。
反対に、ウォームスタートとは、Lambda関数が既存のコンテナ上で素早く実行される状態を指します。
一般的に、コールドスタートは以下のようなケースで発生します。
- AWS Lambda関数のコードや設定が更新された場合
- 需要増により、AWS Lambda関数の新しい実行環境が必要になった場合
基本的に、AWS Lambda関数を一度呼び出した後は、極力再利用がされるようになっており、ウォームスタートが発生することが多いですが、需要増によりコールドスタートが発生することもあると解釈した方が良いでしょう。
AWS Lambdaのコールドスタートを起こしてみる
まずは、Lambda関数におけるコールドスタート自体を確認しておきます。
サンプルコードはこちらです。
import time import logging logger = logging.getLogger() logger.setLevel(logging.INFO) large_resource = None def initialize_large_resource(): global large_resource logger.info("Initializing large resource (only on cold start)...") time.sleep(3) # 擬似的な遅延 large_resource = [i for i in range(10**6)] # 大きなリストを作成 logger.info("Resource initialized!") # Lambdaハンドラー def lambda_handler(event, context): global large_resource # グローバル変数が初期化されていないなら初期化 if large_resource is None: initialize_large_resource() start_time = time.time() response = {"message": "Hello, World!", "input": event} end_time = time.time() logger.info(f"Execution time: {end_time - start_time:.3f} seconds") return response
このコードをデプロイした直後に、Lambda関数を呼び出すと、コールドスタートが発生します。
初回はグローバル変数が初期化されていないので、初期化処理が実行されます。
初期化処理に数秒かかる実装にしているため、起動時に時間がかかる挙動になります。
Function Logs: START RequestId: 6bbce71b-04cd-4f2f-b482-bedd5693eb0e Version: $LATEST [INFO] 2024-12-24T09:05:00.610Z 6bbce71b-04cd-4f2f-b482-bedd5693eb0e Initializing large resource (only on cold start)... [INFO] 2024-12-24T09:05:04.812Z 6bbce71b-04cd-4f2f-b482-bedd5693eb0e Resource initialized! [INFO] 2024-12-24T09:05:04.812Z 6bbce71b-04cd-4f2f-b482-bedd5693eb0e Execution time: 0.000 seconds END RequestId: 6bbce71b-04cd-4f2f-b482-bedd5693eb0e REPORT RequestId: 6bbce71b-04cd-4f2f-b482-bedd5693eb0e Duration: 4241.56 ms Billed Duration: 4242 ms Memory Size: 128 MB Max Memory Used: 69 MB Init Duration: 101.13 ms Request ID: 6bbce71b-04cd-4f2f-b482-bedd5693eb0e
2回目以降の実行では、ウォームスタートが発生し、初期化処理がスキップされるため、実行時間が短縮されます。
Function Logs: START RequestId: 874e1347-0313-4a38-b163-050a02eb87c4 Version: $LATEST [INFO] 2024-12-24T09:05:48.614Z 874e1347-0313-4a38-b163-050a02eb87c4 Execution time: 0.000 seconds END RequestId: 874e1347-0313-4a38-b163-050a02eb87c4 REPORT RequestId: 874e1347-0313-4a38-b163-050a02eb87c4 Duration: 1.81 ms Billed Duration: 2 ms Memory Size: 128 MB Max Memory Used: 69 MB Request ID: 874e1347-0313-4a38-b163-050a02eb87c4
この関数に対して、改行を追加でもなんでもよいので修正を加えてデプロイし、再度呼び出すと、再びコールドスタートが発生します。
このように、コールドスタートはLambda関数のコードや設定が更新された場合に発生することがわかります。
AWS Lambda SnapStartを使ってコールドスタートを改善
では、これに対してAWS Lambda SnapStartを試してみます。
SnapStartは以下のステップで動作することで、コールドスタートの時間を短縮する機能です。
- Lambda関数のコードをスナップショットとして保存
- 次回以降の起動時にスナップショットを復元する
- 復元後の状態からLambda関数を起動する
この時、1.と2.において処理を挟むことができ、これをフックと呼びます。
このフックと併せることで、より実務に沿った使い方がイメージできると思うので、一気にフック処理を試してみます。
PythonにおけるSnapStartのフックの使い方はこちらに記載されています。
フック:register_before_snapshotを使ってみる
まずは、SnapStartのフック:register_before_snapshot
を使ってみます。
import time import logging from snapshot_restore_py import register_before_snapshot logger = logging.getLogger() logger.setLevel(logging.INFO) large_resource = None @register_before_snapshot def before_checkpoint(): if large_resource is None: initialize_large_resource() def initialize_large_resource(): global large_resource logger.info("Initializing large resource (only on cold start)...") time.sleep(3) # 擬似的な遅延 large_resource = [i for i in range(10**6)] # 大きなリストを作成 logger.info("Resource initialized!") # Lambdaハンドラー def lambda_handler(event, context): global large_resource # グローバル変数が初期化されていないなら初期化 if large_resource is None: initialize_large_resource() start_time = time.time() response = {"message": "Hello, World!", "input": event} end_time = time.time() logger.info(f"Execution time: {end_time - start_time:.3f} seconds") return response
これをデプロイした後、スナップショットを作成します。
まずは、設定でSnapStartを有効にします。
この状態でバージョンを作成します。
メッセージもあるように、SnapStartを有効にしてからバージョンを作成すると、バージョン作成に時間がかかるようになります。
晴れてバージョンが作成されたら、そのバージョンを指定してLambda関数を呼び出すと、初回のコールドスタート時でもグローバル変数の初期化がスキップされ、起動時間が短縮されていることが確認できます。
これはSnapStartの機能により、コードの読み込みとグローバル変数の初期化処理が済んだ状態でLambda関数が起動されるためです。
フック:register_before_snapshot
は、スナップショットを作成する前に実行されるため、初期化処理がスナップショットに含まれることになります。
Function Logs: RESTORE_START Runtime Version: python:3.13.v13 Runtime Version ARN: arn:aws:lambda:ap-northeast-1::runtime:b881cbc9a10a8bcb3def9d9e9fe38f922bb36510a1d92d4ce85cf2a899eeabd8 RESTORE_REPORT Restore Duration: 566.08 ms START RequestId: 23361531-c5e9-4080-9978-0d9be9793e03 Version: 1 [INFO] 2024-12-24T08:51:45.369Z 23361531-c5e9-4080-9978-0d9be9793e03 Execution time: 0.000 seconds END RequestId: 23361531-c5e9-4080-9978-0d9be9793e03 REPORT RequestId: 23361531-c5e9-4080-9978-0d9be9793e03 Duration: 28.67 ms Billed Duration: 93 ms Memory Size: 128 MB Max Memory Used: 76 MB Restore Duration: 566.08 ms Billed Restore Duration: 64 ms Request ID: 23361531-c5e9-4080-9978-0d9be9793e03
フック:register_after_restoreを使ってみる
今度は、スナップショットを復元した後のフック:register_after_restore
を使ってみます。
import time import logging from snapshot_restore_py import register_after_restore logger = logging.getLogger() logger.setLevel(logging.INFO) large_resource = None @register_after_restore def after_restore(): if large_resource is None: initialize_large_resource() def initialize_large_resource(): global large_resource logger.info("Initializing large resource (only on cold start)...") time.sleep(3) # 擬似的な遅延 large_resource = [i for i in range(10**6)] # 大きなリストを作成 logger.info("Resource initialized!") # Lambdaハンドラー def lambda_handler(event, context): global large_resource # グローバル変数が初期化されていないなら初期化 if large_resource is None: initialize_large_resource() start_time = time.time() response = {"message": "Hello, World!", "input": event} end_time = time.time() logger.info(f"Execution time: {end_time - start_time:.3f} seconds") return response
こちらも同じように、これをデプロイした後、スナップショットを有効化した状態でバージョンを作成します。
そして、作成したバージョンを指定してLambda関数を呼び出すと、初回はグローバル変数の初期化処理が走り起動が遅く、2回目以降は初期化処理がスキップされて起動が早いというスナップショットを適用する前と同じ挙動を確認できます。
フック:register_after_restore
は、スナップショットを復元した後に実行されるため、初期化処理がスナップショットに含まれないことになります。
これが、register_before_snapshot
を使った時との挙動の違いを産んでいます。
register_before_snapshotとregister_after_restoreの使い分け
Lambda関数を書く時、起動を早くするため一部の変数をグローバル変数にしておくことがあります。
この時、データベースコネクションなど、永続的に保持されると問題になる変数は、register_after_restore
を使うとよいでしょう。
それら以外の、1回しか初期化しない変数・データは、register_before_snapshot
を使うとよいと思われます。
まとめ
AWS Lambda SnapStartを使うことで、Lambda関数のコールドスタートを改善することができます。
コールドスタートはデプロイ以外だと、不意な需要増により発生することが多いので、需要の予測が難しい場合にはAWS Lambda SnapStartは有効です。
兼安 聡(執筆記事の一覧)
アプリケーションサービス部 DS3課所属
2024 Japan AWS Top Engineers (Database)
2024 Japan AWS All Certifications Engineers
Certified ScrumMaster
PMP
広島在住です。今日も明日も修行中です。