我注意到.NET IHttpAsyncHandler(以及IHttpHandler,在较小程度上)在受到并发Web请求时会泄漏内存。
在我的测试中,Visual Studio Web服务器(Cassini)从6MB内存跳到100MB以上,测试完成后,没有一个被回收。
问题可以轻松复制。创建一个包含两个项目的新解决方案(LeakyHandler):
在LeakyHandler.WebApp中:
在LeakyHandler.ConsoleApp:
随着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&gt; 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)
中的ProcessRequest
,问题仍然存在,它只是更加微妙。这个例子使问题变得更加明显,从而加剧了问题,但基本原理是相同的。
答案 0 :(得分:13)
我已经查看了你的代码(然后运行它),我不相信你看到的内存增加实际上是内存泄漏。
您遇到的问题是您的调用代码(控制台应用程序)基本上是在紧密循环中运行。
但是,您的处理程序必须处理每个请求,并且Thread.Sleep(10)
还会“唠叨”。这样做的实际结果是你的处理程序无法跟上进来的请求,所以它的“工作集”随着更多请求排队等待处理而增长和增长。
我拿了你的代码,并在控制台应用程序中添加了一个AutoResetEvent,执行了
在.WaitOne()
之后 request.BeginGetResponse(GetResponseCallback, request);
和
在.Set()
之后 streamReader.ReadToEnd();
这具有同步呼叫的效果,因此在第一次呼叫回叫(和完成)之前不能进行下一次呼叫。你看到的行为消失了。
总之,我认为这纯粹是失控的情况,实际上并不是内存泄漏。
注意:我在GetResponseCallback方法中使用以下内容监视内存:
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine(GC.GetTotalMemory(true));
[编辑以响应Anton的评论] 我并不是说这里没有问题。 如果你的使用场景使得处理程序的这种锤击是真正的使用场景,那么显然你有一个问题。我的观点是,它不是一个记忆泄漏问题,而是容量问题。解决这个问题的方法可能是编写一个可以更快运行的处理程序,或者扩展到多个服务器等等。
泄漏是在资源完成后保留资源,增加工作集的大小。这些资源尚未“完成”,它们处于队列中并等待服务。一旦完成,我相信它们正在被正确发布。
[编辑以回应Anton的进一步评论] 好的 - 我发现了一些东西!我认为这是一个在IIS下不会发生的Cassini问题。您是否在Cassini(Visual Studio开发Web服务器)下运行处理程序?
当我在Cassini下运行时,我也看到了这些泄漏的System.Runtime.Remoting命名空间实例。如果我将处理程序设置为在IIS下运行,我看不到它们。你能确认一下你的情况吗?
这让我想起了我见过的其他远程/卡西尼号问题。 IIRC,具有类似IPrincipal的实例,需要存在于模块的BeginRequest中,并且在模块生命周期的末尾,需要从Cassini中的MarshalByRefObject派生而不是IIS。出于某种原因,似乎Cassini在内部进行了一些远程操作,而IIS则没有。
答案 1 :(得分:4)
您正在测量的内存可能会被分配但CLR未使用。要检查尝试呼叫:
GC.Collect();
context.Response.Write(GC.GetTotalMemory(true));
ProcessRequest()
中的。让您的控制台应用程序报告来自服务器的响应,以查看活动对象实际使用了多少内存。如果这个数字保持相当稳定,那么CLR只是忽略了做GC,因为它认为它有足够的RAM可用。如果这个数字稳步增加,那么你确实会遇到内存泄漏,你可能会使用WinDbg和SOS.dll或其他(商业)工具来解决这个问题。
编辑:好的,看起来你有一个真正的内存泄漏。下一步是弄清楚这些对象的内容。您可以使用SOS !gcroot 命令。 .NET 2.0 here有一个很好的解释,但是如果你可以在.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
答案 2 :(得分:1)
您的代码没有问题,并且没有内存泄漏。尝试连续几次运行您的控制台应用程序(或增加您的样本大小,但要注意,如果您有太多的并发请求正在休眠,最终您的http请求将被拒绝。)
使用您的代码,我发现当Web服务器内存使用量达到大约130MB时,就会有足够的压力让垃圾收集开始并将其减少到大约60MB。
也许这不是您期望的行为,但运行时已经决定快速响应您的快速传入请求比花费时间在GC上更重要。
答案 3 :(得分:-1)
您的代码很可能会出现问题。
我要检查的第一件事是你是否在代码中分离了所有的事件处理程序。每个+ =应该由 - =事件处理程序镜像。这就是大多数.Net应用程序创建内存泄漏的方式。