EventCounters in .NET

Dieser Artikel gilt für: ✔️ .NET Core 3.0 SDK und neuere Versionen

EventCounters sind .NET-APIs, die für eine schlanke und plattformübergreifende Leistungsmetriksammlung nahezu in Echtzeit verwendet werden. EventCounters wurden als plattformübergreifende Alternative zu den „Leistungsindikatoren“ von .NET Framework unter Windows hinzugefügt. In diesem Artikel erfahren Sie, was EventCounters sind, wie Sie diese implementieren und wie Sie sie nutzen können.

Die .NET-Laufzeit und einige .NET-Bibliotheken veröffentlichen grundlegende Diagnoseinformationen mithilfe von EventCounters ab .NET Core 3.0. Abgesehen von den EventCounters, die von der .NET-Laufzeit bereitgestellt werden, können Sie auch Ihre eigenen EventCounters implementieren. EventCounters kann zum Nachverfolgen verschiedener Metriken verwendet werden. Weitere Informationen finden Sie unter Bekannte EventCounters in .NET.

EventCounters sind Teil einer EventSource und werden in regelmäßigen Abständen automatisch an Listenertools übermittelt. Wie alle anderen Ereignisse für eine EventSource können sie sowohl prozessintern als auch prozessextern über EventListener und EventPipe genutzt werden. Dieser Artikel konzentriert sich auf die plattformübergreifenden Funktionen von EventCounters und schließt PerfView und ETW (Event Tracing for Windows, Ereignisablaufverfolgung für Windows) bewusst aus, obwohl beides mit EventCounters verwendet werden kann.

Abbildung zu EventCounters in-process und out-of-process

Übersicht über die EventCounter-API

Es gibt zwei Hauptkategorien von EventCounters. Einige Leistungsindikatoren beziehen sich auf „Ratenwerte“, etwa auf die Gesamtzahl der Ausnahmen, die Gesamtzahl der GCs und die Gesamtzahl der Anforderungen. Andere Leistungsindikatoren sind „Momentaufnahmewerte“, z. B. Heapverwendung, CPU-Auslastung und Arbeitssatzgröße. Innerhalb jeder dieser Kategorien von Indikatoren gibt es zwei Arten von Indikatoren, die sich dadurch unterscheiden, wie sie ihren Wert ermitteln. Abrufindikatoren erhalten ihren Wert über einen Rückruf, und bei nicht abrufenden Indikatoren wird der Wert direkt für die Instanz des Indikators festgelegt.

Die Indikatoren werden durch die folgenden Implementierungen dargestellt:

Ein Ereignislistener gibt an, wie lang Messintervalle sind. Am Ende jedes Intervalls wird ein Wert an den Listener für jeden Indikator übermittelt. Die Implementierungen eines Indikators bestimmen, welche APIs und Berechnungen verwendet werden, um den Wert für jedes Intervall zu generieren.

  • EventCounter zeichnet einen Satz von Werten auf. Die EventCounter.WriteMetric-Methode fügt dem Satz einen neuen Wert hinzu. Mit jedem Intervall wird eine statistische Zusammenfassung für den Satz berechnet, z. B. Mindestwert, Maximalwert und Mittelwert. Das Tool dotnet-counters zeigt immer den Mittelwert an. EventCounter ist nützlich, um einen diskreten Satz von Vorgängen zu beschreiben. Zu den üblichen Verwendungszwecken kann die Überwachung der durchschnittlichen Größe der jüngsten E/A-Vorgänge in Bytes oder des durchschnittlichen Geldwerts einer Reihe von Finanztransaktionen gehören.

  • IncrementingEventCounter zeichnet eine laufende Summe für jedes Zeitintervall auf. Die IncrementingEventCounter.Increment-Methode fügt der Summe Werte hinzu. Wenn Increment() z. B. drei Mal während eines Intervalls mit den Werten 1, 2 und 5 aufgerufen wird, wird die laufende Summe von 8 als Indikatorwert für dieses Intervall gemeldet. Mit dem Tool dotnet-counters wird die Rate als die aufgezeichnete Summe/Zeit angezeigt. IncrementingEventCounter ist hilfreich, um zu messen, wie häufig eine Aktion ausgeführt wird, z. B. die Anzahl der pro Sekunde verarbeiteten Anforderungen.

  • PollingCounter verwendet einen Rückruf, um den Wert zu bestimmen, der gemeldet wird. Bei jedem Zeitintervall wird die vom Benutzer bereitgestellte Rückruffunktion aufgerufen, und der Rückgabewert wird als Indikatorwert verwendet. PollingCounter kann verwendet werden, um eine Metrik aus einer externen Quelle abzufragen, z. B. zum Abrufen der aktuellen freien Bytes auf einem Datenträger. Der Indikator kann auch verwendet werden, um benutzerdefinierte Statistiken aufzuzeichnen, die bei Bedarf von einer Anwendung berechnet werden können. Beispiele hierfür sind Angaben zum 95. Perzentil der aktuellen Anforderungslatenz oder die aktuelle Treffer- oder Fehlquote eines Caches.

  • IncrementingPollingCounter verwendet einen Rückruf, um den gemeldeten Inkrementwert zu ermitteln. In jedem Zeitintervall wird der Rückruf aufgerufen, dann wird die Differenz zwischen dem aktuellen Aufruf und dem letzten Aufruf als gemeldeter Wert verwendet. Das Tool dotnet-counters zeigt die Differenz immer als Rate an, als aufgezeichneten Wert bzw. als Zeit. Dieser Indikator ist nützlich, wenn es nicht praktikabel ist, eine API für jedes Vorkommen aufzurufen, aber die Möglichkeit besteht, die Gesamtzahl der Vorkommen abzufragen. Sie könnten z. B. die Anzahl der pro Sekunde in eine Datei geschriebenen Bytes aufzeichnen, auch ohne Benachrichtigung bei jedem Schreiben eines Bytes.

Implementieren einer EventSource

Der folgende Code implementiert eine Beispiel-EventSource, die als benannter "Sample.EventCounter.Minimal"-Anbieter zur Verfügung gestellt wird. Diese Quelle enthält ein EventCounter-Element, das die Zeit zur Verarbeitung der Anforderung darstellt. Ein solcher Zähler besitzt einen Namen (d. h. eine eindeutige ID an der Quelle) und einen Anzeigenamen. Beides wird von Listenertools wie dotnet-counters verwendet.

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);
    }
}

Sie verwenden dotnet-counters ps, um eine Liste der .NET-Prozesse anzuzeigen, die überwacht werden können:

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

Übergeben Sie den EventSource-Namen an die --counters-Option, um die Überwachung des Indikators zu starten:

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

Das folgende Beispiel zeigt die Überwachunsausgabe:

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

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

Drücken Sie q, um den Überwachungsbefehl zu beenden.

Bedingte Indikatoren

Beim Implementieren einer EventSourcekönnen die enthaltenen Leistungsindikatoren bedingt instanziiert werden, wenn die EventSource.OnEventCommand-Methode mit dem Wert Command für EventCommand.Enable aufgerufen wird. Um eine Indikatorinstanz nur dann sicher zu instanziieren, wenn sie null ist, verwenden Sie den NULL-Sammeloperator. Darüber hinaus können benutzerdefinierte Methoden die IsEnabled-Methode auswerten, um zu bestimmen, ob die aktuelle Ereignisquelle aktiviert ist.

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);
    }
}

Tipp

Bedingte Indikatoren sind Leistungsindikatoren, die bedingt instanziiert werden: eine Mikrooptimierung. Die Laufzeit übernimmt dieses Muster für Szenarien, in denen normalerweise keine Leistungsindikatoren verwendet werden, um den Bruchteil einer Millisekunde einzusparen.

Beispielindikatoren für die .NET Core-Laufzeit

