Счетчики событий в .NET

Эта статья относится к: ✔️ пакету SDK для .NET Core 3.0 и более поздних версий

Счетчики событий — это API .NET, которые используются для упрощенного кроссплатформенного сбора метрик производительности практически в реальном времени. Счетчики событий были добавлены как альтернатива счетчикам производительности, которые применялись в .NET Framework на базе Windows. В этой статье вы узнаете, что такое счетчики событий и как их реализовать и использовать.

Начиная с NET Core 3.0 среда выполнения .NET и некоторые библиотеки .NET публикуют основные данные диагностики, используя счетчики событий. Помимо счетчиков событий, предоставляемых средой выполнения .NET, вы можете реализовывать и собственные счетчики событий. Счетчики событий можно использовать для отслеживания различных метрик. Узнайте подробнее о них в статье, посвященной известным счетчикам событий в .NET.

Они являются частью EventSource и передаются в средства прослушивания автоматически и на регулярной основе. Как и все остальные события в EventSource, их можно использовать как внутри, так и вне процессов через EventListener и EventPipe. В этой статье рассматриваются межплатформенные возможности счетчиков событий и намеренно исключены PerfView и ETW (трассировка событий для Windows), хотя обе эти функции можно использовать со счетчиками событий.

EventCounters in-proc and out-of-proc diagram image

Общие сведения об API счетчиков событий

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

Счетчики работают следующим образом:

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

  • Метод EventCounter регистрирует набор значений. Метод EventCounter.WriteMetric добавляет новое значение в набор. В каждом интервале вычисляется статистическая сводка по набору, например минимальное, максимальное и среднее значение. Средство dotnet-counters всегда отображает среднее значение. Метод EventCounter пригодится для описания дискретного набора операций. Общее использование может включать мониторинг среднего размера последних операций ввода-вывода в байтах или среднее денежное значение набора финансовых транзакций.

  • Метод IncrementingEventCounter записывает промежуточные итоги для каждого интервала. Метод IncrementingEventCounter.Increment добавляется к итогу. Например, если Increment() за один и тот же интервал вызывается трижды со значениями 1, 2 и 5, то в качестве значения счетчика для этого интервала будет выводиться промежуточный итог 8. Средство dotnet-counters отображает скорость из расчета общая сумма/время. Метод IncrementingEventCounter позволяет измерять частоту выполнения того или иного действия, например число запросов, обрабатываемых в секунду.

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

  • Метод IncrementingPollingCounter определяет выводимое значение приращения, используя обратный вызов. В каждом интервале выполняется обратный вызов, а разница между текущим и предыдущим вызовами становится отчетным значением. Средство dotnet-counters всегда отображает разницу как скорость из расчета отчетное значение/время. Этот счетчик полезен, если при каждом вхождении вызывать API нецелесообразно, но можно запрашивать общее количество вхождений. Например, можно указывать, сколько байтов записывается в файл в секунду, даже без уведомления о записи каждого байта.

Реализация счетчика событий

В приведенном ниже коде реализуется пример EventSource, предоставляемый как именованный поставщик "Sample.EventCounter.Minimal". Этот источник содержит EventCounter, который представляет время обработки запроса. Счетчик имеет имя (т. е. уникальный идентификатор в источнике) и отображаемое имя, и оба этих имени используют средства прослушивателя, такие как dotnet-counter.

using System.Diagnostics.Tracing;

[EventSource(Name = "Sample.EventCounter.Minimal")]
public sealed class MinimalEventCounterSource : EventSource
{
    public static readonly MinimalEventCounterSource Log = new MinimalEventCounterSource();

    private EventCounter _requestCounter;

    private MinimalEventCounterSource() =>
        _requestCounter = new EventCounter("request-time", this)
        {
            DisplayName = "Request Processing Time",
            DisplayUnits = "ms"
        };

    public void Request(string url, long elapsedMilliseconds)
    {
        WriteEvent(1, url, elapsedMilliseconds);
        _requestCounter?.WriteMetric(elapsedMilliseconds);
    }

