OutOfMemoryException, когда доступно много памяти

У нас есть приложение, работающее на 5 (серверных) узлах (16 ядер, 128 ГБ памяти каждый), которые загружают почти 70 ГБ данных на каждом компьютере. Это приложение распространяется и обслуживает одновременных клиентов, поэтому существует много использования сокетов. Аналогично, для синхронизации между несколькими потоками используется несколько методов синхронизации, в основном используя System.Threading.Monitor.

Теперь проблема заключается в том, что, пока приложение запущено, и данные перемещаются между этими узлами сервера и между клиентами и серверами, одна или две серверные машины начинают получать OutOfMemoryException, хотя имеется еще 40% памяти. У нас есть ощущение, что это исключение исходит из неуправляемого кода. Хотя мы не делаем никаких неуправляемых вызовов, мы видели, что последний вызов в стеке стека исключений OOM всегда является вызовом структуры, который внутренне вызывает неуправляемый код.

Ниже приведены несколько примеров.

Exception of type 'System.OutOfMemoryException' was thrown.
   at System.Threading.Monitor.ObjPulseAll(Object obj)
   ....

Exception of type 'System.OutOfMemoryException' was thrown.
   at System.Threading.Monitor.ObjWait(Boolean exitContext, Int32 millisecondsTimeout, Object obj)
   at System.Threading.Monitor.Wait(Object obj, TimeSpan timeout)
   ....

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

Любая помощь будет оценена.

EDIT:

Ниже приведены некоторые подробности:

  • Приложение работает в процессе x64.
  • Windows Server 2012 R2
  • .NET Framework 4.5
  • Сервер GC включен
  • AllowLargeObject установлен флаг.

EDIT2: Обратите внимание, что это не утечка памяти. Здесь доступен размер процесса 70 ГБ.

Ответ 1

Некоторые предварительные вопросы, которые предлагали другие пользователи, были классными, но считали ли вы ленивым и профилирующим ваше приложение?

Я могу вспомнить профайлера Ants из Redgate или dotmemory из JetBrains, ссылки ниже.

http://www.red-gate.com/products/dotnet-development/ants-memory-profiler/

https://www.jetbrains.com/dotmemory/

Ответ 2

Даже если есть утечка памяти из неуправляемого кода, если у вас имеется 40% доступная память, вы должны иметь возможность выделять объекты. Я думаю, что это проблема фрагментации, а не утечка памяти.

1- Это данные, которые вы пытаетесь выделить в больших или маленьких кусках?

2- Вы пытались заставить сборщик мусора (вызывая GC.Collect())? сбор мусора не только освобождает память, но и сокращает ее фрагментацию.

Ответ 3

GC.Collect() будет освобождать только память, где объект не ссылается ни на что другое.

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

В качестве упражнения во избежание утечек рекомендуется реализовать объекты IDisposable для объектов (даже это означает, что они предназначены для освобождения неуправляемых объектов), просто с точки зрения обеспечения того, чтобы все обработчики были отключены, коллекции очищаются правильно и любые другие ссылки на объекты установлены в нуль.

Ответ 4

Я предлагаю использовать ADPlus или другие инструменты для получения дампа вашего процесса, когда это исключение возникает. Используя этот дамп, вы можете отлаживать свою дамп файл с помощью WinDbg. Все приведенные ниже команды взяты из сообщения в блоге Изучение дампов памяти ASP.Net для идиотов (например, Me).

Исследование утечек памяти

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

!dumpheap
Команда

"dumpheap" даст вам количество объектов и использование памяти для объектов. Затем вы можете исследовать, какие типы объектов используют большую часть вашей памяти.

!dumpheap -type System.IO.MemoryStream
Команда

"dumpheap-type" отобразит все объекты в куче, которые имеют тип MemoryStream. Хорошая вещь о WinDbg - вы можете исследовать неуправляемые утечки памяти: Пример 1 и Пример2.

Ответ 5

Если это проблема фрагментации, вы не можете решить ее без какого-либо профилирования. Найдите профилировщик памяти, который поддерживает обнаружение фрагментации, чтобы точно знать причину этой фрагментации.

