Почему IHttpAsyncHandler пропускает память под нагрузкой? - PullRequest
11 голосов
/ 13 мая 2010

Я заметил, что .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. Создает большое количество запросов HttpWeb для test.ashx и выполняет их асинхронно.

По мере увеличения количества запросов HttpWebRequest (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

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

Важное примечание

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

Ответы [ 4 ]

14 голосов
/ 19 мая 2010

Я посмотрел на ваш код (и запустил его), и я не верю, что увеличение памяти, которое вы видите, на самом деле является утечкой памяти.

Проблема, с которой вы столкнулись, заключается в том, что ваш вызывающий код (консольное приложение) по сути работает в узком цикле.

Тем не менее, ваш обработчик должен обрабатывать каждый запрос, и он дополнительно "nobbled" Thread.Sleep(10). Практическим результатом этого является то, что ваш обработчик не успевает за поступающими запросами, поэтому его «рабочий набор» растет и растет по мере того, как все больше запросов помещаются в очередь, ожидая обработки.

Я взял ваш код и добавил AutoResetEvent в консольное приложение, выполнив

.WaitOne() после request.BeginGetResponse(GetResponseCallback, request);

и

.Set() после streamReader.ReadToEnd();

Это имеет эффект синхронизации вызовов, поэтому следующий вызов не может быть выполнен до тех пор, пока первый вызов не перезвонит (и не завершится). Поведение, которое вы видите, исчезает.

Таким образом, я думаю, что это просто сбойная ситуация, а не утечка памяти вообще.

Примечание: я отслеживал память с помощью следующего метода GetResponseCallback:

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

[Изменить в ответ на комментарий от Антона] Я не предполагаю, что здесь нет никаких проблем. Если ваш сценарий использования таков, что использование этого обработчика является реальным сценарием использования, то, очевидно, у вас есть проблема. Я хочу сказать, что это не проблема утечки памяти, а проблема емкости. Чтобы решить эту проблему, можно написать обработчик, который мог бы работать быстрее, или масштабировать до нескольких серверов и т. Д. И т. Д.

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

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

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

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

4 голосов
/ 19 мая 2010

Измеряемая вами память может быть выделена, но не использоваться 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

1 голос
/ 19 мая 2010

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

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

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

0 голосов
/ 13 мая 2010

Скорее всего, это проблема вашего кода.

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

...