KAKEHASHI Tech Blog

カケハシのEngineer Teamによるブログです。

Lambdaを助けるのに理由がいるかい?(スロットリングの話)

こちらの記事は カケハシ Advent Calendar 2022 の19日目の記事になります。

あっという間に2022年も終わりますね⛄️ プラットフォームチームの石黒です。

今年は遅ればせながらFF9をプレイしまして、トロフィーをゲットするためにフィールド上でモーグリのモグオをたてぶえで呼びつけ、「なんでもない」を繰り返して怒られてしまったときに、ふとLambdaのことを思い出しました。

AWS Lambdaは拡張性に優れたコンピューティングサービスですが、モグオと同じく呼び出し回数の制約があります(モグオには怒られるだけですが…)。

今回はLambdaの呼び出し回数にフォーカスして、スロットリングの発生とその回避策について触れていきます。

Lambda同時実行数とスロットリング

Lambdaの呼び出し回数の制約は同時実行数と呼ばれ、リージョン単位で割り当てられています。初期値は1,000ですが申請することで引き上げ可能です。
同じタイミングで上限の1,000リクエスト発生するか、リクエストに応じたLambdaのスケールが間に合わない場合にスロットリングが発生します(詳しくは公式ドキュメントを参照ください)。
スロットリングにより実行されなかったLambdaはエラーレスポンスを返します。APIGatewayから呼び出されている場合は再試行せずそのままエラーになりますが、呼び出し元でリトライ処理が実装されている場合は、さらに実行数が跳ね上がることもあります。こういった事情からも、リトライはエクスポネンシャルバックオフを採用しておくと安心です。

同時実行数は予約できる

さて、この同時実行数はリージョン内の全関数で共有しているため、Lambda関数の数が増えるとスロットリングのリスクも上がります。
上限緩和の申請も回避策のひとつですが、Lambda関数ごとに予約済み同時実行数を設定することができます。
これはリージョン内で定められた同時実行数から指定した関数の分を個別に確保しておけるという制度で、例えば下図のように設定されているとき、とある関数の呼び出しが増えて「予約されていない同時実行数」が枯渇しても、確保しておいた予約済み同時実行数にゆとりがあれば関数A・関数Bは確実に実行させることができます。

同時実行数

予約する数を決めよう

エイヤッ!で決めてしまうワイルドな方法もありますが、一応計算方法が案内されています。

平均ランタイム(秒) * 呼び出し回数/秒

例えば、Durationが平均10秒程度、毎秒100回呼び出されることのある関数の場合、同時実行数は10*100=1,000回ほど確保しておく必要がある一方、Durationが平均0.5秒程度、毎秒100回呼び出される関数の場合、同時実行数は0.5*100=50回あれば充分です。
注意したいのが、 予約した実行数を超過してしまった場合、全体の実行回数が余っていてもその関数はスロットリングしてしまう ということです。

具体例を見てみましょう。
以下は、開発環境でスロットリングが発生していたときのサンプル関数Aのメトリクスの推移です。
トラブルシューティングに基づき、Lambda関数全体の ConcurrentExecutions の最大値と全体の Throttles の合計値、そして関数Aの ConcurrentExecutions の最大値と Throttles の合計値を比較しています。
なお、この例のリージョン全体で共有している予約されていない同時実行数は約2,800あり、関数Aの予約済み同時実行数は300に設定されています。

最初の図では関数Aは292回呼び出されていることがわかります。この段階で、予約した同時実行数に達していないのでスロットリングは発生していません。
次の図では関数Aの同時実行数が300回に達し、スロットリングが146件発生してしまいました。ただし、全体の呼び出し回数は1,546回のため、こちらはスロットリングは発生していません。
つまり、関数Aの予約枠のみ溢れている状態です。

メトリクス1

メトリクス2

このように、確保しておいたからといってスロットリングが絶対に発生しないというわけではありません。
ある程度運用して実行数が読めている関数であれば上記の計算に従って予約済み同時実行数を決められますが、これから運用開始する関数の場合は多めに枠を確保しておくか、最初は予約せず様子を見るのもアリだと思います。世の中甘くないですね。。

予約済み同時実行数をチューニングしよう

