Lambda の同時実行 1,000 の裏側 - Firecracker ウォームプールとワーカーマネージャーの仕組み

Lambda が同時実行数 1,000 のデフォルト制限内でマイクロ VM を管理する仕組み、ウォームプールによる再利用戦略、ワーカーマネージャーの配置判断、コールドスタート率を下げる内部最適化を解説します。

Lambda の実行環境のライフサイクル

Lambda の関数呼び出しが到着すると、Lambda サービスは実行環境 (Execution Environment) を割り当てます。実行環境は Firecracker マイクロ VM 上で動作するサンドボックスで、関数のコード、ランタイム、設定された環境変数を含みます。実行環境のライフサイクルは 3 つのフェーズで構成されます。INIT フェーズでは、ランタイムの起動とハンドラ外のグローバルスコープのコードが実行されます。INVOKE フェーズでは、ハンドラ関数が呼び出されます。SHUTDOWN フェーズでは、ランタイムの終了処理が実行されます。重要なのは、INVOKE フェーズが完了した後、実行環境は即座に破棄されるのではなく、一定時間「フリーズ」状態で保持されるという点です。次のリクエストが到着すると、フリーズされた実行環境が「解凍」されて再利用されます。これがウォームスタートです。フリーズ状態の実行環境は、CPU 時間を消費せず、メモリだけを占有します。Lambda はこのフリーズされた実行環境のプールを管理しており、これが「ウォームプール」です。

ウォームプールの管理戦略

Lambda のウォームプール管理は、コールドスタート率の最小化とリソース効率の最大化のバランスを取る最適化問題です。ウォームプールに実行環境を長時間保持すれば、コールドスタートは減りますが、メモリを占有し続けるため、物理ホストのキャパシティが圧迫されます。逆に、すぐに破棄すればリソースは解放されますが、コールドスタートが増えます。この均衡をどのアルゴリズムで取っているかは Lambda の内部実装で、公開されていません。公式に文書化されているのは、呼び出しを終えた実行環境がフリーズ状態で保持され後続の呼び出しに再利用されること、そして Lambda が任意のタイミングでその実行環境を終了することの 2 点だけです。ウォームプールの保持時間も公表されておらず、関数の呼び出しパターンや物理ホストのキャパシティ状況によって動的に変化します。したがって設計時には、再利用されれば速いが、いつ破棄されても正しく動くことを前提にコードを書く必要があります。ハンドラ外で初期化したコネクションやクライアントを再利用する実装であっても、初回相当の初期化がいつでも起こり得るものとして扱うのが安全です。なお Firecracker 自体は Lambda と Fargate の両サービスで稼働する軽量な仮想化基盤としてオープンソースで公開されており、その設計は NSDI 2020 で発表された論文で読むことができます。

ワーカーマネージャー - どの物理ホストに配置するか

Lambda のワーカーマネージャーは、新しい実行環境をどの物理ホスト (ワーカー) に配置するかを決定するコンポーネントです。この配置判断は、複数の制約を同時に満たす必要があります。第 1 に、物理ホストのキャパシティ (CPU、メモリ) に空きがあること。第 2 に、同一の顧客の実行環境が複数の物理ホストに分散されること (障害隔離)。第 3 に、VPC 接続が必要な関数の場合、対象の VPC のサブネットに ENI が確保できること。2019 年 9 月の改善より前は、VPC 接続の Lambda 関数はコールドスタートのたびに ENI の作成とアタッチの完了を待つ必要がありました。AWS は当時の課題を「ネットワークインターフェイスの作成とアタッチに要する時間がコールドスタートを長くする」と説明しており、追加される遅延の秒数そのものは公表されていません。さらに ENI の数が同時実行数に連動して増えるため、サブネットの IP アドレス消費、アカウントあたりの ENI 上限、ENI 作成 API のレート上限という 3 つの頭打ちも同時に抱えていました。現行方式では、ENI は関数の作成時または VPC 設定の更新時にあらかじめ作成され、実行環境は Hyperplane (Network Load Balancer や NAT Gateway でも使われる AWS のネットワーク機能仮想化基盤) が管理する ENI へトンネルを張って接続します。ENI が実行環境間で共有されるため、必要数はセキュリティグループとサブネットの組み合わせごとに数個で足り、同時実行数の増加が ENI の増加に直結しなくなりました。この変更により、VPC 接続の Lambda 関数のコールドスタートは非 VPC の関数とほぼ同等になりました。

