【第1回】AWS Lambda × Python パフォーマンス最適化入門 — SnapStart Python対応で何が変わる?コールドスタート問題を基礎から理解しよう

AWS

個人用に作った小さなAPIを久しぶりに叩いたら、最初の1回だけ3秒近く待たされて「あ、これがコールドスタートか」と実感しました。普段使いには困らないんですが、フロントから叩くと明らかに気持ち悪い。

というわけで、今回から4回に分けてAWS Lambda(Python)のパフォーマンス最適化について調べたことをまとめていきます。ざっくりこんな流れを予定してます。

  • 第1回(今回):コールドスタートの正体と SnapStart の仕組み・Python対応の現状
  • 第2回:実際に計測する(CloudWatch Logs / X-Ray / Powertools あたり)
  • 第3回:コード側の最適化(インポート、boto3クライアント、レイヤー、パッケージサイズ)
  • 第4回:メモリ設定チューニングとコストのバランス、Provisioned Concurrency との比較

予定は未定なので、途中で順番が変わるかもしれません。あくまで自分の学習ログという感じで。

この記事でわかること

  • AWS Lambda コールドスタートで時間がかかる理由
  • SnapStart がコールドスタート問題にどう対処するのか
  • Python 版 SnapStart の対応状況と制約
  • SnapStart 有効化の設定方法とランタイムフック
  • 課金モデルと Provisioned Concurrency との使い分け

そもそもコールドスタートのどこで時間を食っているのか

Lambdaが新しい実行環境を作るとき、大まかに「コードのダウンロード・展開」「ランタイムの起動」「初期化コード(ハンドラ外)の実行」が走ります。AWSのドキュメントでも、起動レイテンシの最大の要因はこの初期化(Init)フェーズだと書かれています。

Pythonの場合、体感で一番効くのが3つめの「初期化コードの実行」です。import pandasimport numpy、あるいはPydanticのモデル定義、機械学習モデルのロード。この辺を入れると、数百msどころか秒単位まで簡単に膨らみます。逆にハンドラ外がスカスカな関数だと、Pythonのコールドスタートはそこまで悪くない(100ms台になることもあるようです)。つまり「Pythonが遅い」というより「自分が読み込ませてるものが重い」という話でもあるんですよね。ここは第3回で掘ります。

SnapStart は「初期化済みの状態」を丸ごと保存する仕組み

で、本題のSnapStartです。仕組みは意外とシンプルで、関数バージョンを publish したタイミングで一度だけ初期化を実行し、初期化が終わった実行環境のメモリとディスクの状態をスナップショットとして取るという方式。以後、新しい実行環境が必要になったら、そのスナップショットを復元して初期化済みの地点から再開します。

スナップショットは Firecracker microVM のメモリ/ディスク状態を暗号化して、取り出しレイテンシが小さくなるようにキャッシュされる、と説明されています。要は「毎回イチから初期化するのをやめて、初期化済みのセーブデータをロードする」イメージです。ゲームのクイックロードみたいな感覚で理解しています(合ってるかは自信ないですが、大枠はこれで困ってません)。

もともとは2022年にJava向けとして登場した機能で、Python と .NET に拡張されたのは2024年11月。Java専用だった時期が長かったので、Python使いとしては「やっと来た」やつです。

Python対応の現状と、意外と多い制約

2026年8月時点で押さえておきたいのはこの辺です。

  • 対応ランタイムは Python 3.12 以降(Java 11以降、.NET 8以降も対応)
  • Node.js や Ruby などのマネージドランタイム、OS-onlyランタイム、コンテナイメージは非対応
  • Provisioned Concurrency(プロビジョニングされた同時実行)とは併用できない
  • EFS、512MBを超えるエフェメラルストレージも非対応
  • 使えるのは publish済みのバージョンとエイリアス経由だけ$LATEST では効かない
  • リージョンはほぼ全ての商用リージョンで利用可(アジアパシフィックのニュージーランド・台北を除く、とドキュメントに記載あり)

個人的に一番の落とし穴は「コンテナイメージ非対応」です。Dockerでイメージ固めてデプロイするスタイルに慣れてると、そもそも選択肢に入りません。自分はまさにそれで、試すためにzipデプロイに戻しました。

有効化はほぼワンフラグ、でもコードは少し気をつける

設定自体は驚くほど簡単です。SAMだとこれだけ。

