はじめに
パフォーマンスの観点で、意識すると Lambda 活用のステップアップが見込める事項についてまとめます。
実行環境間で保たれるデータを活用しよう
コールドスタートとウォームスタート
Lambda の実行環境のライフサイクルは、大きく INIT
, INVOKE
, SHUTDOWN
のフェーズから構成されており、どのフェーズから起動されるかによって、起動の種類が以下のように呼ばれています。
起動種別 | 概要 |
---|---|
コールドスタート | 初期化フェーズ(INIT )を含めた起動。プログラムのダウンロード及び実行環境の設定が実行される。 |
ウォームスタート | 初期化フェーズ(INIT )を含めず、INVOKE フェーズからの起動。実行環境が再利用される。 |
再利用される環境で保持されるデータを活用する(グローバルスコープの活用)
import json print("Global Scope") def lambda_handler(event, context): print("Local Scope") return {"statusCode": 200, "body": json.dumps("Hello from Lambda!")}
上記のプログラムを Lambda のコードとして記述し、デプロイしたあとに 2 回実行してみます。ここではコンソール上から「Test」を押下して実行しました。
すると以下のようなログが CloudWatch Logs で確認できます。
INIT_START Runtime Version: python:3.13.v31 Runtime Version ARN: arn:aws:lambda:ap-northeast-1::runtime:... Global Scope START RequestId: 8dd0bc2f-fcef-4328-88d0-05be1c0376aa Version: $LATEST Local Scope END RequestId: 8dd0bc2f-fcef-4328-88d0-05be1c0376aa REPORT RequestId: 8dd0bc2f-fcef-4328-88d0-05be1c0376aa Duration: 2.25 ms Billed Duration: 3 ms Memory Size:... START RequestId: 779aabe1-b32b-4ece-8884-ffe8a8ea1369 Version: $LATEST Local Scope END RequestId: 779aabe1-b32b-4ece-8884-ffe8a8ea1369 REPORT RequestId: 779aabe1-b32b-4ece-8884-ffe8a8ea1369 Duration: 1.90 ms Billed Duration: 2 ms Memory Size:...
「Global Scope」という文字列は 1 回、「Local Scope」という文字列は 2 回表示されています。ここからもわかるように、ハンドラ(lambda_handler 関数)外で記述されたグローバルスコープの標準出力は、初期化フェーズ(INIT
)の 1 度のみ実行されます。環境が再利用して実行されるウォームスタートでは、グローバルスコープは処理されず、ハンドラ内の処理が実行され、「Local Scope」という文字列が実行した回数(2 回)だけ表示されます。
この性質を利用することで、ハンドラ内で共通的に利用する変数、関数、インスタンスはグローバルスコープに記述することで、その分の処理を効率化ができます。
ただし、リクエスト間で共有したくない情報などは、グローバルスコープに記載しないよう注意が必要です。
エフェメラルストレージを活用する
Lambda から特定のファイルをダウンロードする処理を模擬するため、lambda-tmp-behavior-{account_number}
という名前の S3 バケットを作成し、そこにサンプルデータを記載した JSON ファイルを配置します。
今回用意したサンプルデータは以下です。
[ { "id": 1, "name": "Alice", "age": 30 }, { "id": 2, "name": "Bob", "age": 25 }, { "id": 3, "name": "Charlie", "age": 35 } ]
そして、以下のプログラムを 2 回実行してみます。
import json import boto3 s3_client = boto3.client('s3') response = s3_client.get_object(Bucket='lambda-tmp-behavior-{account_number}', Key='sample.json') body = response['Body'] # Lambdaの/tmpディレクトリに保存 with open('/tmp/sample.json', 'wb') as f: f.write(body.read()) def lambda_handler(event, context): with open('/tmp/sample.json', 'r', encoding='utf-8') as f: data = json.load(f) print(data) return {"statusCode": 200, "body": json.dumps("Hello from Lambda!")}
CloudWatch Logs の出力は以下になります。
INIT_START Runtime Version: python:3.13.v31... START RequestId: ... [{'id': 1, 'name': 'Alice', 'age': 30}, {'id': 2, 'name': 'Bob', 'age': 25}, {'id': 3, 'name': 'Charlie', 'age': 35}] END RequestId: ... REPORT RequestId: ... START RequestId: ... [{'id': 1, 'name': 'Alice', 'age': 30}, {'id': 2, 'name': 'Bob', 'age': 25}, {'id': 3, 'name': 'Charlie', 'age': 35}] END RequestId: ... REPORT RequestId: ...
ファイルの取得処理はグローバルスコープに記載したため、実行環境存続期間中初めの 1 度だけ実行されます。ローカルスコープに記載された JSON データのパース処理は、初回及び実行環境を再利用した 2 回目の合計 2 回実行されました。
このように /tmp
ディレクトリを利用することで、特定ファイルを実行環境存続期間中に共有できます。ただし、前セクションでの説明と同様、実行環境間で参照させたくないデータが含まれる場合は、そもそも /tmp
を利用しないか、データ処理後はファイルを削除する等の対応が必要です。
遅延ロード
遅延ロードの仕組みを説明するために、処理を同期的に実行する場合と、非同期的に実行する場合とで分けて考えてみます。Python では非同期処理を行うための標準ライブラリとして asyncio
が提供されており、今回はこれを使って非同期処理の具体例を示します。
遅延ロードしない場合
import asyncio import json def lambda_handler(event, context): body = json.loads(event.get('body')) process = body['process'] if process == 'sync': pass elif process == 'async': pass return {"statusCode": 200, "body": json.dumps("Hello from Lambda!")}
遅延ロードする場合
import json def lambda_handler(event, context): body = json.loads(event.get('body')) process = body['process'] if process == 'sync': pass elif process == 'async': # 遅延ロード import asyncio pass return {"statusCode": 200, "body": json.dumps("Hello from Lambda!")}
違いは、単に asyncio
モジュールのインポートが、グローバルスコープからローカルスコープに変更されたのみです。
遅延ロードによる効果
前述のそれぞれのケースで、 INIT
フェーズに要した処理時間は以下の通りです。以下のデータは X-Ray のトレースを有効化して計測しました。
遅延ロード | 平均 [ミリ秒] | 標準偏差 [ミリ秒] |
---|---|---|
なし | 144.4 | 7.02 |
あり | 83.6 | 4.72 |
当然の結果ではありますが asyncio
を遅延ロードしたことにより、INIT
フェーズに要する時間を削減できました。遅延ロードするモジュールを選定することで、コールドスタート問題を改善することができます。
コンピューティングパワーのチューニング
Lambda では実行環境に割り当てるメモリ量を 128 MB - 10,240 MB
の範囲で設定できます。また、メモリの増強に伴って CPU のパワーも比例的に増加すると述べられています。
以下のサンプルプログラムに対して、128 MB 及び 256 MB のメモリを割り当てた場合の処理速度を計測しました。ここでは、フィボナッチ数列の第 30 項を求めるのに要した時間を計測しています。
import json import time def lambda_handler(event, context): start_time = time.time() fibonacci(30) end_time = time.time() delta = end_time - start_time print(delta) return {"statusCode": 200, "body": json.dumps("Hello from Lambda!")} def fibonacci(n): if n <= 1: return n return fibonacci(n-1) + fibonacci(n-2)
計測結果は以下です。
メモリ [MB] | 平均 [秒] | 標準偏差 [秒] |
---|---|---|
128 | 2.31 | 0.0181 |
256 | 1.14 | 0.0161 |
メモリを 2 倍にした場合、処理速度もおよそ 2 倍になりました。
Lambda のコストは割り当てたメモリ量に比例して増加するため、料金面での最適化が求められます。費用対効果を考慮して適切なメモリ設定を決定する必要がありますが、特定のシナリオでは、多くのメモリを割り当てることで性能向上が実現可能だと考えられます。
CPU boost
公式ドキュメント等ではあまり記載がありませんが、 AWS Lambda Performance Tuning Deep Dive
の資料には INIT
フェーズの 10 秒間に CPU が増強される CPU boost
という特性があると述べられています。
この特性について、メモリ 128 MB の設定のもと、以下の 2 パターンで処理速度を計測しました。
グローバルスコープの処理開始直後にサンプルの処理を実施する場合
import json import time def fibonacci(n): if n <= 1: return n return fibonacci(n-1) + fibonacci(n-2) start_time = time.time() fibonacci(30) end_time = time.time() delta = end_time - start_time print(delta) def lambda_handler(event, context): return {"statusCode": 200, "body": json.dumps("Hello from Lambda!")}
グローバルスコープの処理開始 10 秒後にサンプルの処理を実施する場合
import json import time def fibonacci(n): if n <= 1: return n return fibonacci(n-1) + fibonacci(n-2) time.sleep(10) # 10 秒待機 start_time = time.time() fibonacci(30) end_time = time.time() delta = end_time - start_time print(delta) def lambda_handler(event, context): return {"statusCode": 200, "body": json.dumps("Hello from Lambda!")}
計測結果は以下です。
計測タイミング | 平均 [秒] | 標準偏差 [秒] |
---|---|---|
直後 | 0.174 | 0.00648 |
10 秒後 | 2.33 | 0.0720 |
この結果から、グローバルスコープは単に共通化で活用するだけではなく、10 秒間だけではありますが CPU パワーが増強される CPU boost の恩恵を受けられることが分かりました。