    protected override void Dispose(bool disposing)
    {
        _requestCounter?.Dispose();
        _requestCounter = null;

        base.Dispose(disposing);
    }
}

Метод dotnet-counters ps отображает список процессов .NET, которые можно отслеживать:

dotnet-counters ps
   1398652 dotnet     C:\Program Files\dotnet\dotnet.exe
   1399072 dotnet     C:\Program Files\dotnet\dotnet.exe
   1399112 dotnet     C:\Program Files\dotnet\dotnet.exe
   1401880 dotnet     C:\Program Files\dotnet\dotnet.exe
   1400180 sample-counters C:\sample-counters\bin\Debug\netcoreapp3.1\sample-counters.exe

Передайте имя EventSource в параметр --counters, чтобы начать мониторинг счетчика:

dotnet-counters monitor --process-id 1400180 --counters Sample.EventCounter.Minimal

В следующем примере показаны выходные данные мониторинга:

Press p to pause, r to resume, q to quit.
    Status: Running

[Samples-EventCounterDemos-Minimal]
    Request Processing Time (ms)                            0.445

Нажмите q, чтобы остановить мониторинг.

Условные счетчики

При реализации EventSource для соответствующих счетчиков могут быть созданы условные экземпляры при вызове метода EventSource.OnEventCommand со значением Command параметра EventCommand.Enable. Чтобы экземпляр счетчика создавался только в том случае, если он имеет значение null, используйте оператор присваивания объединения со значением NULL. Кроме того, пользовательские методы могут оценивать метод IsEnabled, чтобы определить, включен ли текущий источник событий.

using System.Diagnostics.Tracing;

[EventSource(Name = "Sample.EventCounter.Conditional")]
public sealed class ConditionalEventCounterSource : EventSource
{
    public static readonly ConditionalEventCounterSource Log = new ConditionalEventCounterSource();

    private EventCounter _requestCounter;

    private ConditionalEventCounterSource() { }

    protected override void OnEventCommand(EventCommandEventArgs args)
    {
        if (args.Command == EventCommand.Enable)
        {
            _requestCounter ??= new EventCounter("request-time", this)
            {
                DisplayName = "Request Processing Time",
                DisplayUnits = "ms"
            };
        }
    }

    public void Request(string url, float elapsedMilliseconds)
    {
        if (IsEnabled())
        {
            _requestCounter?.WriteMetric(elapsedMilliseconds);
        }
    }

    protected override void Dispose(bool disposing)
    {
        _requestCounter?.Dispose();
        _requestCounter = null;

        base.Dispose(disposing);
    }
}

Совет

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

Примеры счетчиков в среде выполнения .NET Core

В среде выполнения .NET Core можно привести множество хороших примеров реализации. Ниже показана реализация среды выполнения для счетчика, который отслеживает размер рабочего набора приложения.

var workingSetCounter = new PollingCounter(
    "working-set",
    this,
    () => (double)(Environment.WorkingSet / 1_000_000))
{
    DisplayName = "Working Set",
    DisplayUnits = "MB"
};

Метод PollingCounter сообщает о текущем объеме физической памяти, сопоставленном с процессом (рабочим набором) приложения, так как он фиксирует метрику в определенный момент времени. Обратный вызов для опроса значения — это заданное лямбда-выражение, которое, по сути, представляет собой вызов API System.Environment.WorkingSet. DisplayName и DisplayUnits — необязательные свойства, которые можно задать, чтобы помочь клиентской стороне счетчика отображать значение точнее. Например, dotnet-counters использует эти свойства для более удобного отображения имен счетчиков на экране.

Внимание

Свойства DisplayName не локализуются.

Для PollingCounter и IncrementingPollingCounterне нужно выполнять никаких других действий. Оба они сами опрашивают значения через указанный клиентом интервал.

Ниже приведен пример счетчика среды выполнения, реализованного с помощью IncrementingPollingCounter.

