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

Ant*_*ton 11 .net memory-leaks ihttpasynchandler ihttphandler

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

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

问题可以很容易地重现.使用两个项目创建一个新的解决方案(LeakyHandler):

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

在LeakyHandler.WebApp中:

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

在LeakyHandler.ConsoleApp中:

  1. 为test.ashx生成大量HttpWebRequests并异步执行它们.

随着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
    }
}
Run Code Online (Sandbox Code Playgroud)

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>
Run Code Online (Sandbox Code Playgroud)

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);
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

调试更新

我使用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,它也会更加微妙.这个例子通过使问题变得更加明显而加剧了问题,但基本原理是相同的.

Rob*_*ine 14

我已经看过你的代码(并运行它),我不相信你看到的增加的内存实际上是内存泄漏.

您遇到的问题是您的调用代码(控制台应用程序)基本上是在紧密循环中运行.

但是,你的处理程序必须处理每个请求,并且另外被"nobbled" Thread.Sleep(10).这样做的实际结果是你的处理程序无法跟上进来的请求,因此它的"工作集"随着更多请求排队等待处理而增长和增长.

我拿了你的代码,并在控制台应用程序中添加了一个AutoResetEvent

.WaitOne()request.BeginGetResponse(GetResponseCallback, request);

和a

.Set()streamReader.ReadToEnd();

这具有同步呼叫的效果,因此在第一次呼叫回叫(和完成)之前不能进行下一次呼叫.你看到的行为消失了.

总而言之,我认为这纯粹是失控的情况,实际上并不是内存泄漏.

注意:我在GetResponseCallback方法中使用以下内容监视内存:

 GC.Collect();
 GC.WaitForPendingFinalizers();
 Console.WriteLine(GC.GetTotalMemory(true));
Run Code Online (Sandbox Code Playgroud)

[编辑回应安东的评论] 我并不是说这里没有问题.如果你的使用场景使得处理程序的这种锤击是真正的使用场景,那么显然你有一个问题.我的观点是,它不是一个记忆泄漏问题,而是容量问题.解决这个问题的方法可能是编写一个可以更快运行的处理程序,或者扩展到多个服务器等等.

泄漏是在资源完成后保留资源,增加工作集的大小.这些资源尚未"完成",它们处于队列中并等待服务.一旦完成,我相信它们正在被正确发布.

[编辑回应安东的进一步评论] 好的 - 我发现了一些东西!我认为这是一个在IIS下不会发生的Cassini问题.您是否在Cassini(Visual Studio开发Web服务器)下运行处理程序?

当我在Cassini下运行时,我也看到这些泄漏的System.Runtime.Remoting命名空间实例.如果我将处理程序设置为在IIS下运行,我看不到它们.你能否确认一下你的情况?

这让我想起了我见过的其他远程/卡西尼问题.IIRC,有一个类似IPrincipal的实例,需要存在于模块的BeginRequest中,并且在模块生命周期结束时,需要从Cassini中的MarshalByRefObject派生而不是IIS.出于某种原因,似乎卡西尼在内部进行了一些远程操作,而IIS则没有.

  • @Anton - 我怀疑你错过了我说的话.我不是说请求应该按顺序顺序到达,我说这不是内存泄漏.您可能有一个*capacity*问题,可以通过编写更有效的处理程序或扩展来解决,但如果您正在寻找"泄漏"的来源,我不相信您会发现它,因为我不喜欢不相信有一个.你在看"错误的问题" (3认同)
  • @Rob,根据你的定义,这*是*内存泄漏.资源完成后绝对会被保留.运行示例大小为10,000的示例.在一两分钟内,所有请求将"完成"并返回响应.但是,分析托管堆,即使在等待一小时和强制垃圾收集之后,第2代堆仍然保留10,000组远程处理对象.这些*永远不会消失并且不被*释放,因此内存泄漏. (2认同)
  • @Rob(再次,抱歉:)),即使在更小的样本量中也存在泄漏.运行示例大小为1000的示例.经过5次迭代后,gen 2堆中保存了5000组远程处理对象.经过10次迭代后,其中有10,000次.这是在完全强制垃圾收集之后.这怎么可能不是泄漏? (2认同)
  • @Rob(最后一次,我保证),即使我使用AutoResetEvent强制一个请求在下一次进行之前完成,泄漏也存在,它只是更加微妙.转储堆并使用WinDbg进行检查非常清楚地表明,每个请求分配的数千个远程处理对象仍未*被释放. (2认同)