Operating Lambdaを読んでLambdaのパフォーマンスチューニングしてみたら○○%性能改善した
きっかけ
先日妻からの要望を受け、家事や週末にやろうと思っていたことを、思いついた瞬間に書き留めるための私的なアプリを作りました。
LINEのWebhookをAPI Gateway配下のLambdaが受け付け、ビューア兼データストアとして利用しているNotionに格納するというシンプルな構成です。
シンプルにもかかわらず、なんかもっさりしてるな~と思ったので、これを機にAWS ブログのOperating Lambdaシリーズを読んでパフォーマンスチューニングしてみよう!と思ったのがきっかけです。
対象読者
- Lambdaのパフォーマンスチューニングに興味がある方
- Lambdaのライフサイクルについて興味がある方
- Provisioned Concurrencyの設定方法を知りたい方
- AWS Lambda Power Tuningを使ってみたい方
- Lambdaのコスト削減に興味がある方
改善結果だけ先に知りたいという方は「6. 改善結果」をご覧ください。
1. 本記事の進め方
本記事では、個人的に開発した私的アプリを題材に、以下のAmazon Web Services ブログに基づいてLambdaの性能改善を行います。
Operating Lambda: パフォーマンスの最適化 – Part 1
Operating Lambda: パフォーマンスの最適化 – Part 2
各章では、特定の改善ポイントに焦点を当て、具体的な方法とその効果を検証していきます。
これから進める各章を通じて、Lambdaの性能をどのように最適化し、実行時間の短縮、コスト削減、ユーザーエクスペリエンスの向上を実現するかを学んでいきたいと思います。
2. 改善対象のアプリの紹介と現状のパフォーマンスについて
改善対象の備忘録アプリについて
conversationLine関数がLINEからのWebhookを受け付け、他の関数にルーティングし、最終的にLINEにレスポンスを返却します。
registerTask関数が、LINEに送ったメッセージをもとにNotionにタスクを登録します。
getTasks関数がNotionに格納されている、未完了タスクの一覧を取得します。
現状のパフォーマンス測定
実際に処理を起動して現状のパフォーマンスを測定してみます。
以下、実際にアプリが動いているgif動画です。
あえて早送りなしで投稿します。
…かなり遅いですね…
正直使いたくないです。
X-RayをPythonに仕込んでいたので、正確なタイムラインも確認してみます。
(はじめてX-Rayを利用しましたが、導入はすごく簡単でした。)
最初にAPI Gatewayが受け付けてから、LINEにレスポンスを送信するまで、8.07秒 かかっていることがわかります。
3. [part1] Lambdaのライフサイクルを理解してパフォーマンス向上
現状のパフォーマンスも分かったところで早速、Operating Lambdaをもとにパフォーマンスチューニングを始めていきます。
コールドスタートとレイテンシーを理解する
まずはライフサイクルについて理解することで、パフォーマンスを向上させていきます。
Lambdaはリクエストを受け取ると実行環境を作成し、ハンドラー関数を実行します。
実行環境を作成してから、関数を実行する場合をコールドスタートといいます。
直感的にわかりやすいですが、環境作成の待ち時間が発生します。
以下、Builders Flashの記事に掲載されていた、gif動画がわかりやすいので引用させていただきました。
X-Rayの結果を確認する
2.の最後に掲載しているX-Rayの結果はコールドスタートによるものです。
Initializationに1秒かかっていることがわかりますが、これがコールドスタート故に発生する時間となります。
さらに今回は、coversationLine関数からsend_api_requestによってregisterTask関数を呼び出しています。
以下の通り、registerTask関数もInitializationに1.15秒かかっており、コールドスタートでなければ、約2秒(約20%)の性能改善になることが期待されます。
コールドスタートの改善方法
Operating Lambdaでも紹介されている通り、コールドスタートの改善にはいくつかの方法があります。
- 関数ウォーマー
- Provisioned Concurrency(プロビジョニングされた同時実行)
前者の関数ウォーマーは様々な理由でコールドスタートの減少を保証できないため、本番ワークロードに向いていません。(詳細はOperating Lambda part1参照)
そこで、今回はProvisioned Concurrencyを設定してみました。
Provisioned Concurrencyで改善してみる
Provisioned Concurrencyの設定
Provisioned Concurrencyを有効化するためにはLambdaにバージョンを発行する必要があります。
Lambda関数のバージョンタブから新しいバージョンを作成します。
次に設定タブ→同時実行ペインを選択し、プロビジョニングされた同時実行設定を設定しようとしてみますが、バリデーションエラーで有効化できませんでした。
ドキュメントを確認してみると、予約なしアカウントの同時実行数から100を引いた数まで設定できるとのことでした。
アカウントで設定できるのは、予約なしアカウントの同時実行数から 100 を引いた数までです。残りの 100 単位の同時実行数は、予約済み同時実行を使用しない関数用です。たとえば、アカウントの同時実行数の制限が 1,000 で、他の関数に予約済みまたはプロビジョニング済み同時実行を割り当てていない場合、1 つの関数に対して最大 900 のプロビジョニング済み同時実行単位を設定できます。
予約なしアカウントの同時実行数はService Quotasから確認できます。
現状10しかなかったので、この状態ではProvisioned Concurrencyは設定できません。
なのでデフォルトのクォータ値である1000まで引き上げリクエストを行いました。
即承認とはいかず、約8時間後に承認されました。
同時実行数のクオータも緩和されたため、設定を続けていきます。
先ほどの警告は消え、プロビジョニングされた同時実行を1に設定できました。
しばらくすると、ステータスが「準備完了」になりましたので、再計測を行います。
測定
ウォームアップをした後の2発目のリクエストが以下の通りです。
1.27秒まで改善されています。
正直これは想定外でしたが、最初のトレースでもかなり時間がかかっていたget_parameters処理がかなり高速化されました。
get_parameters処理は以下の拡張機能を使って、Parameter Storeに格納されたシークレットを取得しています。
シークレットがLambdaインスタンスにキャッシュされたため、速度が改善しました。
Operating LambdaのPart1については以上となります。
続いて、Part2について確認していきます。
4. [part2] メモリとCPUのチューニングによってパフォーマンス向上
メモリとコンピューティングパワー
Part2ではLambdaの計算資源周りに着目しています。
Lambda関数はメモリ量によって利用できる仮想CPUも変わるため、パフォーマンスチューニングにおいて非常に重要なパラメータです。
加えて、Lambdaの課金体系はメモリ量×実行時間によって決定されます。
メモリ量と実行時間の最適解を導くことは、コスト面を見ても非常に重要な観点です。
AWS Lambda Power Tuningで最適なリソースを探る
オンプレでVMを利用していた際、最適なメモリ割り当て量を導くプロセスは非常に大変でした…
性能試験を計画し、シナリオやデータを準備し、測定、改善、再測定…を繰り返すことで最適なメモリ割り当て量を探っていました。
ここで紹介されているのが、「AWS Lambda Power Tuning」です。
Serverless Application Repository(SAR)からStep Functionをデプロイし、実行するだけで実行時間とコストのバランスを見ることができます。
この章では実際にこちらを利用して、最適なリソース量を探っていきます。
AWS Lambda Power Tuningのデプロイ
以下のGitHubから、SARにアクセスし、
Deployをクリック
遷移したページで、デプロイ時のパラメータを入力し、デプロイをクリックすることで簡単にデプロイできます。(この際すべてデフォルト値でもデプロイできますが、検証用なので費用を抑えるためにログ保管期限だけ短くしました。)
ほどなくして、デプロイタブでデプロイが完了していることが確認できます。
AWS Lambda Power Tuningの実行
利用方法は先ほどのGitHubの「README-EXECUTE.md」を確認していきます。
今回はマネコンから実行する方法で試してみます。
非常に簡単で、先ほどSARからデプロイしたStepFunctionを実行する際にJSONを渡すだけです。
JSONのパラメータは以下から確認できます。
実行が正常に終わると以下のようにグラフビューが成功の緑色に変わります。
「実行の入力と出力」タブを開くとStep Functionの出力結果が表示されています。
visualization句のURLにアクセスすると以下のようなグラフが確認できます。
今回は128~2048で試行した結果、2048MBが最も高速であり、256MBが最もコスト効率が良いことがわかりました。
コスト削減にはリソース割り当てを減らすことをまず考えてしまいがちですが、今回のように、スペックを上げることでコスト効率、処理速度も高速化できるケースもあることはおさえておきたいところです。
測定
コールドスタート、ウォームアップ後の処理時間は以下の通りです。
いずれも30%程度改善しています。これでコストも安くなる可能性が高いのですから、メモリチューニングは非常に重要ですね。
メモリ | コールドスタート | ウォームアップ後 |
---|---|---|
128MB | 8.07秒 | 1.27秒 |
256MB | 6.21秒 | 0.87秒 |
256MB コールドスタート
256MB ウォームアップ後
5. [part2] 静的初期化の最適化によってパフォーマンス向上
続いてもPart2の話題です。
Part1にてLambdaのライフサイクルについて学習しました。
ここではライフサイクルを考慮して、初期化の処理を行うことでパフォーマンスを向上するテクニックについて紹介されています。
静的初期化の仕組みとパフォーマンス向上テクニック
Handler関数外に定義されている処理は、Initの際に動作します。
以後、インスタンスが使いまわされている間は再実行されません。
記事内でも紹介されていますが、定数や他サービスへの接続の初期化など、リクエストごとに変得る必要がない処理は静的初期化内で実施してしまうことで、ウォームアップ後の処理時間を短縮することができます。
コールドスタートが主なワークロードの場合はこのテクニックはあまり輝かないですが、Provisioned Concurrencyを利用するワークロードの場合は有用なテクニックですね。
Lambda拡張機能をInitフェーズで実装したかった
「したかった」というように今回の検証ではうまくいきませんでした。
静的初期化プラクティスを試してみようと思い、都度再作成する必要がないパラメータをハンドラー関数外に出してみようと思いました。
今回のアプリではRDBMSにプールを作りに行くような処理や定数を用意する処理がほとんどなく、使えそうなのが、以下のLambda拡張機能を利用したシークレットの取得部分でした。
しかし、どうしてもINITフェーズがタイムアウト(10秒)してしまい、実装できませんでした…
プロビジョニングされた同時実行を用いるとタイムアウト時間を延ばすことができるので、試してみましたが、失敗してしまいました。
以下のシーケンス図を見るに、静的初期化を行うFunction InitフェーズはExtension Initフェーズの後に実行されるのかなと思いましたが、
実際のログを見るとFunction Initフェーズでパラメータ取得を試行しているループ内で、Extenstion Initが完了しているように見えます。
このあたりは時間が取れるタイミングで別記事で検証してみようと思います。
詳しい方いらっしゃればコメントで教えてください。
Part2はここまでで以上となります。
6. 改善結果
コールドスタート時から約80%、ウォームアップ後では約30%の改善に成功しました。
(以下「4.[part2] メモリとCPUのチューニングによってパフォーマンス向上」の再掲です。)
コールドスタート | ウォームアップ後 | |
---|---|---|
改善前 | 8.07秒 | 1.27秒 |
改善後 | 6.21秒 | 0.87秒 |
7. 終わりに
今回はOperating Lambdaに沿って、私的なアプリケーションのパフォーマンスチューニングを行いました。
今までのキャリアは低レイヤ中心のインフラエンジニアですが、そんな私でも簡単にパフォーマンスチューニングができたので、Operating Lambdaはかなりの良記事かと思います。
Lambdaを扱う方はぜひご一読いただければと思います!
Discussion