我注意到 .NET IHttpAsyncHandler(和 IHttpHandler,在较小程度上)在受到并发 Web 请求时会泄漏内存。
在我的测试中,Visual Studio Web 服务器 (Cassini) 的内存从 6MB 跃升至 100MB 以上,并且一旦测试完成,就没有一个内存被回收。
问题很容易重现。使用两个项目创建一个新的解决方案 (LeakyHandler):
- 一个 ASP.NET Web 应用程序 (LeakyHandler.WebApp)
- 控制台应用程序 (LeakyHandler.ConsoleApp)
在 LeakyHandler.WebApp 中:
- 创建一个实现 IHttpAsyncHandler 的名为 TestHandler 的类。
- 在请求处理中,做一个简短的 Sleep 并结束响应。
- 将 HTTP 处理程序作为 test.ashx 添加到 Web.config。
在 LeakyHandler.ConsoleApp 中:
- 生成大量 HttpWebRequest 到 test.ashx 并异步执行。
随着 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
,它也更加微妙。该示例通过使问题更加明显而加剧了问题,但基本原理是相同的。