var monitorContentionCounter = new IncrementingPollingCounter(
    "monitor-lock-contention-count",
    this,
    () => Monitor.LockContentionCount
)
{
    DisplayName = "Monitor Lock Contention Count",
    DisplayRateTimeScale = TimeSpan.FromSeconds(1)
};

IncrementingPollingCounter использует API Monitor.LockContentionCount для сообщения о приращении общего количества конфликтов блокировок. Свойство DisplayRateTimeScale необязательное, но может подсказать, какой интервал лучше выбрать для счетчика. Например, число конфликтов блокировки лучше отображать как количество в секунду, поэтому для параметра DisplayRateTimeScale этого счетчика устанавливается значение одна секунда. Частоту отображения данных можно настраивать для различных типов счетчиков скорости.

Примечание.

Параметр DisplayRateTimeScaleне используется средством dotnet-counters, а прослушиватели событий не обязаны его использовать.

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

Параллелизм

Совет

API счетчиков событий не гарантирует безопасность потоков. Когда делегаты, передаваемые в экземпляры PollingCounter или IncrementingPollingCounter, вызываются несколькими потоками, за безопасность потоков таких делегатов отвечаете вы.

Например, рассмотрим следующий EventSource для отслеживания запросов.

using System;
using System.Diagnostics.Tracing;

public class RequestEventSource : EventSource
{
    public static readonly RequestEventSource Log = new RequestEventSource();

    private IncrementingPollingCounter _requestRateCounter;
    private long _requestCount = 0;

    private RequestEventSource() =>
        _requestRateCounter = new IncrementingPollingCounter("request-rate", this, () => _requestCount)
        {
            DisplayName = "Request Rate",
            DisplayRateTimeScale = TimeSpan.FromSeconds(1)
        };

    public void AddRequest() => ++ _requestCount;

    protected override void Dispose(bool disposing)
    {
        _requestRateCounter?.Dispose();
        _requestRateCounter = null;

        base.Dispose(disposing);
    }
}

Метод AddRequest() может вызываться из обработчика запросов, а RequestRateCounter опрашивает значение с интервалом, указанным клиентом счетчика. Однако метод AddRequest() могут вызываться одновременно несколько потоков, создавая для _requestCount состояние гонки. Альтернативный, поточно-ориентированный способ приращения _requestCount — использовать Interlocked.Increment.

public void AddRequest() => Interlocked.Increment(ref _requestCount);

Чтобы предотвратить разорванные операции чтения (в 32-разрядных архитектурах) поля _requestCount типа long, используйте Interlocked.Read.

_requestRateCounter = new IncrementingPollingCounter("request-rate", this, () => Interlocked.Read(ref _requestCount))
{
    DisplayName = "Request Rate",
    DisplayRateTimeScale = TimeSpan.FromSeconds(1)
};

Использование счетчиков событий

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

  • Транспортировка событий в поток необработанных данных через ETW или EventPipe:

    ETW API входят с ОС Windows, а EventPipe доступен как .NET API или как диагностический протокол IPC.

  • Декодирование потока двоичных событий в события:

    Библиотека TraceEvent обрабатывает оба формата потоковой передачи, ETW и EventPipe.

  • Средства командной строки и графического интерфейса:

    Такие средства, как PerfView (ETW или EventPipe), dotnet-counters (только EventPipe) и dotnet-monitor (только EventPipe).

Использование вне процессов

Чаще всего счетчики событий используются вне процессов. Для получения данных в кроссплатформенном режиме через EventPipe можно использовать dotnet-counters. Средство dotnet-counters — это кроссплатформенный глобальный инструмент dotnet CLI, который можно использовать для отслеживания значений счетчиков. Чтобы узнать, как использовать dotnet-counters для мониторинга счетчиков, ознакомьтесь с информацией о dotnet-counters или изучите руководство Измерение производительности с помощью счетчиков событий.

dotnet-trace

