1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Lambdaのパフォーマンスチューニングで処理速度を3倍速くした話

1
Posted at

こんにちは。まちやです!

皆さんはLambdaのコールドスタート時の処理速度に悩まされたことは
ありますでしょうか?

Webアプリを開発していたのですが、画面のレンダリングに平均11秒かかるという深刻なパフォーマンス問題に直面しました。
調査の結果、原因はLambdaの処理の遅さにあることが判明。
改善対応を実施し、以下の結果を得ることができました。

コールドスタート ウォームスタート
改善前 平均 11.1 秒 平均 0.4 秒
改善後 平均 3.9 秒 平均 0.3 秒

本記事では、改善の過程で得られた知見を共有します!

動作環境

  • 言語:Python 3.13
  • ランタイム:AWS Lambda(Python 3.13)

前提知識
本記事はLambdaのライフサイクル(Init / Invoke フェーズ等)を理解していると読みやすいです。
参考:Lambdaのhandler外(メソッド以外)のコードは「コールドスタート時の1回だけ実行される」という話 - DevelopersIO


Webアプリのアーキテクチャ概要

本アプリは以下のような構成です。

  • DNS:Route 53 でドメイン管理
  • セキュリティ:WAF でWebアプリへの不正アクセスを防御
  • フロントエンド:VPC内のWeb Applicationサーバーでホスティング
  • バックエンド:複数のLambda(API群)で各種APIを処理
  • データベース:RDS Proxy経由でAurora Serverless v2に接続
  • 認証情報管理:Secrets Manager でDB接続情報等を管理

クライアントからのリクエストはInternet → Route 53 → WAF → Web Application
→ 各Lambda(API群)→ RDS Proxy → Aurora Serverless v2 という流れで処理されます。

image.png


【調査】X-Rayでボトルネックを特定する

まず、API GatewayとLambdaに AWS X-Ray を有効化し、各APIのトレースを取得しました。

AWS X-Rayの使い方:AWS公式ドキュメント - X-Ray

X-Rayトレース結果(改善前)

No  API メモリ(MB) Cold全体(秒) Init(秒) 実行(秒) Warm全体(秒) Warm実行(秒)
1 API1 512 8.2 4.5 3.0 1.2 1.2
2 API2 256 11.3 2.2 8.2 0.3 0.3
3 API3 512 12.5 4.6 7.3 0.2 0.2
4 API4 512 8.5 4.4 3.5 0.3 0.3
5 API5 256 11.6 2.3 8.0 0.3 0.3
6 API6 512 14.2 4.4 9.1 1.3 1.3
7 API7 512 12.6 4.5 7.4 0.3 0.3
8 API8 512 9.1 4.7 3.7 0.1 0.1
9 API9 512 12.4 4.4 7.3 0.4 0.3
10 API10 256 11.0 2.1 8.1 0.2 0.2
11 API11 256 10.6 2.1 7.7 0.3 0.2
12 API12 256 11.4 2.5 8.2 0.2 0.2

考察

トレース結果から、2つの問題が浮かび上がりました。

① Initフェーズに時間がかかりすぎている
コールドスタート時のInitフェーズだけで平均 3.6秒 を要していました。
Webアプリとして初期化だけでこれだけの時間がかかるのは致命的です。

② コールドスタート時のInvokeフェーズが異常に遅い

  • コールドスタート時の実行時間(Init除く):平均 6.8秒
  • ウォームスタート時の実行時間:平均 0.4秒

この差は「毎回実行する必要のない処理が、コールドスタート時に毎回走っているのでは?」と推察しました。


【改善策1】Lambda SnapStartでInitフェーズを高速化

SnapStartとは?

コールドスタートを改善する主な方法は2つあります。

① Lambda SnapStart
InitフェーズのサーバーState(メモリ・ディスク)をスナップショットとしてS3に保存しておき、起動時にそれをリストアすることで、通常のInitよりも高速に起動できる機能です。

② Provisioned Concurrency
初期化済みのLambda実行環境を指定した同時実行数分だけ事前にプールしておく機能です。設定した数のインスタンスを常にWarm状態に保ちます。

今回の選択:SnapStart

Provisioned Concurrencyは「Warm状態のサーバーを常時起動し続けるコスト」が発生し、「実行した分だけ課金」というLambdaのメリットが薄れます。
そのため、今回は SnapStart を採用しました。
また、参照系APIのみに絞って適用しています。

SnapStart適用後のX-Rayトレース結果

SnapStart適用後はInitフェーズがなくなり、代わりにRestoreフェーズ(スナップショットのリストア)が追加されます。

No  API メモリ(MB) Cold全体(秒) Restore(秒) Invocation(秒) Warm全体(秒) Warm Invocation(秒)
1 API1 512 5.7 0.9 4.6 1.2 1.1
2 API2 256 6.9 0.8 5.9 0.2 0.1
3 API3 512 6.5 1.4 5.0 0.2 0.2
4 API4 512 6.1 0.9 5.0 0.2 0.2
5 API5 256 7.1 0.9 6.1 0.1 0.1
6 API6 512 8.0 0.8 7.0 1.0 1.0
7 API7 512 6.9 1.0 5.8 0.2 0.2
8 API8 512 5.5 0.8 4.6 0.1 0.1
9 API9 512 6.6 0.9 5.5 0.3 0.2
10 API10 256 6.7 0.8 5.7 0.1 0.1
11 API11 256 6.4 0.8 5.5 0.1 0.1
12 API12 256 6.7 0.8 5.7 0.1 0.1