それでは、この関数Aの予約済み同時実行数を再計算してみましょう。
上図メトリクスから、スロットリングが発生したときの関数AのDurationはおよそ0.8秒(800ミリ秒)で推移しており、同時実行数が300とスロットリングしてしまった分が146あるので、0.8*(300+146)=356.8回と導出できます。

この結果から、予約済み同時実行数を360〜400程度に引き上げてもいいかもしれませんね。
実際に設定する場合は、1回のスロットリングの情報だけでなく、もう少し長い期間のDurationや呼び出し回数の情報を見るとより精度が上がりそうです。

以下は、先程と同じ関数の、過去1ヶ月のDurationの最小値・平均値・最大値のメトリクスです。Durationは平均0.9秒と、スロットリングが発生したときより長めの値となっており、最大値は約2秒にもなります。
同時実行数はそのままで、Durationだけ最大値に寄せても2.0*(300+146)=892回と想定数が大きく跳ね上がってしまいました。平均値の場合は0.9*(300+146)=401.4回となります。
Durationの最大値のメトリクスを見ると、まれに大きなリクエストが発生しており、それ以外は大きな値の変動はなさそうです。例えば予約済み同時実行数を900回まで引き上げると、スロットリングの発生はほぼなくなるかもしれませんが、ほとんどの期間で実行数の枠を過剰に確保することとなります。
今回は、予約済み同時実行数を300回から400回に上げることにしました。そうなると時々発生する大きなリクエストに耐えられずスロットリングが発生することが考えられるので、必ず実行したい非同期処理であれば、デッドレターキューを設定してリクエストが落ち着いてから再試行できるような仕組みにしておくとデータを保護することができます。

Duration

繰り返しになりますが、予約済み同時実行数を設定すると、予約されていない同時実行数(リージョンで共有している実行数)が設定した分だけ減ります。
リージョン全体で使える数には限りがあるので、欲張りすぎず、必要な分だけ設定するのが平和です。

予約済み同時実行数を設定する

実際に予約済み同時実行数を設定してみましょう。
マネジメントコンソールでは、Lambda関数の詳細ページの「設定」タブ > 同時実行の「編集」から設定します。

マネジメントコンソール

我々のチームではCDKを使ってリソース管理をしているのですが、CDKの場合はreservedConcurrentExecutionsオプションで設定可能です。

スロットリングのモニタリングも忘れずに!

残念ながら、予約済み同時実行数を設定しても前述の通りスロットリングが発生してしまうこともあります。発生してしまったときのことも考えておくとより安心して眠れそうです🛌
ここは自分がハマった点なのですが、APIGatewayと統合されているLambdaでスロットリングが発生したとき、APIGatewayは429エラーでなく500エラーを返します。
Lambda関数の実行がされないためログを用いた調査が行えず、通常の内部エラーより原因特定の難易度が上がります🥵
そこで、スロットリングの発生を検知できるようにしておくと、「短時間でLambdaが大量に呼び出され、実行されなかったリクエストがある」ことがすぐに把握できます。
スロットリングの検知は、メトリクスの「Throttles」の値の変化を見ることで実現可能です。CloudWatchアラームやDatadogなどのツールでモニタリングしておきましょう👍

まとめ

  • Lambdaには同時実行数の制約があり、超過するとスロットリングが発生する
  • 関数単位で同時実行数の予約をすることで対策可能
  • しかし、スロットリングするときはスロットリングする
  • スロットリングはモニタリングしておく

トラブルシューティングの記事でも述べられているように、スロットリングの対策として呼び出し側でのエクスポネンシャルバックオフやデッドレターキューによるスロットリングリクエストの捕捉も重要な施策です。
上限緩和申請や予約済み同時実行数の確保を行う前に、これらがきちんと行えているかも確認したいですね☝️
もちろん、モグオにしたように用もないのに呼ぶのは厳禁です!笑

Lambda関数は最初に作成したときから設定を見直していない😅なんて方も多いんじゃないでしょうか?
タイムアウトの設定やメモリの調整も関数ごとに行えるので、併せて確認したいところです。
みなさんもLambdaの設定を見直して、年末年始は心置きなくゲームを楽しみましょう🌅