为什么IHttpAsyncHandler在负载下泄漏内存?

时间:2010-05-12 22:58:41

标签: .net memory-leaks ihttphandler ihttpasynchandler

我注意到.NET IHttpAsyncHandler(以及IHttpHandler,在较小程度上)在受到并发Web请求时会泄漏内存。

在我的测试中,Visual Studio Web服务器(Cassini)从6MB内存跳到100MB以上,测试完成后,没有一个被回收。

问题可以轻松复制。创建一个包含两个项目的新解决方案(LeakyHandler):

  1. ASP.NET Web应用程序(LeakyHandler.WebApp)
  2. 控制台应用程序(LeakyHandler.ConsoleApp)
  3. 在LeakyHandler.WebApp中:

    1. 创建一个名为TestHandler的类,它实现了IHttpAsyncHandler。
    2. 在请求处理中,进行简短的休眠并结束响应。
    3. 将HTTP处理程序添加到Web.config作为test.ashx。
    4. 在LeakyHandler.ConsoleApp:

      1. 为test.ashx生成大量HttpWebRequests并异步执行它们。
      2. 随着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,问题仍然存在,它只是更加微妙。这个例子使问题变得更加明显,从而加剧了问题,但基本原理是相同的。

4 个答案:

答案 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应用程序创建内存泄漏的方式。