SnapStart適用結果

コールドスタート ウォームスタート
適用前 平均 11.1 秒 平均 0.4 秒
適用後 平均 6.6 秒 平均 0.3 秒

【改善策2】Secrets Managerの取得情報をグローバル変数にキャッシュする

問題

今回のアプリはDBが複数スキーマ構成のため、スキーマごとに異なる接続情報をSecrets Managerから取得する必要がありました。

改善前のコード(イメージ)

def get_password(schema):
    # Secrets Manager APIを呼ぶ
    return secrets_manager.get_secret(f"{schema}/password")

def get_schema_name(schema):
    # Secrets Manager APIを呼ぶ
    return secrets_manager.get_secret(f"{schema}/name")

def lambda_handler(event, context):
    for schema in ["schema_a", "schema_b", "schema_c"]:
        password = get_password(schema)    # 3回呼ばれる
        name = get_schema_name(schema)     # 3回呼ばれる
        # → 合計6回 Secrets Manager APIを呼ぶ

アプリ仕様の関係で3つのスキーマへの接続が必要なのですが、
メソッドが呼ばれるたびにSecrets Manager APIを叩いていたため、合計6回のAPI呼び出しが発生していました。

改善後のコード(イメージ)

# グローバルスコープでキャッシュ
_secrets_cache = {}

def get_secret(schema):
    if schema not in _secrets_cache:
        # 初回のみ Secrets Manager APIを呼ぶ
        secret = secrets_manager.get_secret(f"{schema}/credentials")
        _secrets_cache[schema] = {
            "password": secret["password"],
            "name": secret["name"]
        }
    return _secrets_cache[schema]

def lambda_handler(event, context):
    for schema in ["schema_a", "schema_b", "schema_c"]:
        credentials = get_secret(schema)
        # → 合計3回に削減(初回のみ、以降はキャッシュを利用)

パスワードとスキーマ名をセットで取得・キャッシュすることで、API呼び出しを6回→3回に削減しました。

AWSも推奨しています
AWS公式動画でも、Secrets Managerの認証情報取得はグローバルスコープでキャッシュすることが推奨されています。

適用結果

コールドスタート ウォームスタート
改善前 平均 6.6 秒 平均 0.3 秒
改善後 平均 5.8 秒 平均 0.3 秒

このアプリは複数スキーマのDB設計にしてますが
1つのスキーマにデータを集約することができれば
RDSProxyのコネクションプーリングが使えるし、
Secrets Managerへの問合せも一回で済むんで
もっと早くなるんだろうとは思います・・・


【改善策3】boto3クライアントをグローバルスコープで初期化する

問題

AWSのLambda ベストプラクティスでは、boto3クライアントはグローバルスコープで宣言することが推奨されています。

しかし、改善前のコードでは lambda_handler 内でクライアントを初期化しているケースが多数ありました。

改善前のコード(イメージ)

def lambda_handler(event, context):
    # ハンドラー内で毎回初期化 → 呼び出しのたびに100〜300msのコストが発生
    s3_client = boto3.client('s3')
    dynamodb = boto3.resource('dynamodb')
    ...

改善後のコード(イメージ)

import boto3

# グローバルスコープで初期化 → 再利用されるためコスト発生は初回のみ
s3_client = boto3.client('s3')
dynamodb = boto3.resource('dynamodb')

def lambda_handler(event, context):
    # 初期化済みのクライアントをそのまま利用
    s3_client.put_object(...)
    ...

boto3.client() の初期化には 100〜300ms 程度かかってました。
グローバルスコープで宣言することで、Lambdaの実行環境が再利用される限りクライアントも再利用されるため、2回目以降の呼び出しでは初期化コストがかかりません。

適用結果

コールドスタート ウォームスタート
改善前 平均 5.8 秒 平均 0.3 秒
改善後 平均 3.9 秒 平均 0.3 秒

まとめ

3つの改善策を適用した結果、コールドスタートを 11.1秒 → 3.9秒 に短縮できました。

改善策 コールドスタート 効果
改善前(ベースライン) 11.1 秒 -
① Lambda SnapStart適用 6.6 秒 ▲ 4.5秒短縮
② Secrets Managerキャッシュ化 5.8 秒 ▲ 0.8秒短縮
③ boto3クライアントのグローバル化 3.9 秒 ▲ 1.9秒短縮
合計 3.9 秒 ▲ 7.2秒短縮(約65%改善)

今回の学び

1. まずX-Rayで計測する
「なんとなく遅い」の状態から改善を始めても効果は出にくいので、X-Rayでトレースを取り、どのフェーズに時間がかかっているかを定量的に把握することが改善の第一歩です。

2. グローバルスコープを有効活用する
Lambdaはウォームスタート時に実行環境が再利用されます。この特性を活かし、以下のリソースはグローバルスコープで初期化・キャッシュするのがベストプラクティスです。

  • boto3クライアント
  • Secrets Managerから取得した認証情報
  • DBコネクション など

3. パフォーマンス改善はコスト削減にもつながる
Lambdaは実行時間に応じて課金されます。処理時間を短縮することは、ユーザー体験の向上だけでなく、Lambda実行コストの削減にも直結します。まさに一石二鳥です。


以上、Lambda のパフォーマンスチューニングの実例でした。
同じような問題で悩んでいる方の参考になれば幸いです!

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?