Ответ 6

Сбор мусора с помощью LargeObjectHeapCompactionMode = CompactOnce может помочь исправить фрагментацию.

GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect();

Ответ 7

Обратите внимание, что пока подписчик событий подписан, издатель события содержит ссылку на подписчика. Это распространенная причина утечек памяти в .NET, и в вашем случае это не будет серьезной утечкой, но если управляемый объект удерживает указатель или дескриптор неуправляемого объекта, то он не удаляет этот неуправляемый объект и поэтому вызывает фрагментацию памяти,

Если вы уверены, что причиной фрагментации является неуправляемый компонент и что у вас что-то не хватает, и если у вас есть доступ к коду умененного компонента, вы можете перекомпилировать его и связать с ним, используя приличный распределитель памяти, такой как клад, Но это должно быть сделано, когда больше нечего делать и после серьезного профилирования.

Ответ 8

В .NET 4.5 команда CLR улучшила распределение кучи больших объектов (LOH). Даже тогда они по-прежнему рекомендуют группировать объекты, чтобы помочь работе с большими объектами. Похоже, фрагментация LOH случается реже в 4.5, но это все равно может произойти. Но из трассировки стека он не связан с LOH.

Даниэль Лейн предложил ГС-тупики. Мы также видели, как это происходит на производственных системах, и они определенно вызывают проблемы с размером процесса и из-за нехватки памяти.

Одна вещь, которую вы можете сделать, - запустить Debug Diagnostics Tool, захватить полный дамп при возникновении OutOfMemoryException, а затем провести анализ анализа дампа для информации о сбоях и памяти. Я видел, как некоторые интересные вещи случаются с родными и управляемыми кучами из этого отчета. Например, мы обнаружили, что драйвер принтера выделил 1 ГБ неуправляемой кучи в 32-битной системе. Исправлена ​​проблема с драйвером. Конечно, это была клиентская система, но что-то подобное могло произойти с вашим сервером.

Я согласен, что это звучит как ошибка основного режима. Рассматривая реализацию System.Threading.Monitor.Wait, ObjWait, PulseAll и ObjPulseAll из .NET 4.5 Reference Code показывает эти классы вызывают собственные методы:

   /*========================================================================
    ** Sends a notification to all waiting objects. 
    ========================================================================*/
    [System.Security.SecurityCritical]  // auto-generated
    [ResourceExposure(ResourceScope.None)]
    [MethodImplAttribute(MethodImplOptions.InternalCall)]
    private static extern void ObjPulseAll(Object obj);

    [System.Security.SecuritySafeCritical]  // auto-generated
    public static void PulseAll(Object obj)
    {
        if (obj == null)
        {
            throw new ArgumentNullException("obj");
        }
        Contract.EndContractBlock();

        ObjPulseAll(obj);
    }

Комментарий к статье Раймонда Чена о "PulseEvent в корне ошибочен" от Майка Диммика:

Monitor.PulseAll - это оболочка вокруг Monitor.ObjPulseAll, которая является внутренний вызов внутренней функции CLR ObjectNative:: PulseAll. Это, в свою очередь, обертывает ObjHeader:: PulseAll, который обертывает SyncBlock:: PulseAll. Это просто сидит в цикле, вызывающем SetEvent, до тех пор, пока на объект не будет больше потоков.

Если у кого-то есть доступ к исходному коду для CLI, возможно, они могут опубликовать больше об этой функции и о том, из чего может произойти ошибка памяти.

Ответ 9

Уверенное предположение, не видя вашего кода, заключается в том, что у вас есть проблема с блокировкой STA при завершении, особенно учитывая, что это высокая система concurrency, судя по вашим здоровенным требованиям к оборудованию. В любом случае, видя, что вы пытались заставить ГК тупик, имеет смысл, если финализация зашла в тупик, GC не сможет выполнять свою работу. Надеюсь, это вам поможет.

Расширенные методы предотвращения и обнаружения тупиковых ситуаций в .NET-приложениях

