CLR с изнанки

Усовершенствования производственной диагностики в CLR 4

Джон Лэнгдон

В группе Common Language Runtime (CLR) имеется группа, занимающаяся предоставлением API и служб, позволяющих создавать диагностические инструменты для управляемого кода. Два наших крупнейших компонента (с точки зрения инженерных ресурсов) - это управляемая отладка и API профилирования (ICorDebug* и ICorProfiler*, соответственно).

Как и оставшаяся часть групп CLR и framework, наша ценность реализуется только в приложениях, созданных в результате наших усилий. Например, группы Visual Studio используют эти API отладки и профилирования для своего управляемого отладчика и инструментов профилирования производительности, а ряд сторонних разработчиков создают инструменты, использующие API профилирования.

В течение последних 10 лет большое внимание в этой области, как для CLR, так и для Visual Studio, уделялось обеспечению настольных сценариев разработчика: обход исходного кода в отладчике для поиска ошибок кода; запуск приложения с инструментом профилирования производительности для выявления медленных частей кода; редактирование и продолжение для уменьшения времени цикла редактирования, сборки, отладки и т.д. Эти инструменты могут быть полезными для поиска ошибок в приложениях после их установки на компьютере пользователя или развертывании на сервере (оба случая далее называются производством), также имеется ряд сторонних поставщиков, создающих инструменты диагностики мирового класса на основе вашей работы.

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

CLR 4 (среда выполнения в основе Microsoft .NET Framework 4) является первым выпуском, в котором мы сделали значительное усилие по внедрению комментариев пользователей, атакже начали расширять сценарии поддержки диагностических API по направлению к сфере производства.

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

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

Наконец, я кратко поясню, как мы облегчили развертывание средств профилирования, исключив их зависимость от реестра. На протяжении всей статьи основное внимание будет уделяться новым инструментам, поддерживающим результаты наших усилий, но по возможности я буду включать ссылки на дополнительные ресурсы, которые помогут вам понять, как использовать преимущества наших API через Visual Studio.

Отладка на основе дампа

Одно из популярных средств, которые мы предоставляем в Visual Studio 2010, — отладка управляемого дампа. Дампы процессов (обычно их называют просто дампами) часто применяются в ситуациях производственной отладки как для управляемого, так и для неуправляемого кода. Дамп фактически является снимком состояния процесса в определенный момент времени. Точнее, в файл помещается содержимое виртуальной памяти процесса (или ее некое подмножество).

До Visual Studio 2010, чтобы вести отладку управляемого кода в дампе, вам нужно было использовать специализированное расширение sos.dll Windows-отладчика (которое позволяет анализировать такие дампы) вместо более привычных инструментов в стиле Visual Studio (где вы скорее всего писали и отлаживали свой код в процессе разработки). Нашей целью было сделать так, чтобы вы, используя дамп для диагностики проблем в Visual Studio, получали нечто вроде стоп-кадра состояния при активной отладке (live debugging) — по аналогии с тем, когда вы отлаживаете код и останавливаете его выполнение в точке прерывания.

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

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

В CLR 2 отладочный API поддерживал отладку только выполняемых процессов, что затрудняло применение инструментария в только что описанных ситуациях. Фактически тот API изначально не был рассчитан на отладку на основе дампов. Одно лишь то, что этот API использует вспомогательный поток, выполняемый в целевом процессе для обслуживания запросов отладчика, по сути, перечеркивает отладку на основе дампов.

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

Чтобы предоставить решение для отладки управляемого кода в файле дампа, нам нужно было создать API, который не требовал бы выполнения кода в мишени для анализа состояния управляемого кода. В то же время, учитывая, что авторы отладчиков (в основном для Visual Studio) уже вложили значительные усилия в поддержку активной отладки на основе существующего API отладки, мы не хотели заставлять использовать два разных API.

В итоге в CLR 4 ряд API-средств отладчика был реализован заново (в основном были затронуты те API, которые были нужны для анализа кода и данных), чтобы избавиться от использования вспомогательного потока. В итоге существующему API больше не требуется знать, что представляет собой мишень — файл дампа или активный процесс. Более того, авторы отладчиков теперь могут использовать один и тот же API для разных сценариев отладки (активной и на основе дампа). Однако при активной отладке с расчетом на управление выполнением (с заданием точек прерывания и пошаговым проходом кода) отладочный API по-прежнему использует вспомогательный поток. В долгосрочной перспективе мы намерены убрать зависимость от такого потока и в этих сценариях отладки. Рик Байерз (Rick Byers) (бывший разработчик API отладочных сервисов) опубликовал очень полезную статью в блоге, в которой подробнее описывается эта работа (blogs.msdn.com/rmbyers/archive/2008/10/27/icordebug-re-architecture-in-clr-4-0.aspx.).

