11

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

在我的测试中,Visual Studio Web 服务器 (Cassini) 的内存从 6MB 跃升至 100MB 以上,并且一旦测试完成,就没有一个内存被回收。

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

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

在 LeakyHandler.WebApp 中:

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

在 LeakyHandler.ConsoleApp 中:

  1. 生成大量 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,它也更加微妙。该示例通过使问题更加明显而加剧了问题,但基本原理是相同的。

4

4 回答 4

14

我查看了您的代码(并运行它),我不相信您看到的内存增加实际上是内存泄漏。

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

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

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

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

和一个

.Set()streamReader.ReadToEnd();

这具有同步调用的效果,因此在第一个调用回调(并完成)之前不能进行下一个调用。你看到的行为消失了。

总之,我认为这纯粹是一种失控的情况,实际上根本不是内存泄漏。

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

 GC.Collect();
 GC.WaitForPendingFinalizers();
 Console.WriteLine(GC.GetTotalMemory(true));

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

泄漏是指资源在完成后被保留,从而增加了工作集的大小。这些资源还没有“完成”,它们在队列中等待服务。一旦它们完成,我相信它们会被正确释放。

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

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

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

于 2010-05-19T16:50:03.197 回答
4

您正在测量的内存可能已被 CLR 分配但未使用。要检查尝试调用:

GC.Collect();
context.Response.Write(GC.GetTotalMemory(true));

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

于 2010-05-18T23:29:38.200 回答
1

你的代码没有问题,也没有内存泄漏。尝试连续运行您的控制台应用程序几次(或增加您的样本大小,但请注意,如果您有太多的并发请求处于休眠状态,最终您的 http 请求将被拒绝)。

使用您的代码,我发现当 Web 服务器内存使用量达到大约 130MB 时,垃圾收集启动的压力足够大,并将其减少到大约 60MB。

也许这不是您期望的行为,但运行时已经决定快速响应您的快速传入请求比花时间处理 GC 更重要。

于 2010-05-18T23:16:41.700 回答
-2

这很可能是您的代码中的问题。

我要检查的第一件事是您是否分离代码中的所有事件处理程序。每个 += 都应该由事件处理程序的 -= 镜像。这就是大多数 .Net 应用程序造成内存泄漏的方式。

于 2010-05-13T06:23:00.247 回答