为什么 IHttpAsyncHandler 在负载下会泄漏内存?
我注意到 .NET IHttpAsyncHandler(以及较小程度的 IHttpHandler)在遭受并发 Web 请求时会泄漏内存。
在我的测试中,Visual Studio Web 服务器(Cassini)从 6MB 内存跃升至超过 100MB,并且一旦测试完成,就不会回收任何内存。
该问题可以很容易地重现。创建一个包含两个项目的新解决方案 (LeakyHandler):
- 一个 ASP.NET Web 应用程序 (LeakyHandler.WebApp)
- 一个控制台应用程序 (LeakyHandler.ConsoleApp)
在 LeakyHandler.WebApp 中:
- 创建一个名为 TestHandler 的类,该类实现 IHttpAsyncHandler。
- 在请求处理中,做短暂的Sleep并结束响应。
- 将 HTTP 处理程序添加到 Web.config 作为 test.ashx。
在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 代堆中,不会被收集,即使在强制完全垃圾回收之后。
重要说明
即使强制顺序请求,甚至在 ProcessRequest
中没有 Thread.Sleep(10)
,问题也存在,只是更加微妙。该示例使问题变得更加明显,从而加剧了问题,但基本原理是相同的。
I have noticed that the .NET IHttpAsyncHandler (and the IHttpHandler, to a lesser degree) leak memory when subjected to concurrent web requests.
In my tests, the Visual Studio web server (Cassini) jumps from 6MB memory to over 100MB, and once the test is finished, none of it is reclaimed.
The problem can be reproduced easily. Create a new solution (LeakyHandler) with two projects:
- An ASP.NET web application (LeakyHandler.WebApp)
- A Console application (LeakyHandler.ConsoleApp)
In LeakyHandler.WebApp:
- Create a class called TestHandler that implements IHttpAsyncHandler.
- In the request processing, do a brief Sleep and end the response.
- Add the HTTP handler to Web.config as test.ashx.
In LeakyHandler.ConsoleApp:
- Generate a large number of HttpWebRequests to test.ashx and execute them asynchronously.
As the number of HttpWebRequests (sampleSize) is increased, the memory leak is made more and more apparent.
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);
}
}
}
}
Debugging Update
I used WinDbg to look into the dump files, and a few suspicious types are being held in memory and never released. Each time I run a test with a sample size of 10,000, I end up with 10,000 more of these objects being held in memory.
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
These objects lie in the Generation 2 heap and are not collected, even after a forced full garbage collection.
Important Note
The problem exists even when forcing sequential requests and even without the Thread.Sleep(10)
in ProcessRequest
, it's just a lot more subtle. The example exacerbates the problem by making it more readily apparent, but the fundamentals are the same.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(4)
我查看了您的代码(并运行了它),我不相信您看到的内存增加实际上是内存泄漏。
您遇到的问题是您的调用代码(控制台应用程序)本质上是在紧密循环中运行。
但是,您的处理程序必须处理每个请求,并且还受到
Thread.Sleep(10)
的“束缚”。这样做的实际结果是,您的处理程序无法跟上传入的请求,因此它的“工作集”随着更多请求排队等待处理而不断增长。我获取了您的代码并将 AutoResetEvent 添加到控制台应用程序,
在
request.BeginGetResponse(GetResponseCallback, request);
和
.Set 之后执行
after.WaitOne()
()streamReader.ReadToEnd();
这具有同步调用的效果,因此在第一个调用回调(并完成)之前无法进行下一个调用。您所看到的行为就会消失。
综上所述,我认为这纯粹是一种失控情况,实际上根本不是内存泄漏。
注意:我在 GetResponseCallback 方法中使用以下内容监视内存:
[编辑以响应 Anton 的评论]
我并不是说这里完全没有问题。
如果您的使用场景是处理程序的这种锤击是真实的使用场景,那么显然您遇到了问题。我的观点是,这不是内存泄漏问题,而是容量问题。解决这个问题的方法可能是编写一个运行速度更快的处理程序,或者扩展到多个服务器等。
泄漏是指资源在使用完毕后仍被保留,从而增加了工作集。这些资源尚未“完成”,它们处于队列中并等待服务。一旦它们完成,我相信它们就会被正确释放。
[编辑以回应安东的进一步评论]
好吧——我发现了一些东西!我认为这是 Cassini 问题,在 IIS 下不会发生。您是否在 Cassini(Visual Studio 开发 Web 服务器)下运行处理程序?
当我仅在 Cassini 下运行时,我也看到了这些泄漏的 System.Runtime.Remoting 命名空间实例。如果我将处理程序设置为在 IIS 下运行,我就看不到它们。您能否确认您是否属于这种情况?
这让我想起了我见过的其他一些远程/卡西尼问题。 IIRC 具有类似 IPrincipal 的实例,需要存在于模块的 BeginRequest 中,并且在模块生命周期结束时也需要从 Cassini 中的 MarshalByRefObject 派生,而不是 IIS。由于某种原因,Cassini 似乎在内部进行了一些远程处理,而 IIS 则没有。
I've had a look at your code (and run it) and I don't believe the increasing memory you are seeing is actually a memory leak.
The problem you've got is that your calling code (the console app) is essentially running in a tight loop.
However, your handler has to process each request, and is additionally being "nobbled" by the
Thread.Sleep(10)
. The practical upshot of this is that your handler can't keep up with the requests coming in, so its "working set" grows and grows as more requests queue up, waiting to be processed.I took your code and added an AutoResetEvent to the console app, doing a
.WaitOne()
afterrequest.BeginGetResponse(GetResponseCallback, request);
and a
.Set()
afterstreamReader.ReadToEnd();
This has the effect of synchronising the calls so the next call can't be made until the first call has called back (and completed). The behaviour you are seeing goes away.
In summary, I think this is purely an runaway situation and not actually a memory leak at all.
Note: I monitored the memory with the following, in the GetResponseCallback method:
[Edit in response to comment from Anton]
I'm not suggesting there is no problem here at all.
If your usage scenario is such that this hammering of the handler is a real usage scenario, then clearly you have an issue. My point is that it is not a memeory leak issue, but a capacity issue. The way to approach solving this would be, maybe, to write a handler that could run faster, or to scale out to multiple servers, etc, etc.
A leak is when resources are held onto after they are finished with, increasing the size of the working set. These resources have not been "finished with", they are in a queue and waiting to be serviced. Once they are complete I believe they are being released correctly.
[Edit in response to Anton's further comments]
OK - I've uncovered something! I think this is a Cassini issue that doesn't occur under IIS. Are you running your handler under Cassini (The Visual Studio Development Web Server)?
I too see these leaky System.Runtime.Remoting namespace instances when I am running under Cassini only. I do not see them if I set the handler up to run under IIS. Can you confirm if this is the case for you?
This reminds me of some other remoting/Cassini issue I've seen. IIRC, having an instance of something like an IPrincipal that needs to exist in the BeginRequest of a module, and also at the end of the module lifecycle, needs to derive from MarshalByRefObject in Cassini but not IIS. For some reason it seems Cassini is doing some remoting internally that IIS isn't.
您正在测量的内存可能已被 CLR 分配但未使用。要检查,请尝试调用:
在
ProcessRequest()
中。让您的控制台应用程序报告来自服务器的响应,以查看活动对象实际使用了多少内存。如果这个数字保持相当稳定,那么 CLR 只是忽略执行 GC,因为它认为它有足够的可用 RAM。如果该数字稳步增加,那么您确实存在内存泄漏,您可以使用 WinDbg 和 SOS.dll 或其他(商业)工具来解决该问题。编辑:好的,看起来你确实有内存泄漏。下一步是弄清楚是什么支撑着这些物体。您可以使用 SOS !gcroot 命令来实现此目的。 .NET 2.0 有一个很好的解释 此处,但如果您可以在 .NET 4.0 上重现此内容,那么它的 SOS.dll 有更好的工具为您提供帮助 - 请参阅http://blogs.msdn.com/tess/archive/2010/03/01/new-commands-in-sos-for-net-4-0-part-1.aspx
The memory you are measuring may be allocated but unused by the CLR. To check try calling:
in
ProcessRequest()
. Have your console app report that response from the server to see how much memory is really used by live objects. If this number remains fairly stable then the CLR is just neglecting to do a GC because it thinks it has enough RAM available as it is. If that number is increasing steadily then you really do have a memory leak, which you could troublshoot with WinDbg and SOS.dll or other (commercial) tools.Edit: OK, looks like you have a real memory leak then. The next step is to figure out what's holding onto those objects. You can use the SOS !gcroot command for that. There is a good explanation for .NET 2.0 here, but if you can reproduce this on .NET 4.0 then its SOS.dll has much better tools to help you - see http://blogs.msdn.com/tess/archive/2010/03/01/new-commands-in-sos-for-net-4-0-part-1.aspx
你的代码没有问题,也没有内存泄漏。尝试连续运行控制台应用程序几次(或增加样本大小,但请注意,如果您有太多并发请求处于睡眠状态,最终您的 http 请求将被拒绝)。
通过您的代码,我发现当 Web 服务器内存使用量达到大约 130MB 时,垃圾收集就会产生足够的压力,并将其减少到大约 60MB。
也许这不是您期望的行为,但是运行时已经决定快速响应快速传入的请求比花时间处理 GC 更重要。
There is no problem with your code, and there is no memory leak. Try running your console application a couple times in a row (or increase your sample size, but beware that eventually your http requests will be rejected if you have too many concurrent requests sleeping).
With your code, I found that when the web server memory usage reached about 130MB, there was enough pressure that garbage collection kicked in and reduced it to about 60MB.
Maybe it's not the behavior you expect, but the runtime has decided it is more important to quickly respond to your rapid incoming requests than to spend time with the GC.
这很可能是您的代码中的问题。
我要检查的第一件事是您是否分离了代码中的所有事件处理程序。每个 += 都应该由事件处理程序的 -= 镜像。这就是大多数 .Net 应用程序造成内存泄漏的方式。
It will be very likely a problem in your code.
The first thing I would check is if you detach all the event-handlers in your code. Every += should be mirrored by a -= of an event handler. This is how most .Net application create a memory leak.