Теперь вы можете использовать ICorDebug для изучения управляемого кода и данных в файле дампа: проходить стеки, перечислять локальные переменные, получать тип исключения и т. д. В случае рухнувших или зависших приложений зачастую вполне достаточно проанализировать в дампе стеки потоков и сопутствующую информацию, чтобы найти причину проблемы.

Мы прекрасно знаем о важности диагностики памяти и других сценариев отладки, но у нам просто не хватило времени создать в CLR 4 новые API, которые позволяли бы отладчикам анализировать управляемую кучу иак, как требуется в такой ситуации. В дальнейшем мы обязательно добавим необходимые диагностические средства для отладки в производственных условиях.Чуть позже я расскажу и о другой работе, проделанной нами в этом направлении.

Также хочу обратить ваше внимание, что новые средства поддерживают как 32-, так и 64-разрядные мишени плюс отладку чисто управляемого и смешанного (управляемого и неуправляемого) кода. Visual Studio 2010 поддерживает смешанный режим для дампов, содержащих управляемый код.

Анализ блокировок-мониторов

Многопоточное программирование может оказаться очень трудным. Каким бы способом вы ни писали многопоточный код — самостоятельно или с применением инфраструктур и/или библиотек, которые многое делают за вас, диагностика асинхронного и параллельного кода бывает весьма сложной. Когда вы имеете дело с логической единицей работы в единственном потоке, понять причинно-следственные связи гораздо легче и зачастую для этого достаточно просмотреть стек вызовов потока. Но если эта работа распределена между несколькими потоками, трассировка потока управления становится намного проблематичнее. Почему не завершается работа? Какая-то ее часть блокируется или происходит что-то еще?

С распространением многоядерных процессоров разработчики все чаще смотрят в сторону параллельного программирования как способа повышения производительности ПО, поскольку теперь уже нельзя рассчитывать на простое повышение тактовых частот процессоров. Разработчики в Microsoft последние несколько лет активно работают над тем, чтобы упростить параллельное программирование. Что касается диагностики, то мы добавили несколько простых, но полезных API, которые поддерживают инструментарий, помогающий разработчикам эффективнее справляться со сложностями многопоточного кода. 

В CLR API отладчика были добавлены API-средства анализа блокировок-мониторов (monitor locks). Проще говоря, мониторы позволяют синхронизировать в программах доступ из нескольких потоков к общим ресурсам (неким объектам в вашем .NET-коде). Пока один поток блокирует ресурс, другой поток ждет. Как только поток, владеющий блокировкой, освобождает ее, ожидавший поток может захватить ее и получить доступ к ресурсу.

В .NET Framework мониторы предоставляются напрямую через пространство имен System.Threading.Monitor, но гораздо чаще их используют через ключевые слова lock в C# и SyncLock в Visual Basic. Они также используются в реализациях синхронизирующих методов, в Task Parallel Library (TPL) и других моделях асинхронного программирования. Новые API отладчика позволяют лучше понять, на каком объекте (если таковой есть) блокирован данный поток и какой поток (если таковой есть) владеет блокировкой для данного объекта. С помощью этих API отладчики помогают разработчикам выявлять взаимоблокировки и видеть, когда несколько потоков конкурируют за некий ресурс, так как подобные вещи весьма негативно влияют на производительность приложения.

Пример разновидности инструментов, поддерживаемых в результате нашей работы, вы найдете в средства отладки параллельных программ в Visual Studio 2010. На эту тему в сентябрьском номере MSDN Magazine за 2009 год был опубликован отличный обзор от Дэниела Мота (Daniel Moth) и Стефена Тауба (Stephen Toub) (msdn.microsoft.com/magazine/ee410778).

Больше всего в отладке дампов нам нравится возможность создания абстрактных представлений мишени отладки; по сути, это новая функциональность анализа (вроде изучения блокировок-мониторов), которая ценна как при активной отладке, так и при отладке на основе дампов. Именно эта функциональность, как ожидается, будет наиболее полезной разработчикам как в процессе создания приложения, так и при его сопровождении в производственных условиях.И поддержка анализа блокировок-мониторов в процессе отладки на основе дампов — превосходное пополнение в арсенале производственных диагностических средств CLR 4.

Тесс Феррандез (Tess Ferrandez), инженер технической поддержки Microsoft, выложила на Channel 9 видеоролик (channel9.msdn.com/posts/Glucose/Hanselminutes-on-9-Debugging-Crash-Dumps-with-Tess-Ferrandez-and-VS2010/), в котором она имитирует ситуацию с высокой конкуренцией за блокировки (подобные ситуации довольно распространены в приложениях организаций-заказчиков). После этого она пошагово показывает, как использовать Visual Studio 2010 для диагностики этой проблемы. Это отличный пример тех типов сценариев отладки, которые теперь поддерживаются новыми средствами.