Средство dotnet-trace можно использовать для получения данных счетчика через EventPipe. Ниже приведен пример использования dotnet-trace для получения данных счетчика.

dotnet-trace collect --process-id <pid> Sample.EventCounter.Minimal:0:0:EventCounterIntervalSec=1

Дополнительные сведения о том, как получать значения счетчиков в перспективе, см. в документации по dotnet-trace.

Azure Application Insights

Счетчики событий можно использовать с помощью Azure Monitor, а именно — Application Insights Azure. Счетчики можно добавлять и удалять, а также выбирать пользовательские или популярные счетчики. Дополнительные сведения см. в статье Настройка счетчиков для получения данных.

dotnet-monitor

Это dotnet-monitor средство упрощает доступ к диагностика из процесса .NET в удаленном и автоматизированном режиме. Наряду с трассировкой оно может отслеживать метрики и собирать дампы памяти и дампы сборки мусора. Оно распространяется как в виде средства командной строки, так и в виде образа Docker. Оно предоставляет REST API, а сбор артефактов диагностики осуществляется с помощью вызовов REST.

Дополнительные сведения см. в dotnet-monitor.

Использование внутри процесса

Значения счетчика можно использовать через API EventListener. EventListener — это внутрипроцессный способ использования любых событий, записанных всеми экземплярами EventSource в приложении. Дополнительные сведения об использовании API EventListener см. в статье EventListener.

Сначала необходимо включить EventSource, который создает значение счетчика. Переопределите метод EventListener.OnEventSourceCreated так, чтобы получать уведомление при создании EventSource, и, если это правильный метод EventSource со счетчиками событий, вы сможете вызывать для него EventListener.EnableEvents. Пример переопределения:

protected override void OnEventSourceCreated(EventSource source)
{
    if (!source.Name.Equals("System.Runtime"))
    {
        return;
    }

    EnableEvents(source, EventLevel.Verbose, EventKeywords.All, new Dictionary<string, string>()
    {
        ["EventCounterIntervalSec"] = "1"
    });
}

Пример кода

Ниже приведен пример класса EventListener, который выводит все имена и значения счетчиков из класса EventSource среды выполнения .NET для публикации внутренних счетчиков (System.Runtime) каждую секунду.

using System;
using System.Collections.Generic;
using System.Diagnostics.Tracing;

public class SimpleEventListener : EventListener
{
    public SimpleEventListener()
    {
    }

    protected override void OnEventSourceCreated(EventSource source)
    {
        if (!source.Name.Equals("System.Runtime"))
        {
            return;
        }

        EnableEvents(source, EventLevel.Verbose, EventKeywords.All, new Dictionary<string, string>()
        {
            ["EventCounterIntervalSec"] = "1"
        });
    }

    protected override void OnEventWritten(EventWrittenEventArgs eventData)
    {
        if (!eventData.EventName.Equals("EventCounters"))
        {
            return;
        }

        for (int i = 0; i < eventData.Payload.Count; ++ i)
        {
            if (eventData.Payload[i] is IDictionary<string, object> eventPayload)
            {
                var (counterName, counterValue) = GetRelevantMetric(eventPayload);
                Console.WriteLine($"{counterName} : {counterValue}");
            }
        }
    }

    private static (string counterName, string counterValue) GetRelevantMetric(
        IDictionary<string, object> eventPayload)
    {
        var counterName = "";
        var counterValue = "";

        if (eventPayload.TryGetValue("DisplayName", out object displayValue))
        {
            counterName = displayValue.ToString();
        }
        if (eventPayload.TryGetValue("Mean", out object value) ||
            eventPayload.TryGetValue("Increment", out value))
        {
            counterValue = value.ToString();
        }

        return (counterName, counterValue);
    }
}

Как показано выше, необходимо убедиться, что аргумент "EventCounterIntervalSec" задан в аргументе filterPayload при вызове EnableEvents. В противном случае счетчики не смогут сбрасывать значения, так как не будут знать, с каким интервалом нужно выполнять сброс.

См. также