.NET IHttpAsyncHandler(および程度は低いですがIHttpHandler)が、同時Web要求にさらされると、メモリをリークすることに気づきました。
私のテストでは、Visual Studio Webサーバー(Cassini)が6MBのメモリから100MBを超えるメモリにジャンプし、テストが終了すると、再利用されることはありません。
問題は簡単に再現できます。2つのプロジェクトで新しいソリューション(LeakyHandler)を作成します。
- ASP.NET Webアプリケーション(LeakyHandler.WebApp)
- コンソールアプリケーション(LeakyHandler.ConsoleApp)
LeakyHandler.WebAppの場合:
- IHttpAsyncHandlerを実装するTestHandlerというクラスを作成します。
- リクエスト処理で、短いスリープを実行し、応答を終了します。
- HTTPハンドラーをtest.ashxとしてWeb.configに追加します。
LeakyHandler.ConsoleAppの場合:
- test.ashxへの多数のHttpWebRequestを生成し、それらを非同期で実行します。
HttpWebRequests(sampleSize)の数が増えると、メモリリークがますます明らかになります。
LeakyHandler.WebApp> TestHandler.cs
namespace LeakyHandler.WebApp
{
public class TestHandler : IHttpAsyncHandler
{
#region IHttpAsyncHandler Members
private ProcessRequestDelegate Delegate { get; set; }
public delegate void ProcessRequestDelegate(HttpContext context);
public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData)
{
Delegate = ProcessRequest;
return Delegate.BeginInvoke(context, cb, extraData);
}
public void EndProcessRequest(IAsyncResult result)
{
Delegate.EndInvoke(result);
}
#endregion
#region IHttpHandler Members
public bool IsReusable
{
get { return true; }
}
public void ProcessRequest(HttpContext context)
{
Thread.Sleep(10);
context.Response.End();
}
#endregion
}
}
LeakyHandler.WebApp> Web.config
<?xml version="1.0"?>
<configuration>
<system.web>
<compilation debug="false" />
<httpHandlers>
<add verb="POST" path="test.ashx" type="LeakyHandler.WebApp.TestHandler" />
</httpHandlers>
</system.web>
</configuration>
LeakyHandler.ConsoleApp> Program.cs
namespace LeakyHandler.ConsoleApp
{
class Program
{
private static int sampleSize = 10000;
private static int startedCount = 0;
private static int completedCount = 0;
static void Main(string[] args)
{
Console.WriteLine("Press any key to start.");
Console.ReadKey();
string url = "http://localhost:3000/test.ashx";
for (int i = 0; i < sampleSize; i++)
{
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
request.Method = "POST";
request.BeginGetResponse(GetResponseCallback, request);
Console.WriteLine("S: " + Interlocked.Increment(ref startedCount));
}
Console.ReadKey();
}
static void GetResponseCallback(IAsyncResult result)
{
HttpWebRequest request = (HttpWebRequest)result.AsyncState;
HttpWebResponse response = (HttpWebResponse)request.EndGetResponse(result);
try
{
using (Stream stream = response.GetResponseStream())
{
using (StreamReader streamReader = new StreamReader(stream))
{
streamReader.ReadToEnd();
System.Console.WriteLine("C: " + Interlocked.Increment(ref completedCount));
}
}
response.Close();
}
catch (Exception ex)
{
System.Console.WriteLine("Error processing response: " + ex.Message);
}
}
}
}
デバッグの更新
WinDbgを使用してダンプファイルを調べましたが、いくつかの疑わしいタイプがメモリに保持されており、解放されていません。10,000のサンプルサイズでテストを実行するたびに、これらのオブジェクトがさらに10,000個メモリに保持されることになります。
System.Runtime.Remoting.ServerIdentity
System.Runtime.Remoting.ObjRef
Microsoft.VisualStudio.WebHost.Connection
System.Runtime.Remoting.Messaging.StackBuilderSink
System.Runtime.Remoting.ChannelInfo
System.Runtime.Remoting.Messaging.ServerObjectTerminatorSink
これらのオブジェクトは第2世代のヒープにあり、強制的に完全なガベージコレクションを実行した後でも収集されません。
重要な注意点
この問題は、シーケンシャルリクエストを強制する場合でも存在し、Thread.Sleep(10)
inがなくてもProcessRequest
、はるかに微妙です。この例は、問題をより簡単に明らかにすることで問題を悪化させますが、基本は同じです。