Amazon Web Services ブログ
Operating Lambda: パフォーマンスの最適化 – Part 1
Operating Lambda シリーズでは、AWS Lambda ベースのアプリケーションを管理している開発者、アーキテクト、およびシステム管理者向けの重要なトピックを取り上げます。この 3 部構成のシリーズでは、Lambda ベースのアプリケーションのパフォーマンスの最適化について説明します。
サーバーレスアプリケーションは、並列化と同時実行が容易であることから、非常に高いパフォーマンスを実現することができます。Lambda サービスはスケーリングを自動的に管理しますが、アプリケーションで使用する個々の Lambda 関数を最適化することで、レイテンシーを削減し、スループットを向上させることもできます。
本稿では、Lambda 実行環境のライフサイクルやコールドスタートの定義、測定方法、およびその改善方法について説明します。
コールドスタートとレイテンシーを理解する
Lambda サービスが Lambda API を介して関数を実行するリクエストを受け取ると、サービスは最初に実行環境を準備します。このステップでは、サービスは内部の Amazon S3 バケット (関数がコンテナパッケージを使用している場合は Amazon Elastic Container Registry) に保存されている関数のコードをダウンロードします。次にサービスは、指定されたメモリ、ランタイム、および各種設定に基づいた環境を作成します。これらが完了すると、Lambda はイベントハンドラー外に記述された初期化コードを実行した後に、最終的にハンドラーコードを実行します。
この図における、環境とコードをセットアップする最初の 2 つのステップは、しばしば「コールドスタート」と呼ばれます。Lambda が関数の準備に要する時間は課金されませんが、全体的な実行時間に対して遅延を及ぼします。
実行が完了すると、実行環境はフリーズされます。リソース管理とパフォーマンスを向上させるために、Lambda サービスは実行環境を不特定期間保持します。この間、同じ関数に対する追加のリクエストが到着すると、サービスは環境を再利用するよう試みます。通常、この 2 番目のリクエストはより迅速に終了します。これは、実行環境がすでに存在し、コードをダウンロードして初期化コードを実行する必要がないためです。これは「ウォームスタート」と呼ばれます。
本番環境の Lambda ワークロードの分析によると、コールドスタートが発生するのは通常、呼び出しの 1% 未満です。コールドスタートに要する時間は、100 ミリ秒未満から 1 秒以上までさまざまです。Lambda サービスはウォームな環境を後続の呼び出しに再利用するため、コールドスタートは通常、本番環境のワークロードよりも開発およびテスト中の関数で多くみられます。これは、一般的に開発やテスト中の関数が呼び出される頻度が、本番環境のそれよりも低いためです。
実行環境のライフサイクル
Lambda サービスは、実行後すぐには実行環境を破棄せず、保持しつづけます。環境の存続期間はさまざまな要因の影響を受け、現在のところ開発者が設定することはできません。存続期間は Lambda サービスの運用上の要因によっても影響されます。
実行環境の再利用は有用ですが、パフォーマンスの最適化をこれに依存すべきではありません。Lambda は、AWS リージョン内の複数の Availability Zones に跨って実行を管理する可用性の高いサービスです。お客様のトラフィックの総量に応じて、サービスはいつでも関数をロードバランスする可能性があります。その結果、短時間に関数が 2 回呼び出されたとしても、この負荷再分散の挙動によりいずれの実行においてもコールドスタートが発生する事態も起こり得ます。
加えて、トラフィックによって Lambda 関数がスケールアップされる際には、関数の追加の同時呼び出しごとに、新たな実行環境が必要となります。これは、すでにウォームになっている既存の同時実行可能な関数存在している場合でも、追加の同時実行分においてはコールドスタートが発生することを意味します。
最後に、Lambda 関数のコードを更新したり、関数の設定を変更した場合、次の呼び出しにおいてコールドスタートが発生します。確実に新しいバージョンのコードのみが使用されるよう、前バージョンの「Latest」エイリアスを実行している既存の環境は破棄されます。
関数ウォーマーの仕組みを理解する
サーバーレスのコミュニティでは、ping メカニズムを介して Lambda 関数を「ウォーム」するためのオープンソースライブラリが提供されています。このアプローチでは、 EventBridge ルール を使用して 1 分ごとに関数の呼び出しをスケジュールすることで、実行環境をアクティブに保ちます。これによって、関数を呼び出す際にウォーム環境が使用される可能性が高まることが期待できます。
しかしながら、これはコールドスタートを減らすことを保証することができる方法ではありません。本番環境においてトラフィックに合わせて関数をスケールアップする必要が生じた場合には効果を発揮しません。また、Lambda サービスの通常の負荷分散オペレーションの一環として別のアベイラビリティーゾーンで関数が実行された場合にも機能しません。さらに、Lambda サービスは実行環境を最新の状態に保つために定期的に破棄するため、ping の隙間に関数が呼び出される可能性があります。これらのすべてのケースで、ウォーミングライブラリを使用しているにもかかわらず、コールドスタートを経験することになります。このアプローチは、開発環境やテスト環境、低トラフィックまたは優先度の低いワークロードにでは適しているかもしれません。
さらに、呼び出しの際にウォームな環境をターゲットとして明示することはできません。Lambda サービスは、内部のキューイングと最適化のファクターに基づいて、どの実行環境がリクエストを受信するかを決定します。そこには、従来のロードバランサーで設定されるような、繰り返し要求に対するアフィニティや「スティッキーセッション」の概念は存在しません。
Provisioned Concurrency によるコールドスタートの削減
ワークロードで予測可能な関数の開始時間が必要な場合は、可能な限り低いレイテンシーを確保するために推奨されるソリューションは Provisioned Concurrency です。この機能により、関数は初期化およびウォームアップされ、プロビジョニングしたスケールで 2 桁のミリ秒で応答できる準備が整います。これは、オンデマンドの Lambda 関数とは異なり、初期化コードの実行を含め、すべてのセットアップアクティビティが呼び出しの前に行われることを意味します。
たとえば、Provisioned Concurrency が 6 の関数では、実際の呼び出しが発生する前から 6 つの実行環境が準備されます。実行環境の準備は初期化から呼び出しまでの間に完了します。
Provisioned Concurrency を設定された関数は、いくつかの重要な点でオンデマンドの関数とは異なります。
- 初期化コードを最適化する必要はありません。時間のかかる初期化処理は呼び出しのずっと前に行われるためレイテンシーに影響しません。Java のように、通常は初期化に時間がかかるランタイムを使用している場合、プロビジョニングされた同時実行を使用することでパフォーマンスの向上が期待できます。
- 初期化コードが、設定された Provisioned Concurrency よりも多く実行される場合があります。Lambda は高可用性であるため、Provisioned Concurrency の 1 単位ごとに、別々のアベイラビリティーゾーンに少なくとも 2 つの実行環境が準備されます。これは、サービスに中断が発生した場合でもコードを実行できるようにするためです。環境が破棄されたり、負荷分散が行われることがあるので、Lambda は可用性を確保するために環境をオーバープロビジョニングします。このアクティビティに対して課金は発生しません。初期化コードがロギングを実装している場合、メインのハンドラが呼び出されていない場合であっても、このコードが実行されるたびに追加のログが観測されます。
- $LATEST バージョンでは Provisioned Concurrency を使用できません。この機能は、発行されたバージョンと関数のエイリアスでのみ使用できます。Provisioned Concurrency を使用するように設定された関数でコールドスタートが観測される場合は、Provisioned Concurrency が設定されたバージョンまたはエイリアスの代わりに $LATEST バージョンを呼び出している可能性があります。
呼び出しパターンを理解する
Lambda の実行環境は、一度に 1 つずつリクエストを処理します。呼び出しの終了後、実行環境は一定期間保持されます。別のリクエストが到着すると、後続のリクエストを処理するために環境が再利用されます。
リクエストが同時に到着すると、Lambda サービスは複数の実行環境を提供するために Lambda 関数をスケールアウトします。各環境は個別にセットアップされる必要があるため、各呼び出しで完全なコールドスタートが発生します。
たとえば、Amazon API Gateway が Lambda 関数を同時に呼び出した場合、Lambda は 6 つの実行環境を作成します。各呼び出しの実行時間には、コールドスタートが含まれます。
ところが API Gateway が Lambda 関数を 6 回連続して呼び出す際に、各呼び出しの間に遅延がある場合、以前の呼び出しが完了していれば既存の実行環境が再利用されます。以下の例では、最初の 2 つの呼び出しでのみコールドスタートが発生し、3 ~ 6 の呼び出しではウォーム環境が使用されます。
非同期呼び出しの場合、呼び出し側と Lambda サービスの間に内部キューが存在します。Lambda は必要に応じて自動的にスケールアップしつつ、このキューからのメッセージをできるだけ早く処理します。関数に Reserved Concurrency が指定されている場合、これは関数単位の同時実行数の上限として機能するため、関数が処理できるようになるまでメッセージは内部キューに保持されます。
たとえば S3 バケットに対して、オブジェクトがバケットに書き込まれた際に Lambda 関数を呼び出すよう設定されていたとします。
Lambda 関数の Reserved Concurrency が 1 であれば、6 つのオブジェクトが同時にバケットに書き込まれたとしてもイベントは単一の実行環境によって順次処理されます。保留中のイベントは内部キューに保持されます。
結論
この記事は、Lambda でのパフォーマンス最適化に関するシリーズの 3 部構成の最初のパートです。Lambda 実行環境の仕組みと、コールドスタートが発生する理由について解説しました。
コールドスタートでのレイテンシーを最小限に抑えるために、関数ウォーマーの仕組みと、Provisioned Concurrency が本番環境のワークロードにおいて推奨されるソリューションである理由をご説明しました。最後に呼び出しのパターンをご紹介し、呼び出しモードが呼び出しの挙動や同時実行にどのように影響するかについてのいくつかの例を提示しました。
パート 2 では、メモリ構成が Lambda のパフォーマンスに及ぼす影響と、静的な初期化コードを最適化する方法について説明します。
サーバーレスの学習リソースについては、こちらで見つけることができます。
この記事の翻訳は Solution Architect 石井が担当しました。原文はこちらからご覧いただけます。