Не только дампы

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

В случае диагностики проблем избыточного использования памяти мы обычно начинаем со списка экземпляров объектов, сгруппированных по типам, где указываются их количество и суммарный размер, и изучаем цепочки объектных ссылок. Передавать файлы дампов, содержащие эту информацию, между производственными машинами и компьютерами инженеров технической поддержки и разработчиков слишком неудобно и требует много времени. Кроме того, приложения постоянно растут в объемах — особенно это касается 64-разрядных программ, а значит, файлы дампов тоже увеличиваются в размерах, и на их передачу и обработку будет уходить все больше времени. Именно поэтому мы разрабатывали средства профилирования с учетом таких ситуаций.

Средства профилирования

На основе API профилирования из CLR построен целый ряд инструментов разных типов. Ситуации, в которых обычно используется API профилирования, можно разбить на три функциональные категории: производительность, память и оснащение (instrumentation).

Средства профилирования производительности (подобные поставляемому с некоторыми версиями Visual Studio) главным образом сообщают вам, где ваш код тратит свое время. Средства профилирования памяти обеспечивают детализацию использования памяти вашим приложением. Наконец, третья категория средств профилирования делает практически все остальное.

Позвольте мне немного прояснить последнюю фразу. Один из механизмов API профилирования позволяет вставлять IL-код в управляемый код в период выполнения. Мы называем такой IL-код оснащением (instrumentation). Клиенты используют эту функциональность для создания инструментов, предназначенных для решения широкого круга задач — от анализа охвата кода тестами (code coverage) до оснащения корпоративных приложений на основе .NET Framework средствами мониторинга и протоколирования.

Одно из преимуществ API профилирования над API отладки заключается в том, что он спроектирован максимально «облегченным». Оба API управляются событиями, например в них есть события для загрузки сборки, создания потока, генерации исключения и др., но в API профилирования вы регистрируетесь только на интересующие вас события. Более того, DLL профилирования загружается в целевой процесс, что обеспечивает быстрый доступ к состоянию в период выполнения.

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

Подключение и отключение средства профилирования

Пока несколько компаний создает средства производственного мониторинга приложений через оснащение IL-кодом, мы не располагаем изобилием инструментов, использующих средства мониторинга производительности и памяти в API профилирования. Здесь основным препятствием до сих пор была невозможность подключения инструментов на основе API профилирования к уже выполняемым процессам.

CLR — в версиях до CLR 4 — во время запуска проверяет, зарегистрировано ли средство профилирования. Если обнаруживается зарегистрированное средство профилирования, CLR загружает его и обеспечивает обратные вызовы, как того требует это средство. Соответствующая DLL никогда не выгружается. Как правило, все работает прекрасно, если задача этого инструмента — получение комплексной картины поведения приложения, но проблемы, о которых вы не знали, при запуске приложения не выявляются.

Вероятно, самый яркий пример тому — диагностика проблем с использованием памяти. Сегодня в этой ситуации мы часто обнаруживаем такой шаблон:сбор множества дампов, изучение различий в созданных типах между каждым дампом, построение графика роста занимаемой памяти, а затем поиск в тех частях кода приложения, откуда были ссылки на подозрительные типы. Может, причина в плохо реализованной схеме кеширования, а может, обработчики событий в одном типе удерживают ссылки на другой тип, который в ином случае находился бы вне области видимости. В итоге клиенты и инженеры технической поддержки тратят массу времени на диагностику проблем такого рода.

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

Чтобы облегчить муки работы в такой ситуации (и в некоторых других), мы добавили новый API, позволяющий средствам профилирования подключаться к выполняемым процессам и использовать подмножество существующего API профилирования. После подключения к процессу это подмножество дает возможность осуществлять выборку и диагностику памяти (см. статью «VS 2010: Attaching the Profiler to a Managed Application» по ссылке blogs.msdn.com/profiler/archive/2009/12/07/vs2010-attaching-the-profiler-to-a-managed-application.aspx) и диагностику памяти: проходить стек, преобразовывать адреса функций в символьные имена, использовать большую часть функций обратного вызова сборщика мусора (Garbage Collector, GC) и анализировать объекты.

