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

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

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

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

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

В LeakyHandler.WebApp:

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

В LeakyHandler.ConsoleApp:

  • Создайте большое количество 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

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

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

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

Ответ 1

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

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

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

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

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

и a

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

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

В целом, я думаю, что это чисто беглая ситуация, а не утечка памяти вообще.

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

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

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

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

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

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

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

Ответ 2

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

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

в ProcessRequest(). Попросите консольное приложение сообщить об этом от сервера, чтобы узнать, сколько памяти действительно используется живыми объектами. Если это число остается довольно стабильным, CLR просто пренебрегает выполнением GC, потому что считает, что он имеет достаточное количество оперативной памяти. Если это число постоянно возрастает, то у вас действительно есть утечка памяти, которую вы можете решить с помощью WinDbg и SOS.dll или других (коммерческих) инструментов.

Изменить: ОК, похоже, что у вас настоящая утечка памяти. Следующий шаг - выяснить, что держится за эти объекты. Для этого вы можете использовать команду SOS ! Gcroot. Существует хорошее объяснение для .NET 2.0 here, но если вы можете воспроизвести это на .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

Ответ 3

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

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

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

Ответ 4

Вероятно, это будет проблемой в вашем коде.

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