解説:教えて那々子先生
リソースリークは、メモリーの解放忘れ、ファイルのクローズ忘れ、通信の接続切断忘れなどによって引き起こされます。これにより、メモリ不足、ファイルのアクセス拒否、最大同時接続数の超過などのリソース不足が引き起こされることがあります。
DB接続のリソースリークの例
例えば、古いWebアプリケーションで時々見かけるのが、以下のように、セッション変数にDB接続オブジェクトを保存するような脆弱なコードです。
// 略
public class Global : HttpApplication
{
//略
void Session_Start(object sender, EventArgs e)
{
var connectionStr = ConfigurationManager.ConnectionStrings["DefaultConnection"].ConnectionString;
var conn = new SqlConnection(connectionStr);
conn.Open();
Session["DB_CONN"] = conn;
}
//略
var conn = (SqlConnection)Session["DB_CONN"];
using (var cmd = conn.CreateCommand())
{
cmd.CommandText = "SELECT 1;";
var result = cmd.ExecuteScalar();
// 略
}
var conn = (SqlConnection)Session["DB_CONN"] ;
conn.Close();
conn.Dispose();
Session.Remove("DB_CONN");
ASP.NET Web Formsにて、セッション状態モードをInProcに設定されている場合を想定したコード。
これはパフォーマンスチューニングを意図した物であると考えられます。DBでクエリを実行する度にDBへの接続と切断を繰り返していれば、塵も積もれば……で大きなタイムロスが発生します。それを避けるため、セッションが有効な間はDBへの接続を繋ぎっぱなしにしておこうと考えたのでしょう。しかし、このようにDB接続オブジェクトをセッション変数に保存するのは誤った方法です。
確かにこの実装は、一見、意図通りに動作します。しかし、セッション数が、DBへの最大接続数を超えたときに不具合が顕在化します。
例えば、以下のような模擬攻撃コードをPowerShellで実行することを考えてみましょう。
$i = 101; while($i-- -gt 0) { Invoke-RestMethod -Uri https://localhost:xxxx/ ; Start-Sleep -s 1}
注意
- 実際に実験する場合は、自ら所有・管理するコンピューター上で、外部に通信が行なわれないよう措置を講じた上で実行すること
- xxxxは対象となるテスト用のWEBアプリケーションのポート番号で置き換えること
- 停止する場合はCtrl+Cキーで停止する
この模擬攻撃コードは、Invoce-RestMethodでhttps://localhost:xxxx/に対してGETリクエストを、1秒おきに合計101回送信するものです。
しばらくすると、接続上限(厳密には接続プール内の接続数の上限)に達して以下のような例外が発生します。

繋ぎっぱなしになったDB接続が積み重なり、接続数の上限に達します。そして、新たな接続を行えなくなり、エラーが発生するようになります。
なぜこのようなことが起きるのでしょうか。
それは、PowerShellのInvoke-RestMethodが、デフォルトではCookie情報を保持しないことが原因です。Webサーバー上では、セッションを識別するためにCookieを使用します。しかし、Cookieが維持されなければ、アクセスの都度、新たなセッションが作成されることとなり、そのたびにSession_Startイベントが発生します。先ほどのコードの例では、Session_Startイベント内でDB接続を開始し、セッション変数内に保持していましたから、どんどんDB接続が積み重なっていき、やがて上限に達するという結果となるのです。
もし悪意がなくとも、Invoke-RestMethod、curlコマンド、wgetコマンドなどを定期実行する場合や、クローラーが自動でページ内容を収集する場合、マンガのようにアプリが異常終了して、新たなセッションが開始してしまうような場合などにこのような状況が生じ得ます。
このように、Webサーバー側ではセッションの終了を必ずしも検知できないため、リソースリークが発生してしまうということになるのです。
一般的にフレームワーク側に接続プール(コネクションプーリング)の仕組みがあり、これを用いることで接続が再利用されるようになります。
先ほどの例のASP.NETの場合、接続文字列においてMin Pool Sizeを1以上に設定することで、予め1個の接続が行なわれ、接続プールに溜め置かれます。
<connectionStrings>
<add name="DefaultConnection" connectionString="Data Source=(LocalDb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\xxxxx.mdf;Initial Catalog=xxxxx;Integrated Security=True;Min Pool Size=10;Max Pool Size=100"
providerName="System.Data.SqlClient" />
</connectionStrings>
conn.Open();の際には、接続プールに溜め置かれた接続があれば割り当てられ、なければ新規接続が行われます。そして、conn.Close();の際に接続プールに接続が戻されます。再びconn.Open();が実行された時には、接続プールに溜め置かれた接続を再利用することになります。
そして、Min Pool Sizeに設定した接続数を超えた接続は、一定時間後に自動的に切断されます。
したがって、わざわざDB接続をセッション変数に保存するというのは、百害あって一利なしの余計なお節介ということになります。むしろ、重大な欠陥といっても過言ではないでしょう。