Es gibt viele hervorragende Beispielimplementierungen in der .NET Core-Laufzeit. Hier sehen Sie die Laufzeitimplementierung für den Leistungsindikator, der die Größe des Arbeitssatzes der Anwendung nachverfolgt.

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

PollingCounter meldet die aktuelle Menge an physischem Arbeitsspeicher, der dem Prozess (Arbeitssatz) der App zugeordnet ist, da eine Metrik zu einem bestimmten Zeitpunkt erfasst wird. Der Rückruf zum Abrufen eines Werts ist der bereitgestellte Lambdaausdruck, bei dem es sich nur um einen Aufrufs der System.Environment.WorkingSet-API handelt. DisplayName und DisplayUnits sind optionale Eigenschaften, die festgelegt werden können, um der Consumerseite des Indikators zu helfen, den Wert deutlicher anzuzeigen. Beispielsweise werden diese Eigenschaften von dotnet-counters zum Anzeigen der anzeigefreundlicheren Version der Indikatornamen verwendet.

Wichtig

Die DisplayName-Eigenschaften sind nicht lokalisiert.

Für PollingCounter und IncrementingPollingCounter sind keine weiteren Aktionen erforderlich. Beide Indikatoren fragen die Werte selbst in einem vom Consumer angeforderten Intervall ab.

Im Folgenden finden Sie ein Beispiel für einen mit IncrementingPollingCounter implementierten Laufzeitindikator.

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

IncrementingPollingCounter verwendet die Monitor.LockContentionCount-API, um das Inkrement der Gesamtzahl der Sperrkonflikte zu melden. Die DisplayRateTimeScale-Eigenschaft ist optional, aber wenn sie verwendet wird, kann Sie einen Hinweis darauf bereitstellen, in welchem Zeitintervall der Indikator am besten angezeigt wird. Beispielsweise wird die Anzahl der Sperrkonflikte am besten als Anzahl pro Sekundeangezeigt, sodass deren DisplayRateTimeScale auf eine Sekunde festgelegt ist. Die Anzeigerate kann für verschiedene Typen von Ratenindikatoren angepasst werden.

Hinweis

DisplayRateTimeScale wird nicht von dotnet-counters verwendet, und Ereignislistener müssen den Indikator nicht verwenden.

Es gibt weitere Indikatorimplementierungen im .NET-Runtime-Repository, die als Referenz verwendet werden können.

Parallelität

Tipp

Die EventCounters-API garantiert keine Threadsicherheit. Wenn die an PollingCounter- oder IncrementingPollingCounter-Instanzen übergebenen Delegaten von mehreren Threads aufgerufen werden, liegt es in Ihrer Verantwortung, die Threadsicherheit der Delegaten zu gewährleisten.

Ziehen Sie z. B. die folgende EventSource in Betracht, um Anforderungen nachzuverfolgen.

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);
    }
}

Die AddRequest()-Methode kann aus einem Anforderungshandler aufgerufen werden, und RequestRateCounter fragt den Wert in dem Intervall ab, das vom Consumer des Indikators angegeben wird. Allerdings kann die AddRequest()-Methode von mehreren Threads gleichzeitig aufgerufen werden, wobei eine Racebedingung für _requestCount festgelegt wird. Eine threadsichere alternative Möglichkeit, _requestCount zu inkrementieren, ist die Verwendung von Interlocked.Increment.

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

Zur Vermeidung unterbrochener Lesevorgänge (in 32-Bit-Architekturen) des long-Felds _requestCount verwenden Sie Interlocked.Read.

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

Nutzen von EventCounters

