概要
API Gateway + Lambda構成のバックエンドシステムで、Secrets Managerから認証情報を取得する処理において、想定よりも数秒程度のレスポンス遅延が発生していました。
調査の結果、boto3クライアントをグローバルスコープで初期化していなかったことが主な原因と判明。グローバルスコープへの移動とリトライ/タイムアウト設定の追加により、性能問題を解決しました。
原因
boto3クライアントを関数ハンドラー内で毎回生成していたため、リクエストごとにクライアント初期化のオーバーヘッドが発生していました。
改善前のコード
以下のようなコードを書いていました。
import json
import boto3
def lambda_handler(event, context):
secrets_client = boto3.client('secretsmanager')
# Secrets Managerから認証情報取得
response = secrets_client.get_secret_value(SecretId='my-secret')
credentials = json.loads(response['SecretString'])
# 以降の処理...
しかし、上記の場合、Lambda関数が呼び出されるたびに、以下の処理が毎回実行されていました。
- boto3クライアントオブジェクトの生成
- AWS認証情報の取得と検証
- エンドポイント解決
- HTTPSコネクションプールの作成
これがリクエストごとに数百ミリ秒〜数秒のオーバーヘッドになっていました。
解決策
import os
import boto3
from botocore.config import Config
# グローバルスコープでConfig定義
BOTO3_RETRY_CONFIG = Config(
retries={
"max_attempts": 10,
"mode": "standard"
},
connect_timeout=5,
read_timeout=5
)
AWS_REGION = os.environ.get('AWS_REGION', 'ap-northeast-1')
# グローバルスコープでクライアント初期化
SECRETS_MANAGER_CLIENT = boto3.session.Session().client(
'secretsmanager',
region_name=AWS_REGION,
config=BOTO3_RETRY_CONFIG
)
from xxx.secrets_manager import SECRETS_MANAGER_CLIENT
def lambda_handler(event, context):
# 既に初期化済みのクライアントを使用(初期化コストなし)
response = SECRETS_MANAGER_CLIENT.get_secret_value(SecretId='my-secret')
# ...
関数内で初期化していると毎回生成コストが発生してしまいますが、
グローバル初期化だと再利用できるので、生成コストが発生しません。
数秒の差が生まれていたのはこれが理由でした。
その他のポイント
リトライ設定の追加
retries={
"max_attempts": 10, # 最大10回までリトライ
"mode": "standard" # AWS SDK標準のリトライ戦略
}
上記により、一時的なネットワーク障害やスロットリングエラー時に自動でリトライしてくれます。
モードは以下の選択肢があるようですが、legacyより機能拡張、adaptiveは実験的、ということでstandardにしました。AWSでもRecommendedと書いてありました。
mode – A string representing the type of retry mode botocore should use. Valid values are:
legacy - The pre-existing retry behavior.
standard - The standardized set of retry rules. This will also default to 3 max attempts unless overridden.
adaptive - Retries with additional client side throttling.
standard – (Recommended) The recommended set of retry rules across AWS SDKs.
Adaptive mode is an experimental mode and is subject to change, both in features and behavior.
タイムアウト設定の追加
connect_timeout=5, # 接続タイムアウト5秒
read_timeout=5 # 読み取りタイムアウト5秒
これにより、以下の効果を期待しています。
- ネットワーク遅延時に5秒で早期検知
- タイムアウト後、即座にリトライ可能
- Lambdaタイムアウト前(30秒で設定)に適切にリトライできる
リトライ、タイムアウトは適宜設定されてください。
AWS公式のベストプラクティス
AWS Lambda公式ドキュメントでも、以下のベストプラクティスが推奨されています。
Take advantage of execution environment reuse to improve the performance of your function. Initialize SDK clients and database connections outside of the function handler, and cache static assets locally in the /tmp directory. Subsequent invocations processed by the same instance of your function can reuse these resources. This saves cost by reducing function run time.
日本語にすると
SDKクライアントとデータベース接続は関数ハンドラーの外で初期化
といった感じです。
今回、こちらを意識せずに実装していたため、レスポンス遅延が発生してしまっていました。。。
ということで以上!
上記によりLambda関数のパフォーマンスを向上させることができました。同じ関数インスタンスによる後続の呼び出しでこれらのリソースを再利用でき、実行時間の短縮につながることが分かりました。