はじめに
実は本日開催のイベントでProvisioned Concurrencyという機能について話す予定です。
serverless-newworld.connpass.com
このイベントは省エネで特定のトピックについて簡単に話すというのをモットーとしていまして、普段の各種イベントでの講演のようにプレゼンテーション用のスライドを作ったりはしないつもりだったんです。と、言いつつも前回は過去に書いたブログ記事を使って話をしたんです。でも今回はそれすらない。
最初はそれでもいいかと思っていたんですが、口頭だけだとなかなか難しそうだなと思えてきたので4時間後のイベントのためにブログをしたためています。
大前提
AWS Lambdaにはコールドスタートというものがあるのは多くの人が知っていると思います。以下はこのあたりに関する前回の記事のコピペです。
リクエストが来たらそれを処理するLambdaファンクションのコンテナを作成し、処理を実行します。
このLambdaファンクションのコンテナは1つあたり同時に1リクエスト(1イベント)しか処理しません。従って同時に100リクエスト来た場合は同じLambdaファンクションのコンテナ100個で処理されます。ただし、必ずしも毎回100コンテナが1から作成されて起動されるわけではありません。
Lambdaファンクションのコンテナは起動コストの効率化のために再利用も行います。いわゆるウォームスタートというものですね。ウォームスタート可能なコンテナがあれば新しくコンテナを作ることはせずにそれを利用します。しかし、ウォームスタートできるコンテナがない場合やあってもその数以上のリクエストを同時に処理する必要が発生した場合は新しくコンテナが作成・起動されます。これがデフォルトの同時実行1000だと1,000コンテナが同時に実行される可能性があるわけです。もちろん10,000であれば10,000個です。
はい、なんとなく理解しましたか?
そんなコールドスタートをみんななるだけ避けたいと思ってあの手この手で対応をしようとしています。とはいえ、コールドスタートはゼロにはできません。なので、本来はコールドスタートをいかに短くするかっていうのが基本ではありますが、今回はその話はしません。
そんなコールドスタートを避けるために古来から空実行と言うか、本来のリクエストではないリクエストを事前に送っておくなどして来たるべきに備えて先にコールドスタートを発生させておくことで、本来必要なリクエストを処理する際には起動済のコンテナが再利用されることを狙うってことをやっている人たちもいました。
これ自体は実のところあまり意味があるものではありません。というのも結局のところ、事前にリクエストをしたところでそれでカバーできるのはそのリクエスト数分だけなのです。
つまり何分かに1回実行するだけでは維持されるコンテナは1つだけです。そこに2以上の同時アクセスが来たらあぶれた分はコールドスタートが発生します。Pre Warmingのためのリクエスト数を増やせばいいのですが、そうすると本番で必要となる同時実行数分をリクエストし続けておく必要があり、サーバーレスのメリットでもあるNever Pay for Idleという特性が失われます。
また、いくらこれをやってもLambdaファンクションのデプロイをしたり、設定変更をするとコンテナの作り直しになるため、全体的にコールドスタートが発生してしまうことは防ぎようがありません。
トラフィックバースト
さて、先ほどの説明で同時実行という言葉が出てきました。これはデフォルトでは1,000に制限されています。デフォルトでは、と言ったのはこれは必要に応じて緩和申請をすることができるからです。とはいえ無尽蔵に、無制限に好きな値で申請できるわけではもちろんないです。必要なサイズを申請することになり、それが許可されればそのアカウントについては同時実行数が増えることになります。
さて、とあるLambdaファンクションに対する実行リクエストが大量に発生したときにどうなるかなんですが、この許可された同時実行数までは特に制限なく実行されると思っている人も多いですが、これは勘違いです。
Lambdaには同時実行のバーストについても制限があり、東京リージョンの場合は1,000となっています。これは先ほどの同時実行とは異なり緩和不可な制限となっています。
ではこのバーストの制限とはどういうものか。
平たく言うと同時実行数が1,001以上に許可されていた状態であっても、大量のリクエストが同時に発生した場合一気にそこまで増えるのではなく、このバースト制限までしか最初の段階ではコンテナの数は増えないということです。
例えば許可された同時実行数が30,000だとして、全然リクエストが来ていない状態(つまり、起動された状態で維持されたコンテナが0の状態)とします。この状態で10,000リクエストが同時に来た場合、この10,000リクエストを処理する分のコンテナが一気にコールドスタートするわけではないです。あくまでも最初の段階では1,000リクエスト分までで、その後もリクエストが続いている場合は1分ごとに500コンテナずつ起動されていきます。
これは、すべてのリクエストを処理するコンテナが起動しきるまで続きます。その前に許可された同時実行数に達した場合はその時点でこの動きは止まります。入ってくるリクエストにこのスケールのスピードが追いつかない場合はスロットリングされます。もちろん同時実行数に達した場合も同様です。
スパイクが発生するときにこのあたりの仕様・特性を知らないと悩むことになります。また、知っていても悩むことになります。スパイク時にコールドスタートが起きるのを減らしたいはもちろんなんですが、そもそもコールドスタートが起きることもない状態になってしまう場合もあるからですね。
Provisioned Concurrency
簡単に言うとPre Warming as a Serviceです。要はコールドスタートが起きないように一定量を事前に温めて置くことができようになったってことです。
とはいえ事前にプロビジョニングしたとしても、設定してすぐに効果を発揮するわけではなくて徐々に準備されていきます。このときも先ほどのトラフィックバーストと同様の挙動です。つまり、東京リージョンであれば一度に最大1,000をコールドスタートして初期化した後、1分ごとに500ずつ設定された値までプロビジョニングされていく感じです。
あと大事なこととしてお金かかります。
何が解決するのか
予測されるスパイクに対してはあらかじめWarm状態で待ち受けられるのでコールドスタートによるレイテンシ増を減らせます。予測されるスパイクに対してはってのがポイントです。結局のところ、Provisioned Concurrencyを設定していても、それを上回るリクエストが来てしまうと同じです。この場合、上回ったリクエストに関しては前述のバーストの制限の影響も受けます。
あとはスパイクはそれほど発生しないけれどもコールドスタートが重い処理なんかのために使うのもいいかもしれません。
設定のコツ
事前にプロビジョニングしたいLambdaファンクションがいくつあるかによって変わってきます。Lambdaの同時実行の制限自体はアカウント全体で共有されます。なので、各Lambdaファンクションでどのくらいのスループットを出したいかによって変わってきます。
デフォルトの同時実行1000の状況だとProvisioned Concurrencyとして設定できるのは900までです。設定したいLambdaファンクションが1つの場合はそれに全振りすればいいのですが、例えば2種類ある場合は、各ファンクションで処理したいスループット、例えばxx rpsとかと各関数の実行時間を考慮して決めていきます。
例えばファンクションがAとBの2つあって、それぞれ実行時間が500msと1000msだった場合に両方とお600rps処理するにはAに300、Bに600プロビジョニングすることになります。
もう少し詳しく説明すると、デフォルトの場合に設定可能な値は合計900までという話をしました。これをファンクションA、Bにどうやって割り振っていくかです。Aは500ms/リクエストなので要件の600rpsを処理可能にしようとすると1リクエストが500ms = 0.5秒なので1つのコンテナでは秒間で2リクエスト処理できます。したがって600rps、つまり秒間600リクエストを処理するには600/2=300必要ということになります。同様の計算でBは600をプロビジョニングすることになります。
ポイントは同時実行の制限を共有するということです。
まとめ
Provisioned Concurrencyは銀の弾丸ではないけど金の弾丸ではあるかも知れない。つまりお金で解決する手段ってことですね。前述のとおり、Provisioned Concurrencyは無料で設定可能な機能ではないので。
必要に応じてコールドスタートの時間自体を短くするように基本のチューニングを施す、アーキテクチャを見直すなども当然ながら必要ということですね。