先日、AWS で RDS Proxy が SQL Server をサポートしました。
RDS Proxy を使用せずに Lambda と RDS を組み合わせた場合には、
というような影響を受け、リクエストごとにコネクションを生成してしまうため RDS Proxy を介して接続をすることで接続を最適にすることができることは把握していたのですが、Azure Functions の HTTP Trigger を使用した場合には、どうなるのかを今まで気にしていなかったので、この機会に確認をしてみました。
しばやん先生からフィードバックいただいた、こちらの内容のとおりではありますが、実際に挙動を確認して把握しておこうかと。
Consumption Plan でも 200 までしかスケールしないので、Azure Functions では Connection Pool を使うようにしておけばまず問題にならない認識です(最近は Consumption Plan が必要な場面ではまず Cosmos DB しか使ってませんが
— Tatsuro Shibamura (@shibayan) September 20, 2022
検証方法
今回は、Lambda / Azure Functions から、次の処理を HTTP 経由のアクセスで実行しています。
$con = New-Object System.Data.SqlClient.SqlConnection("Server=xxxxx.database.windows.net;user=xxxxx;password=xxxxx;Database=DB01;Application Name=Pwsh") $con.Open() $cmd = $con.CreateCommand() $cmd.CommandText = "WAITFOR DELAY '00:00:00.500'" $cmd.ExecuteNonQuery() $con.Dispose()
Lambda / Azure Functions ともに、Serverless で作成した SQL Database に WAITFOR DELAY を実行するというシンプルな処理です。
これを「ab -n 1000 -c 600 https://~」で実行した際に、SQL Database 上の接続がどのようになるのかを、次のクエリを実行して確認します。
WITH Base AS( SELECT es.host_name, ec.client_net_address, count(*) AS cnt FROM sys.dm_exec_sessions AS es inner join sys.dm_exec_connections AS ec ON es.session_id = ec.session_id WHERE es.program_name = 'pwsh' GROUP BY es.host_name, ec.client_net_address ) SELECT *, COUNT(*) OVER() Row_Count, (SELECT SUM(cnt) FROM BASE) AS Total_Session FROM BASE
検証結果
Lambda で検証してみる
最初に RDS Proxy を介さずに Lambda が SQL Database にアクセスするとどうなるかを確認してみました。
実行結果としては次のようになりました。426 行の結果となっているため、最初のほうのみ結果を貼り付けています。
今回は、500 ミリ秒の WAIT が明示的に設定されている処理が実行されているのですが、、Lambda 関数のスケーリング に記載されている挙動により、処理要求を受け付けられるインスタンスが存在していない場合は、新しいインスタンスが起動され、今回の検証であれば、426 インスタンスが最終的に起動されています。(多重度が低ければインスタンスの再利用がされると思いますので、今回のケースは処理に時間がかかるものが、瞬間的に複数同時実行された場合の挙動になると考えています)
各インスタンスからは 1,2 セッションの少数の接続が行われているというような状態となっており、大量のインスタンスが並列で並んでいるため、コネクションプールが利用される可能性が低くなっています。
RDS Proxy を使用せず、Lambda から直接接続を使用して DB にアクセスをした場合には、応答の並列 (同時実行) 状況によっては、このような状態となる可能性があるということですね。
Azure Functions で検証してみる
それでは、従量課金の Azure Functions に対して実行するとどうなるでしょうか?
Azure Functions で実行した場合には次のような実行状況となりました。
Azure Functions のスケーリングについては、Azure Functions のホスティング オプション に記載されており、Windows の場合は最大で 200 インスタンスまでスケールされます。
Azure Functions の HTTP トリガーで実行した場合、一つのインスタンスで複数の要求を処理しようとするため、今回のケースであれば 10 台までインスタンスが作成され、各インスタンスの接続については、コネクションプールを使用して、接続の再利用が行われる形で処理が行われていました。
Azure Functions の HTTP Trigger は、1 インスタンスで複数の要求を同時に処理する傾向があるようですね。
- コネクションプールを使用しながら、1 インスタンスで同時に複数要求を処理
- 自動的なスケールが行われるので、各インスタンスの処理状況については、偏りが多少みられる
- スケールのトリガーを自分で設定できないということも言えるのかもしれませんね。
というような感じなのでしょうかね。
まとめ
同時実行要求が多い場合には、次のような特性がありそうという結果となりました。
- Lambda
- 要求ごと (要求を処理できるインスタンスが無い場合) にインスタンスを起動し、各インスタンスから少量の接続が行われている状態となる
- 同時実行要求が多い環境ではコネクションプールが利用される可能性が低い
- Azure Functions (従量課金)
- インスタンス数はスケールリミットの上限となるが、同時要求が発生しても要求ごとにインスタンスは起動せず、1 インスタンスで複数要求を処理するため、各インスタンスから複数接続が行われている状態となる
- 1 インスタンスで複数要求処理される傾向があるため、コネクションプールが利用される可能性が高い
- インスタンス数のスケールは自動制御のため、後から起動したインスタンスでは処理数に偏りが出そう
SQL Server 側の接続数の上限については、
- SQL Server: 32,767
- Azure SQL Database: 300~30,000
となり、標準的な環境であれば接続数の上限に達するという可能性は低いかと思います。(接続数 (セッション数) の上限の前に同時ワーカー数の上限に達するはずです)
しかし、同時実行を行った場合に、各実行基盤がどのように接続を行うかは把握しておくとよいのではないでしょうか。
Azure Functions で DB に接続して処理を行うものは、タイマートリガーで実装して、メトリクスとなる情報を取得するということはよくやっているのですが、HTTP Trigger で実装した場合の挙動は今まで意識したことが無かったので、良い機会だったので調べてみたというお話でした。
Serverless の実行環境の処理方法、各社で特性がありなかなか面白いですね。