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

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

В моих тестах веб-сервер Visual Studio (Кассини) спрыгивает с 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. Генерируйте большое количество HttpWebRequests к 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) в ProcessRequest, это просто намного более тонко. Пример усиливает проблему путем создания его с большей готовностью очевидным, но основные принципы являются тем же.

11
задан Anton 20 May 2010 в 14:39
поделиться

4 ответа

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

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

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

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

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

и

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

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

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

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

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

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

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

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

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

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

14
ответ дан 3 December 2019 в 07:11
поделиться

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

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

-2
ответ дан 3 December 2019 в 07:11
поделиться

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

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

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

1
ответ дан 3 December 2019 в 07:11
поделиться

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

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

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

4
ответ дан 3 December 2019 в 07:11
поделиться
Другие вопросы по тегам:

Похожие вопросы: