Drive your career as React Developer with Symphony Solutions!
×Закрыть

Подходы к диагностированию на .NET Core

Привет всем, меня зовут Игорь Фесенко, я работаю Application Architect в SoftServe Inc. и являюсь Microsoft® Most Valuable Professional. В работе я постоянно ищу пути для улучшения и упрощения уже написанного или еще не написанного исходного кода. В данной статье хотел бы поговорить про диагностирование .NET приложений, в частности .NET Core.

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

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

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

Для начала, давайте разберемся с основными примитивами, которые повсеместно используются в контексте диагностирования:

  • События (events) — то, что произошло, обычно сообщение и связанные с ним данные, например Старт запроса, путь: /индекс, параметры запроса: год=2020
  • Метрики (metrics) — числа, которые представляют текущее состояния объекта, например, количество запросов в секунду: 100, использование ЦПУ: 40%
  • Дампы (dumps) — содержимое рабочей памяти процесса в определенный момент времени, так же включает дополнительную информацию о состоянии программы или системы.

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

ILogger

Я думаю, что с этим подходом знаком каждый, потому что это стандартный механизм, который представляет интерфейс для структурированной записи сообщений. Сложно сделать идеальный подход, который будет подходить всем разработчикам, но мне данный подход и реализация нравится, и я считаю его оптимальным. Не нужно каждый раз на новом проекте тратить время, на то, чтоб решить с командой, а как же все-таки мы будем писать сообщения и какой интерфейс будем реализовывать. Так же, ILogger может быть легко встроен в вашу библиотеку (нужно подключить Microsoft.Extensions.Logging.Abstractions зависимость и использовать ILogger в вашем коде) и конечные пользователи смогут легко подставить нужную им реализацию, подходящую для их системы.

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

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

Если посмотреть на этот подход, то можно провести параллель с ранее доступным и знакомым решением, таким как EventSource.

EventSource

Давайте, немного поговорим про данный класс и его назначение. Назначение EventSource — это дать возможность разработчику легко создать строго типизированную спецификацию для записи (чаще всего большого объема) строго типизированных событий, которые могут быть прочитаны из других процессов, например, Event Tracing for Windows (ETW) или LTTng для Linux. И как всегда, мы говорим про компромиссы, и как вы понимаете, что за возможность писать диагностические сообщение сверх быстро мы расплачиваемся гибкостью и удобством. Мне приходилось использовать данный подход на разных версиях .NET и если вам придётся использовать этот подход, то возможно вы столкнетесь с этой проблемой так же.

Когда вы будете реализовывать свой класс и наследоваться от EventSource вы можете увидеть, что разные источники приводят информацию используя разные пространства имен: Microsoft.Diagnostics.Tracing.EventSource или System.Diagnostics.Tracing.EventSource. Решение довольно простое — если вы используете версию .NET Framework начиная с 4.6 (технически можно с 4.5, но в версии 4.6 были добавлены новые возможности, которые довольно полезны) то тогда используйте встроенный System.Diagnostics.Tracing.EventSource, иначе необходимо подключить зависимость.

Следующее, что я рекомендую сделать — это запланировать и провести дизайн-сессию посвященную информации, которая вам правда полезна и нужна и требует, чтоб её записывали. Цель этой сессии — необходимость определить возможные сообщения и их формат еще задолго до того, как вы сможете сделать первую запись диагностического сообщения. Поэтому EventSource отлично подходит, когда вы хотите записать данные с информацией низкого уровня, чтобы просмотреть ее позже и, скорее всего, скомпилировать в какую-то статистику или сводку.

Например, вы хотите сохранить что-то вроде «прочитано с диска 4096 байт данных» или «файл загружен за 5.6 мс». В дальнейшем эту информацию можно агрегировать, например «150 мс потрачено на операции ввод-вывода файловой системы», или детально проанализировать связанные события, если нужно найти отклонения. Огромный плюс данного подхода — это то, что сообщения можно читать с других внешних процессов. Внутри .NET Framework очень активно используется данный подход, прекрасный пример, возможность получить события (Runtime Events) исполняющей среды (CLR). Начиная с версии .NET Core выше 2.1 это доступно кроссплатформенно. Для этого достаточно реализовать и зарегистрировать EventListener (пример кода).

DiagnosticSource

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

Для решения данной проблемы как раз хорошо подходит DiagnosticSource.

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

Первое, что нужно сделать, то это реализовать класс подписчик. Для того, кто сталкивался с библиотекой Reactive Extensions, интерфейс, который нужно реализовать, будет больше чем понятный. Если вы не встречали ранее или не использовали, то не переживайте, особых проблем не будет. Нам нужно реализовать всего один метод OnNext в каждом из двух интерфейсов: в первом случае, IObserver для того что мы могли подписаться только на нужные нам события, и во втором случае — интерфейс IObserver> необходимый для того чтоб описать желаемую логику при получении самих событий. Можно объединить реализации для двух интерфейсов и разместить всё в одном классе:

public sealed class HttpCoreDiagnosticSourceListener : IObserver<DiagnosticListener>, IObserver<KeyValuePair<string, object>>
    {
        public void OnCompleted() { }
        public void OnError(Exception error) { }
        
        public void OnNext(DiagnosticListener listener)
        {
            if (listener.Name == "HttpHandlerDiagnosticListener")
            {
                listener.Subscribe(this);
            }
        }
        public void OnNext(KeyValuePair<string, object> item)
        {
            Console.Write($"Event: {item.Key} ");
            if (Activity.Current != null)
            {
                Console.Write($"ActivityName: {Activity.Current?.OperationName} Id: {Activity.Current?.Id} ");
            }
            Console.WriteLine();
            Console.WriteLine(JsonSerializer.Serialize(item.Value));
            Console.WriteLine();
        }
    }

в первом методе OnNext мы описываем, что хотим подписаться только на события, приходящие от HttpHandlerDiagnosticListener, и во втором OnNext реализовываем логику обработки всех событий, на которые мы подписались.

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

using var subscription = DiagnosticListener.AllListeners.Subscribe(new HttpCoreDiagnosticSourceListener());
// no changes
await new HttpClient().GetAsync("https://example.com");

Также вы можете просмотреть полный пример исходного кода.

Данный подход, дает возможность конечному пользователю решить, что он хочет делать с информацией и как ее отображать. Но тут скрывается один неприятный побочный эффект, а именно возможности изменять состояние объекта и нужно быть очень осторожным с этим. Чтобы защитить себя можно использовать адаптер, для работы которого необходимо подключить зависимость Microsoft.Extensions.DiagnosticAdapter. Соответственно в вашем коде, может быть и как ILogger так и DiagnosticSource и что самое главное, то интерфейс последнего содержит всего лишь два метода:

bool IsEnabled(string name)
void Write(string name, object value);

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

Дополнительно, вы еще можете увидеть один класс, про который некоторые разработчики забывают, а именно System.Diagnostics.Activity. Данный объект — это решение, которое позволяет идентифицировать и сопоставлять сообщения из разных сервисов.

.NET Core runtime diagnostic tools

И напоследок, я хотел бы рассмотреть еще один подход для сбора диагностической информации с уже существующего приложения во времени его выполнения. С моей практики, наличие хорошего набора инструментов может очень сильно помочь вам в поиске проблемы. Я думаю, что вы согласны со мной, что исправление дефекта чаще всего состоит на 80% из времени необходимого на поиск самой проблемы. Поэтому я хотел бы обратить внимание на полезный набор утилит командной строки (.NET Core CLI runtime diagnostic tools). Если конечное приложение версии .NET Core 3.0 и выше, то тогда мы сможем получать много диагностической информации без необходимости модифицировать исходный код приложения или перезапускать уже исполняемый процесс.

Основной набор инструментов, который упрощает диагностику и решение проблем с производительностью, поиском утечки памяти, высокой загрузки ЦПУ, взаимоблокировок (deadlocks):

dotnet-counters — инструмент командной строки для наблюдения за метриками, генерируемыми приложениями .NET Core в режиме реального времени (смотреть здесь).

dotnet-trace — инструмент командной строки для просмотра сообщений, которые отправляются .NET Core приложением. Реализовано с помощью нового кроссплатформенного EventPipe API, которое включено по умолчанию для каждого .NET Core приложения (смотреть здесь).

dotnet-dump — инструмент, который позволяет собирать и анализировать дампы процессов (смотреть здесь).

Так же рекомендую посмотреть использование EventPipe API и DiagnosticClient API, используя которые вы можете написать свой инструмент командной строки для решения ваших специфических задач диагностики работы приложения. В примере происходит подключение к внешнему процессу и старт мониторинг событий сборщика мусора (Garbage Collector).

Полезные ссылки

  1. EventSource User’s Guide
  2. EventRegister User’s Guide
  3. DiagnosticSource User’s Guide
  4. Activity User’s Guide
  5. .NET Core Runtime Diagnostic Tools
  6. DiagnosticSource User’s Guide
LinkedIn
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Несколько опечаток

которые довольны полезны
в сообщении, что ускорить поиск ошибки
Reactive Extensions интерфейс будет больше чем понятный
два интерефейса
интерфейс IObserver> необходимый для того

(что-то потерялось)

Спасибо большое, Игорь! Поправил.

Спасибо! Ещё я бы упомянул AppInsights, который унифицирует tracing, logging и метрики (включая пользовательские), но это уже может вырасти в серию других статей.

Мне интересно — есть ли альтернативы AppInsights с таким же набором возможностей для .NET? OpenTelemetry ещё сырой, Elastic Stack не поддерживает пользовательские метрики... В идеале, хочется иметь возможность развернуть свой on prem AppInsights, т.к. не все клиенты готовы отправлять телеметрию наружу.

Абсолютно согласен, Антон! Application Insights и другие схожие продукты/сервисы вобрали в себя много из описаных практик и постоянно эволюционируют. Поэтому это точно может выливаться в отдельный цикл статей. Кстати если интересно, то у меня был небольшой вводный доклад про мониторинг (правда давно, но некоторые вещи остались прежними), презентация тут 1drv.ms/...​1kgIxHUjuNfNE_1PZAWuz27DA и видео запись тут www.youtube.com/watch?v=1OZNgD_eIqU.

Все проекты, в которых я участвовал и где требовалось, чтоб диагностические записи не уходили за пределы окружения, использовался ELK стек с кучей надстроек. Из сервисов я знаю, что есть такие как sematext.com/enterprise, но опыта использования на реальных проектах у меня нет.

Поэтому и особо выбирать нам и не с чего, поэтому и появляется много самописных решений.

Подписаться на комментарии