В только что описанной ситуации инструменты позволят корпоративным клиентам, используя эту функциональность, подключиться к приложению, которое стало занимать слишком большой объем памяти или медленно отвечать на запросы, изучить, что выполняется в данный момент, и понять, какие типы существуют в управляемой куче и что именно удерживает их там. После сбора информации можно отключить инструмент, и CLR выгрузит DLL средства профилирования. Хотя остальная часть рабочего процесса будет аналогичной прежнему (поиск ссылок в коде на проблемные типы и др.), мы ожидаем, что новые инструменты этой разновидности значительно повлияют на среднее время устранения таких проблем. Более подробно об этом и других средствах профилирования см. блог Дэйва Бромена (Dave Broman), разработчика API профилирования в CLR (blogs.msdn.com/davbr).

Однако хотел бы обратить ваше внимание на два ограничения. Во-первых, диагностика памяти с подключением ограничена непараллельными (или блокирующими) режимами GC: когда GC приостанавливает выполнение всего управляемого кода в момент сбора мусора. Хотя по умолчанию используется параллельный режим GC, в ASP.NET применяется серверный режим GC, который не является параллельным. Это среда, в которой чаще всего наблюдаются эти проблемы. Мы высоко оцениваем полезность профилирующей диагностики с подключением для клиентских приложений и предполагаем создать необходимую функциональность в следующем выпуске. Все дело в расстановке приоритетов — мы выбрали наиболее распространенный случай для CLR 4.

Во-вторых, после подключения API профилирования не позволяет оснащать код приложения IL-кодом. Эта функциональность особенно важна для разработчиков средств мониторинга корпоративных приложений, которым нужно динамически изменять оснащение в ответ на текущие условия в период выполнения. Эту функциональность мы часто называем повторной JIT-компиляцией (re-JIT). На сегодняшний день у вас есть лишь одна возможность изменить IL-код метода: при его первой JIT-компиляции. Поддержку повторной JIT-компиляции мы предполагаем ввести в будущем выпуске.

Активация средств профилирования без использования реестра

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

До CLR 4, чтобы какой-либо инструмент мог загрузить свою DLL в управляемое приложение, он должен был создать две основные части конфигурационной информации. Первая — пара переменных окружения, сообщающих CLR, что нужно включить профилирование и какую реализацию профилирования следует загрузить (ее CLSID или ProgID) при запуске CLR. Учитывая, что DLL средств профилирования реализуются как внутренние COM-серверы (где CLR является клиентом), второй частью конфигурационных данных была соответствующая информация о регистрации COM, которая хранится в реестре. Она главным образом сообщала исполняющей среде через COM, где искать нужную DLL на диске.

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

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

В CLR 4 мы убрали необходимость в регистрации DLL средства профилирования на основе COM в реестре. Переменные окружения по-прежнему требуются, если вы хотите запускать свое приложение под контролем средства профилирования, но конфигурационной информации больше не требуется. Инструмент просто вызывает новый API подключения, передает путь к DLL, и CLR загружает средство профилирования. В дальнейшем мы еще больше облегчим настройку средства профилирования, потому что в ряде случаев клиентов не устраивает даже простое изменение переменных окружения.

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

Заключение

Работа над функциями диагностики для CLR имеет и плюсы, и минусы. Я видел множество примеров того, как клиенты бьются над определенными проблемами; тем не менее, всегда существует отличная работа, которую мы можем выполнить, чтобы упростить развертывание и облегчить жизнь клиентам. Если список проблем, с которыми сталкиваются клиенты, не меняется, –нет, мы не удаляем из него пункты, – значит, мы не преуспеваем в своей работе.

Я считаю, что в CLR 4 мы создали средства, которые не только дают моментальный выигрыш за счет устранения ряда насущных для клиентов проблем, но и закладывают фундамент, на который мы сможем опереться в будущем. Мы продолжим развивать API и сервисы, которые еще больше упростят доступ к значимой диагностической информации на всех этапах жизненного цикла приложения, поэтому вы сможете больше времени уделять созданию новой функциональности для своих клиентов, а не отладке.

Мы заинтересованы в обратной связи в области диагностики и поэтому опубликовали опросный лист на surveymonkey.com/s/developer-productivity-survey. Теперь, когда CLR 4 готова к выпуску, мы переключаем основное внимание на планирование предстоящих выпусков, и данные из этого опросного листа помогут нам правильнее расставить свои приоритеты. Если у вас есть несколько свободных минут, мы были бы рады узнать ваши соображения.                                

Джон Лэнгдон   (Jon Langdon) — менеджер программ в группе CLR, где в основном занимается диагностикой. До перехода в группу CLR был консультантом в Microsoft Services, помогал заказчикам диагностировать и устранять проблемы в крупномасштабных корпоративных приложениях.

Выражаю благодарность за рецензирование статьи эксперту: Рик Баерз (Rick Byers)