同時実行数の管理とバーストリミット

Lambda のアカウントあたりのデフォルト同時実行数 1,000 は、リージョン内のすべての関数で共有されます。関数 A が 800 の同時実行を使用していると、関数 B は 200 しか使用できません。一方、同時実行数が増えていく速度 (スケーリングレート) は共有ではなく関数単位で、公式ドキュメントでは「各リージョン・各関数につき 10 秒あたり 1,000 の実行環境インスタンス (または 10 秒あたり毎秒 10,000 リクエスト)」と定義されています。リージョンごとに初期バースト枠が決まっていて、その後は 1 分あたり一定数ずつしか増えないという旧モデルは、2023 年 11 月の改定で置き換えられました。関数単位の上限になったため、ある関数がスケーリングレートを使い切っても、同じリージョンの別の関数の立ち上がり速度は影響を受けません。この速度を超えて到着した呼び出しはスロットリングされ、同期呼び出しではエラー、非同期呼び出しでは内部キューでの再試行になります。速度の目安としては、デフォルトの 1,000 までなら 10 秒で到達でき、上限引き上げ後に 3,000 から 10,000 まで伸ばす場合でも 70 秒程度の水準です。この上限は、Firecracker マイクロ VM の起動、ENI の確保、物理ホストのキャパシティ割り当てといった内部インフラが、急激な負荷増加に追従するための時間を確保する設計です。

コールドスタート対策の 2 世代 - Provisioned Concurrency と SnapStart

立ち上がりでコールドスタートを避けたい場合の手段は 2 世代あります。Provisioned Concurrency は、指定した数の実行環境を事前に初期化してウォーム状態で維持する機能で、スケーリングレートの制約を待たずに二桁ミリ秒で応答できます。もう 1 つが Lambda SnapStart です。SnapStart では、関数バージョンを発行した時点で Lambda が初期化まで実行し、初期化済み実行環境のメモリとディスクの状態を Firecracker マイクロ VM のスナップショットとして取得、暗号化してキャッシュします。呼び出し時およびスケールアウト時には、ゼロから INIT フェーズを実行する代わりにキャッシュ済みスナップショットから実行環境を再開するため、本記事の主題である実行環境の初期化そのものの位置づけが変わります。対応ランタイムは Java 11 以降、Python 3.12 以降、.NET 8 以降で、発行済みバージョンとそれを指すエイリアスでのみ有効です。Provisioned Concurrency との併用、Amazon EFSAmazon S3 Files、512 MB を超える一時ストレージには対応しません。また 1 つのスナップショットが複数の実行環境の初期状態として再利用されるため、初期化時に生成した ID や乱数の一意性、初期化時に確立したネットワーク接続の状態は、ハンドラ側で作り直す前提で設計する必要があります。

実行環境の再利用で注意すべきこと

実行環境の再利用 (ウォームスタート) は、パフォーマンスの観点では有利ですが、開発者が注意すべき副作用があります。第 1 に、グローバル変数の状態が保持されることです。前回の呼び出しで設定したグローバル変数の値が、次の呼び出しでも残っています。これを利用してデータベース接続をキャッシュする (接続プーリング) のは推奨されるパターンですが、リクエスト固有のデータをグローバル変数に保存すると、次のリクエストで前のリクエストのデータが漏洩するバグが発生します。第 2 に、/tmp ディレクトリのファイルが保持されることです。前回の呼び出しで /tmp に書き込んだファイルが残っているため、ディスク容量 (最大 10 GB) を使い切る可能性があります。第 3 に、バックグラウンドプロセスが残ることです。ハンドラ関数内で非同期処理を開始し、完了を待たずにレスポンスを返した場合、その非同期処理は次の呼び出し時にまだ実行中の可能性があります。

参考資料 (AWS 公式)

本ページの一次情報は AWS 公式サイトおよび公式ドキュメントです。最新の仕様 / 料金は次の公式ページで確認できます。

本ページと公式ドキュメントの記述が食い違う場合は、公式ドキュメントを正としてください。