Es gibt zwei Hauptmethoden, um EventCounters zu nutzen: Prozessintern (In-Process) und Prozessextern (Out-of-Process). Die Nutzung von EventCounters lässt sich in drei Ebenen von verschiedenen genutzten Technologien unterscheiden.

  • Transportereignisse in einem Rohdatenstrom über ETW oder EventPipe:

    ETW-APIs sind im Lieferumfang des Windows-Betriebssystems enthalten, und EventPipe ist als .NET-API oder als diagnostisches IPC-Protokoll verfügbar.

  • Decodieren des binären Ereignisdatenstroms in Ereignisse:

    Die TraceEvent-Bibliothek verarbeitet sowohl das ETW- als auch das EventPipe-Streamformat.

  • Befehlszeilen- und GUI-Tools:

    Tools wie PerfView (ETW oder EventPipe), dotnet-counters (nur EventPipe) und dotnet-monitor (nur EventPipe).

Out-of-Process-Nutzung

Die Out-of-Process-Nutzung von EventCounters ist ein gängiger Ansatz. Sie können dotnet-counters verwenden, um die Indikatoren in plattformübergreifender Weise über eine EventPipe zu nutzen. Das Tool dotnet-counters ist ein plattformübergreifendes globales CLI-Tool, das zum Überwachen der Indikatorwerte verwendet werden kann. Informationen dazu, wie Sie dotnet-counters zum Überwachen Ihrer Leistungsindikatoren verwenden, finden Sie unter dotnet-counters oder im Tutorial Messen der Leistung mithilfe von EventCounters.

dotnet-trace

Das Tool dotnet-trace kann verwendet werden, um die Indikatordaten über eine EventPipe zu nutzen. Im Folgenden finden Sie ein Beispiel für die Verwendung von dotnet-trace zum Erfassen von Indikatordaten.

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

Weitere Informationen zum Erfassen von Indikatorwerten im Zeitverlauf finden Sie in der dotnet-trace-Dokumentation.

Azure Application Insights

EventCounters können von Azure Monitor genutzt werden, insbesondere von Azure Application Insights. Leistungsindikatoren können hinzugefügt und entfernt werden, und Sie können benutzerdefinierte oder bekannte Leistungsindikatoren angeben. Weitere Informationen finden Sie unter Anpassen der zu erfassenden Leistungsindikatoren.

dotnet-monitor

Das dotnet-monitor-Tool erleichtert den Remote- und automatisierten Zugriff auf Diagnosen aus einem .NET-Prozess. Neben der Stapelüberwachung kann es zum Überwachen von Metriken und zum Erfassen von Arbeitsspeicherabbildern und Garbage-Collection-Speicherabbildern verwendet werden. Das Tool wird sowohl als CLI-Tool als auch als Docker-Image bereitgestellt. Es stellt eine REST-API zur Verfügung und die Sammlung von Diagnoseartefakten erfolgt über REST-Aufrufe.

Weitere Informationen finden Sie unter dotnet-monitor.

In-Process-Nutzung

Sie können die Indikatorwerte über die EventListener-API nutzen. Ein EventListener ist eine In-Process-Möglichkeit, alle Ereignisse zu nutzen, die von allen Instanzen einer EventSource in der Anwendung geschrieben werden. Weitere Informationen zur Verwendung der EventListener-API finden Sie unter EventListener.

Zuerst muss die EventSource, die den Indikatorwert generiert, aktiviert werden. Überschreiben Sie die EventListener.OnEventSourceCreated-Methode, um eine Benachrichtigung zu erhalten, wenn eine EventSource erstellt wird. Wenn dies die richtige EventSource für Ihre EventCounters ist, können Sie dafür EventListener.EnableEvents aufrufen. Dies ist eine Beispielüberschreibung:

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"
    });
}

Beispielcode

Im Folgenden sehen Sie eine Beispielklasse EventListener, die alle Indikatornamen und -werte aus der EventSource der .NET-Runtime druckt, um ihre internen Indikatoren (System.Runtime) pro Sekunde zu veröffentlichen.

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);
    }
}

Wie oben gezeigt, müssen Sie sicherstellen, dass beim Aufruf von EnableEvents das "EventCounterIntervalSec"-Argument im Argument filterPayload festgelegt wird. Andernfalls können die Leistungsindikatoren keine Werte ausgeben, da sie nicht wissen, in welchem Intervall Werte ausgegeben werden sollen.

Siehe auch