Изображение: Julien Dumont, Flickr К сожалению, иногда случаются ситуации, когда система перестает работать или начинает безудержно потреблять ресурсы, а логи и системные метрики не могут помочь. Ситуация еще усугубляется тем, что на системе в продакшене нет Visual Studio или какого-либо отладчика, невозможно поотлаживаться удаленно. Чаще всего даже нет возможности получить доступ этой машине. В данном случае единственным решением является анализ дампа памяти процесса. Я хочу описать несколько общих сценариев поиска проблем на таких дампах. Это поиск взаимоблокировок, утечек памяти и высокого потребления процессорных ресурсов. Особенность таких проблем в том, что они крайне редко воспроизводятся на компьютерах разработчиков, сложно покрываются автоматизированным тестированием и для их появления нужны большие ресурсы. Поэтому важным этапом при разработке системы является проведение серий нагрузочных тестов на железе, приближенным к такому, на котором она будет работать у заказчика. При проявлении таких проблем сразу же проводить анализ и исправлять. Дамп памяти процесса можно снять разными способами. Например, в Диспетчере задач можно выделить интересующий процесс и в контекстном меню вызвать пункт «Создать файл дампа». Мне же очень помогает утилита ProcDump от Марка Руссиновича. С ее помощью можно снимать разные виды дампов по разным событиям. Например, можно снять полный дамп памяти процесса в момент, когда процесс потребляет CPU больше 30% в течение 10 секунд. В общем, очень полезная утилита. Анализировать дампы можно различными инструментами, например, в Visual Studio или с помощью библиотеке ClrMD. Я же буду использовать WinDbg и Sos. Никаких особенных преимуществ, просто мне удобно с ними работать. Также незаменимым помощником для меня стал плагин Sosex, который очень сильно упрощает жизнь в поиске проблем и анализе. Перед началом наших изысканий отмечу, что для удобства в WinDbg стоит настроить отладочные символы. Этот процесс уже много раз описывался на просторах интернета, поэтому я не буду повторяться, а просто приведу ссылку.  Перейдем к рассмотрению реальных сценариев. Практически все описанные сценарии возникли в реальной практике и мне или моим коллегам пришлось в свое время с ними разобраться. Но, чтобы не нарушать коммерческой тайны, я подготовил несколько демонстрационных примеров, которые иллюстрируют возникшие проблемы. Примеры можно взять здесь. Для правдоподобности будем анализировать не рабочий процесс, а его дамп. Для этого снимем полный дамп памяти процесса с командой procdump.exe -ma PID. Итак, начнем. (Пример 01-MonitorDeadlock) При поиске взаимоблокировок очень хорошо может помочь Sosex. Допустим, мы заметили, что наше приложение не реагирует ни на какие команды. Оно зависло. Что делать?
Запускаем WinDbg (важно, той же разрядности, что и приложение) и загружаем снятый дамп (нажимаем Ctrl+D и в диалоге выбираем файл дамп). Загружаем нужные расширения В Sosex есть прекрасная команда dlk, которая умеет искать взаимоблокировки между блоками синхронизации и/или объектами ReaderWriterLock. Запускаем ее и смотрим на результат. Нам повезло! Sosex нам сразу показал, что у нас есть взаимоблокировка, на каких объектах она возникла и в каких потоках. С помощью команды threads получим полный список управляемых потоков и сопоставим идентификатор управляемого потока с внутренним идентификатором WinDbg. Мы видим, что интересующие нас потоки 3 и 4 имеют захваченные блокировки (столбец Lock Count c 1). Теперь посмотрим стеки этих потоков, чтобы увидеть в каком месте произошла блокировка. Для этого вызовем команду ~N*!clrstack, где N – это внутренний идентификатор потока в отладчике (9 и 10). Часть вывода скрыта для краткости Прекрасно! Теперь мы видим методы, которые вызвали взаимоблокировку. Мы имеем классический пример блокировки на двух локах. (Пример 02-RwlDeadlock) Но иногда бывают ситуации, когда команда dlk не дает результата. Что делать в таком случае? Мы же все равно подозреваем, что взаимоблокировка есть. Для начала посмотрим какие блоки синхронизации есть у процесса и кем они захвачены. Результатов нет. В системе ни один блок синхронизации не захвачен. Возможно, блокировка может быть на объекте ReaderWriterLock. Проверить это можно с помощью команды Sosex rwlock. Отлично! Мы видим, что есть объект ReaderWriteerLock, у которого есть 3 потока, ожидающие блокировки на запись. Посмотрим на стеки этих потоков (не забываем сопоставить CLR идентификаторы сопоставить с внутренними идентификаторами отладчика с помощью команды threads). Мы нашли пример неправильного использования метода UpgradeToWriterLock класса ReaderWriterLock. Из этих примеров можно попробовать описать алгоритм, с помощью которого можно анализировать взаимоблокировки: Этот алгоритм помогает мне при решении, наверное, 80% проблем. Остальные 20% требуют более нетривиальных действий. (Пример 03-MemoryLeak) Иногда может возникнуть ситуация, когда приложение начинает выделять все больше памяти, не освобождая ее. В таком случае необходимо провести анализ выделенной памяти на предмет поиска утечек. В .Net-приложениях утечки могут возникнуть, когда сборщик мусора не может освободить корни этого объекта. Источниками корней могут являться: Утечку памяти можно заметить на графике выделения памяти процесса. Его можно получить с помощью счетчиков производительности, посмотрев на счетчики Process\Private bytes, .Net CLT Memory# Bytes in All Heaps и пр. Но мне удобней наблюдать за графиком Private Bytes в программе Process Explorer. Если на графике мы видим, что постоянно выделяется все больше памяти, которая не освобождается, и что размер второго поколения растет. Это недвусмысленно намекает, что у нас есть утечка памяти. Снимаем дамп памяти процесса и анализируем его. Загружаем снятый дамп в WinDbg и подключаем sos и sosex как описано выше. У sosex есть прекрасная возможность построить индекс кучи для последующего использования (команда bhi). Этот индекс помогает значительно ускорить выполнение команд анализа кучи. Например, на дампе в 5 Гигабайт выполнение команды GcRoot занимало у меня порядка 20 минут, в ее аналог из sosex mroot на построенном индексе выполнился за пару секунд. Сначала посмотрим общую статистику выделения памяти в куче. Как и ожидалось, второе поколение разрослось до неимоверных размеров. Посмотрим статистику выделенных объектов в куче с помощью команды sosex dumpgen. Последняя версия sosex позволяет в качестве искомого поколения указать all и таким образом проанализировать всю кучу. Видно, что в куче выделено больше 15 тысяч объектов EventHandler.
 Посмотрим на поля одного из них (последнего). В этом помогут команды sos.do или sosex.mdt Обработчик событий указывает на объект класса Worker. Выберем несколько конкретных объектов EventHandler и посмотрим на их корни. Видно, что оба объекта зацепились за один и тот же экземпляр класса GlobalNotifier. Можно сказать, что это статический объект, к которому происходит подписка события у объектов Worker. Проанализируем потоки и их стеки. Что происходит в методе Main? Дизассемблируем его. У sosex есть команда muf, которая выводит агрегированный листинг IL-кода и сгенерированного ассемблерного кода. Иногда, когда нет доступа к исходникам, она может быть очень удобной. (Приведенный вывод ограничен) Мы имеем классический пример утечки памяти на событиях C#. В бесконечном цикле создаются новые объекты класса Worker, которые подписываются на событие GlobalNotifier.SomethingHappened. Отписка нигде не происходит, и, следовательно, сборщик мусора не может освободить выделенную память. Попробуем составить примерный алгоритм анализа утечек памяти: (Пример 04-CpuUtilization. Запустите пример и подождите минут 10-15.) На последок осталось рассмотреть случай, когда наша система начинает жадно потреблять процессорные ресурсы. Обычно, высокое потребление ресурсов процессора может говорить о том, что наше приложение могло попасть в бесконечный цикл и единственный способ его понизить – это перезапустить приложение. Снимем дамп процесса и проанализируем его. С помощью команды .time посмотрим, сколько времени наше приложение провело в пользовательском режиме, а сколько в режиме ядра. В моем примере программа провела в режиме ядра 13 секунд, а в пользовательском режиме 14 минут. Это означает, что потоки постоянно выполняют какую-то работу и практически никогда не засыпают. Это довольно странно. В WinDbg есть команда runaway, которая может показать распределение времени выполнения по потокам. С параметрами по умолчанию runaway выводит только время в режиме пользователя. Мы можем видеть, что 4 потока что-то безостановочно молотят на протяжении почти 3 минут каждый. Здесь показаны системные идентификаторы потоков. С помощью команды threads получим внутренние идентификаторы отладчика и заодно посмотрим, не возникло ли никаких исключений. Можно видеть, что во всех интересующих нас потоках возникали исключения KeyNotFoundException. Посмотрим на стеки потоков (приведен вывод для одного потока, стеки остальных аналогичны). Теперь мы знаем место в исходниках, где искать проблему. Если заглянуть в исходники примера, то можно видеть бесконечный цикл, в котором всегда происходит ошибка получения данных из ConcurrentDictionary. Общий алгоритм поиска довольно просто: В реальной жизни все намного сложней: в приложении крутятся десятки или сотни потоков, а в куче выделены миллионы объектов. Это сильно усложняет анализ, но описанные шаги неоднократно помогали мне при анализе проблем с системами. Надеюсь, что приведенные мной примеры использования WinDbg, Sos и Sosex и вам помогут облегчить жизнь и спасти немного нервных клеток. Все исходники примеров для изучения можно взять здесь  Программист