Почему IHttpAsyncHandler пропускает память под нагрузкой?

Я заметил, что.NET IHttpAsyncHandler (и IHttpHandler, в меньшей степени) теряют память при одновременных веб-запросах.

В моих тестах веб-сервер Visual Studio (Cassini) перепрыгивал с 6 МБ памяти до более чем 100 МБ, и после завершения теста ни один из них не восстанавливался.

Проблема может быть легко воспроизведена. Создайте новое решение (LeakyHandler) с двумя проектами:

  1. Веб-приложение ASP.NET (LeakyHandler.WebApp)
  2. Консольное приложение (LeakyHandler.ConsoleApp)

В LeakyHandler.WebApp:

  1. Создайте класс с именем TestHandler, который реализует IHttpAsyncHandler.
  2. При обработке запроса сделайте краткий сон и завершите ответ.
  3. Добавьте обработчик HTTP в Web.config как test.ashx.

В 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 для просмотра файлов дампа, и несколько подозрительных типов хранятся в памяти и никогда не выпускаются. Каждый раз, когда я запускаю тест с размером выборки 10000, я получаю в памяти еще 10000 таких объектов.

  • 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

Эти объекты лежат в куче второго поколения и не собираются даже после принудительной полной сборки мусора.

Важная заметка

Проблема существует даже при форсировании последовательных запросов и даже без Thread.Sleep(10) в ProcessRequestГораздо тоньше. Этот пример усугубляет проблему, делая ее более очевидной, но основные принципы те же.

4 ответа

Решение

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() после request.BeginGetResponse(GetResponseCallback, request);

и

.Set() после streamReader.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:

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

[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.

Утечка - это когда ресурсы задерживаются после того, как они закончатся, увеличивая размер рабочего набора. Эти ресурсы не были "закончены", они находятся в очереди и ожидают обслуживания. Как только они будут завершены, я верю, что их выпускают правильно.

[Изменить в ответ на дальнейшие комментарии Антона] Хорошо - я кое-что раскрыл! Я думаю, что это проблема Кассини, которая не возникает в IIS. Работаете ли вы с обработчиком под Cassini (веб-сервер разработки Visual Studio)?

Я также вижу эти протекающие экземпляры пространства имен System.Runtime.Remoting, когда я работаю только под Cassini. Я не вижу их, если я настроил обработчик для работы под IIS. Можете ли вы подтвердить, так ли это для вас?

Это напоминает мне о некоторой другой проблеме удаленного взаимодействия / Кассини, которую я видел. IIRC, имеющий экземпляр чего-то вроде IPrincipal, который должен существовать в BeginRequest модуля, а также в конце жизненного цикла модуля, должен быть производным от MarshalByRefObject в Cassini, но не от IIS. По какой-то причине кажется, что Кассини делает внутреннее напоминание, что IIS нет.

Измеряемая вами память может быть выделена, но не использована CLR. Чтобы проверить попробуйте позвонить:

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

в ProcessRequest(), Пусть ваше консольное приложение сообщит об этом ответе от сервера, чтобы увидеть, сколько памяти реально используется живыми объектами. Если это число остается довольно стабильным, то CLR просто пренебрегает GC, потому что считает, что у него достаточно оперативной памяти. Если это число постоянно увеличивается, то у вас действительно есть утечка памяти, которую вы можете устранить с помощью 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

Нет проблем с вашим кодом, и нет утечки памяти. Попробуйте запустить консольное приложение пару раз подряд (или увеличьте размер выборки, но помните, что в конечном итоге ваши http-запросы будут отклонены, если у вас слишком много одновременных запросов спит).

С вашим кодом я обнаружил, что когда использование памяти веб-сервера достигло около 130 МБ, было достаточно нагрузки, чтобы запустить сборку мусора и уменьшить ее до около 60 МБ.

Может быть, это не то поведение, которого вы ожидаете, но среда выполнения решила, что более важно быстро реагировать на ваши быстрые входящие запросы, чем проводить время с GC.

Это будет очень вероятно проблема в вашем коде.

Первое, что я хотел бы проверить, это отключить все обработчики событий в вашем коде. Каждый += должен быть отражен - -= обработчика события. Вот как большинство приложений.Net создают утечку памяти.

Другие вопросы по тегам