Resources:
  MyFunc:
    Type: AWS::Serverless::Function
    Properties:
      Runtime: python3.13
      Handler: app.lambda_handler
      AutoPublishAlias: live   # バージョン発行が必要
      SnapStart:
        ApplyOn: PublishedVersions

問題は「スナップショットに含まれてしまうもの」の扱いです。初期化時に作った一意な値やコネクションが、復元された全環境で共有されてしまう。UUIDや乱数のシード、DNSキャッシュ、DB接続、トークンなどが典型例で、AWSも「一意なコンテンツは初期化後に生成しろ」とはっきり書いています。

そのために用意されているのがランタイムフックです。Pythonでは snapshot_restore_py というライブラリ(マネージドランタイムに同梱済み。デプロイパッケージに含めなくてよい)のデコレータを使います。

from snapshot_restore_py import register_before_snapshot, register_after_restore

conn = None

@register_before_snapshot
def before_snapshot():
    global conn
    if conn:
        conn.close()
        conn = None

@register_after_restore
def after_restore():
    global conn
    conn = create_connection()  # 復元後に張り直す

def lambda_handler(event, context):
    return {"ok": conn is not None}

ハマりポイントとして、フックを別モジュールに書いた場合、そのモジュールが handler 側から import されないと無視されるとドキュメントに明記されています。トップレベルに書きましょう。あと @register_after_restore は10秒以内に完了しないと SnapStartTimeoutException になるので、復元後に重い処理を詰め込むのは危険。before側は初期化時間の上限(130秒、または設定タイムアウトの大きい方)にカウントされます。

余談ですが、この「before で閉じて after で開き直す」パターン、書いてるうちにDB接続の寿命管理を自分で考えることになるので、結果的にコネクション周りの理解が進みました。遠回りだけど悪くない。

タダではない:課金モデルは先に見ておくべき

ここが見落としやすいところで、Python / .NET のSnapStartには通常のLambda料金に加えて2種類の課金があります。

  • キャッシュ料金:SnapStartを有効にして publish したバージョンごとに、スナップショットを保持している間かかる。最低3時間分が請求される
  • リストア料金:スナップショットから実行環境が復元されるたびにかかる

どちらもメモリ量に比例します。具体的な単価はリージョンやタイミングで変わりますが、見落としがちなのは「使っていない古いバージョンが残り続ける」パターン。CI/CDで毎回 publish していると、放置されたバージョン分のキャッシュ料金が積み上がります。AWS自身も「未使用のバージョンは削除しろ」と案内しています。

なので、SnapStartを本格的に使うならデプロイパイプラインに古いバージョンの掃除処理を入れるのがほぼ前提かなと。この辺、まだ自分の中でベストプラクティスが固まっていません。もっとスマートなやり方がありそう。

Provisioned Concurrency とどう使い分けるか

両方使えないので選択になります。ドキュメントの書き方を素直に読むと、Provisioned Concurrency は常に初期化済みの環境を保持して2桁ミリ秒で応答する、より厳しいレイテンシ要件向け。ただしアイドル中も課金され続けます。

一方SnapStartは、復元自体に数百ms程度かかるケースもあるようですが、初期化が重い関数なら数秒→サブ秒に落ちる。かつ、待機コストは発生しません。

ざっくりした自分の理解では、初期化が重くてトラフィックが読めない関数はSnapStart、常に一定のリクエストがあって数十msを死守したいならProvisioned Concurrency。逆に、そもそも初期化が軽い関数にSnapStartを入れても旨味は薄く、課金だけ増える可能性があります。ここは実測しないと判断できない部分です。

※この記事にはプロモーションが含まれます

ちなみに、お名前.com レンタルサーバー(WordPressに特化した高速レンタルサーバー。月額990円〜、独自ドメイン実質0円)も気になっています。お名前.com レンタルサーバー

まとめ

SnapStartは「フラグ1個で速くなる魔法」ではなくて、対応ランタイム・バージョン運用・一意性の扱い・課金モデルまで含めて理解してから入れる機能だと思いました。ただ、条件が合ったときの効果はかなり大きいはずです。

次回は実際にコールドスタートを計測する方法について見ていきます。CloudWatch LogsやX-Rayを使った計測方法も含めて、データを取ってから最適化に進むのが安全ですね。

📚 シリーズ「AWS Lambda × Python パフォーマンス最適化入門」(第1回 / 全4回)

→ 次回の記事: 公開後にリンクが追加されます

参考になったらクリックしてもらえると嬉しいです!

Blogmura CloudAWS Ranking
タイトルとURLをコピーしました