В частности, раздел, который представляет интерес, приведен ниже,

Когда ваш код выполняется в потоке с однопоточной квартирой (STA), происходит эквивалент исключительной блокировки. Только один поток может обновлять окно графического интерфейса пользователя или запускать код внутри COM-компонента, находящегося в квартире, внутри STA. Такие потоки имеют очередь сообщений, в которую обрабатываемая информация размещается системой и другими частями приложения. GUI используют эту очередь для получения информации, такой как запросы перерисовки, ввод устройства для обработки и запросы закрытия окна. Прокси-сервер COM использует очередь сообщений для перехода вызовов метода межсетевой обработки в квартиру, для которой компонент имеет сродство. Весь код, выполняемый на STA, отвечает за перекачку очереди сообщений и обработки новых сообщений с помощью цикла сообщений, иначе очередь может засориться, что приведет к потерянному реагированию. В терминах Win32 это означает использование MsgWaitForSingleObject, MsgWaitForMultipleObjects (и их экземпляров Ex) или API CoWaitForMultipleHandles. Ожидание без накачки, такое как WaitForSingleObject или WaitForMultipleObjects (и их эквиваленты Ex), не будет накачать входящие сообщения.

Другими словами, блокировка STA может быть освобождена только путем перекачки очереди сообщений. Приложения, которые выполняют операции, чьи характеристики производительности сильно различаются по потоку графического интерфейса без перекачки сообщений, как и те, которые были отмечены ранее, могут легко блокироваться. Хорошо написанные программы планируют такую ​​долговременную работу в другом месте или накачать сообщения каждый раз, когда они блокируются, чтобы избежать этой проблемы. К счастью, CLR насосы для вас, когда вы блокируете в управляемом коде (через вызов спорного Monitor.Enter, WaitHandle.WaitOne, FileStream.EndRead, Thread.join, и так далее), помогая смягчить эту проблему. Но много кода и даже некоторая часть самой .NET Framework заканчивается блокировкой неуправляемого кода, и в этом случае авторский код блокирующего кода может быть добавлен или, возможно, не был добавлен.

Вот классический пример тупика, вызванного STA. Поток, запущенный в STA, генерирует большое количество экземпляров COM-компонента с резьбой в квартире и, неявно, их соответствующие Runtime Callable Wrappers (RCW). Конечно, эти RCW должны быть доработаны CLR, когда они становятся недоступными, или они будут течь. Но поток финализатора CLR всегда присоединяется к процессу Multithreaded Apartment (MTA), то есть он должен использовать прокси-сервер, который переходит в STA, чтобы вызвать Release на RCW. Если STA не перекачивает, чтобы получить попытку финализатора вызвать метод Finalize для данного RCW, возможно, потому, что он решил блокировать использование ожидания без накачки - поток финализатора будет застревать. Он блокируется, пока STA не блокирует и не откачивает. Если STA никогда не накачивается, поток финализатора никогда не достигнет какого-либо прогресса, и с течением времени произойдет медленное, молчаливое наращивание всех финализированных ресурсов. Это, в свою очередь, может привести к последующему сбою вне памяти или переработке процесса в ASP.NET. Очевидно, что оба результата неудовлетворительны. Высокоуровневые структуры, такие как Windows Forms, Windows Presentation Foundation и COM, скрывают большую часть сложности STA, но они все еще могут быть непредсказуемыми, включая тупик. Контексты синхронизации COM представляют аналогичные, но тонко разные задачи. И, кроме того, многие из этих сбоев будут происходить только в небольшой части тестовых пробегов и часто только при высоком напряжении.

Ответ 10

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

Вы попадаете в ситуацию, когда коллекционер GC не думает, что вам не хватает памяти, потому что большинство вещей в вашей куче 1-го поколения - 8 байтовых ссылок, где на самом деле они похожи на айсберги в море. Большая часть памяти ниже!

Вы можете использовать эти вызовы GC:

System::GC::AddMemoryPressure(sizeOfField);
System::GC::RemoveMemoryPressure(sizeOfField);

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