Udostępnij za pośrednictwem


EventCounters na platformie .NET

Ten artykuł dotyczy: ✔️ .NET Core 3.0 SDK oraz nowszych wersji

Uwaga

W przypadku tworzenia nowych projektów .NET firma Microsoft zaleca zamiast tego korzystanie z nowszych interfejsów API System.Diagnostics.Metrics . Interfejsy API System.Diagnostics.Metrics oferują zwiększoną funkcjonalność, standaryzację i integrację z szerszym ekosystemem narzędzi. Aby uzyskać więcej informacji, zobacz porównanie interfejsu API metryk.

EventCounters to API platformy .NET używane do zbierania metryk wydajności w sposób lekki, międzyplatformowy i niemal w czasie rzeczywistym. EventCounters zostały dodane jako międzyplatformowa alternatywa dla "liczników wydajności" programu .NET Framework w systemie Windows. W tym artykule dowiesz się, czym są liczniki zdarzeń, jak je zaimplementować i jak je obsługiwać.

Środowisko uruchomieniowe platformy .NET i kilka bibliotek platformy .NET publikują podstawowe informacje diagnostyczne przy użyciu usługi EventCounters, począwszy od platformy .NET Core 3.0. Poza elementami EventCounters udostępnianymi przez środowisko uruchomieniowe platformy .NET możesz wdrożyć własne konta zdarzeń. Funkcja EventCounters może służyć do śledzenia różnych metryk. Dowiedz się więcej o nich w dobrze znanych usłudze EventCounters na platformie .NET.

EventCounters są częścią elementu EventSource i są automatycznie wypychane do narzędzi nasłuchujących w regularnych interwałach. Podobnie jak wszystkie inne zdarzenia w obiekcie EventSource, mogą być używane zarówno procesowo, jak i międzyprocesowo za pośrednictwem EventListener i EventPipe. W tym artykule skupiono się na możliwościach obejmujących wiele platform EventCounters i celowo wykluczono PerfView i ETW (Śledzenie zdarzeń w systemie Windows) — chociaż oba te narzędzia można używać z EventCounters.

Obraz diagramu liczników zdarzeń in-proc i out-of-proc

Omówienie interfejsu API EventCounter

Istnieją dwie główne kategorie liczników zdarzeń. Niektóre liczniki są przeznaczone dla wartości "rate", takich jak łączna liczba wyjątków, łączna liczba kontrolerów domeny i łączna liczba żądań. Inne liczniki to wartości chwilowe, takie jak użycie sterty, użycie procesora i rozmiar zestawu roboczego. W każdej z tych kategorii liczników istnieją dwa typy liczników, które różnią się w zależności od ich wartości. Liczniki sondowania pobierają swoją wartość za pośrednictwem wywołania zwrotnego, a liczniki niezwiązane z sondowaniem mają swoje wartości bezpośrednio ustawione w wystąpieniu licznika.

Liczniki są reprezentowane przez następujące implementacje:

Nasłuchiwacz zdarzeń określa, jak długie są interwały pomiarowe. Na końcu każdego interwału wartość jest przesyłana do odbiornika dla każdego licznika. Implementacje licznika określają, jakie interfejsy API i obliczenia są używane do generowania wartości każdego interwału.

  • EventCounter zapisuje zestaw wartości. Metoda EventCounter.WriteMetric dodaje nową wartość do zestawu. W każdym interwale obliczane jest statystyczne podsumowanie zestawu, takie jak minimalna, maksymalna i średnia. Narzędzie dotnet-counters zawsze wyświetla średnią wartość. Element EventCounter jest przydatny do opisania dyskretnego zestawu operacji. Typowe użycie może obejmować monitorowanie średniego rozmiaru w bajtach ostatnich operacji we/wy lub średnią wartość pieniężną zestawu transakcji finansowych.

  • Rejestruje IncrementingEventCounter sumę bieżącą dla każdego interwału czasu. Metoda IncrementingEventCounter.Increment dodaje sumę. Jeśli na przykład Increment() jest wywoływany trzy razy w jednym interwale z wartościami 1, 2, i 5, bieżąca suma 8 zostanie zgłoszona jako wartość licznika dla tego interwału. Narzędzie dotnet-counters wyświetli szybkość jako zarejestrowaną sumę/czas. Jest przydatne narzędzie IncrementingEventCounter do mierzenia, jak często działanie ma miejsce, na przykład liczby przetworzonych żądań na sekundę.

  • Funkcja PollingCounter używa wywołania zwrotnego w celu określenia wartości, która jest zgłaszana. Przy każdym interwale czasu wywoływana jest funkcja wywołania zwrotnego podana przez użytkownika, a wartość zwracana jest używana jako wartość licznika. Element PollingCounter może służyć do wykonywania zapytań dotyczących metryki ze źródła zewnętrznego, na przykład pobierania bieżących wolnych bajtów na dysku. Może również służyć do raportowania niestandardowych statystyk, które można obliczyć na żądanie przez aplikację. Przykłady obejmują raportowanie 95. percentyla ostatnich opóźnień żądań lub obecnego współczynnika trafień lub chybienia pamięci podręcznej.

  • Funkcja IncrementingPollingCounter używa wywołania zwrotnego w celu określenia zgłoszonej wartości przyrostowej. Przy każdym interwale czasu wykonywane jest wywołanie zwrotne, a różnica między bieżącym a ostatnim wywołaniem jest raportowaną wartością. Narzędzie dotnet-counters zawsze wyświetla różnicę jako tempo, zgłoszoną wartość / czas. Ten licznik jest przydatny, gdy nie jest możliwe wywołanie interfejsu API dla każdego wystąpienia, ale można wykonać zapytanie o łączną liczbę wystąpień. Można na przykład zgłosić liczbę bajtów zapisanych w pliku na sekundę, nawet bez powiadomienia za każdym razem, gdy bajt jest zapisywany.

Implementowanie źródła zdarzeń

Poniższy kod implementuje przykład EventSource uwidoczniony jako nazwany "Sample.EventCounter.Minimal" dostawca. To źródło zawiera EventCounter reprezentujący czas przetwarzania żądań. Taki licznik ma nazwę (czyli unikatowy identyfikator w źródle) i nazwę wyświetlaną, które są używane przez narzędzia nasłuchujące, takie jak dotnet-counters.

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 Służy do wyświetlania listy procesów platformy .NET, które można monitorować:

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 Przekaż nazwę do --counters opcji , aby rozpocząć monitorowanie licznika:

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

W poniższym przykładzie przedstawiono dane wyjściowe monitora:

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

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

Naciśnij q , aby zatrzymać polecenie monitorowania.

Liczniki warunkowe

Podczas implementacji EventSource, liczniki mogą być warunkowo tworzone, kiedy metoda EventSource.OnEventCommand jest wywoływana z wartością CommandEventCommand.Enable. Aby bezpiecznie utworzyć wystąpienie licznika tylko wtedy, gdy null, użyj operatora przypisania łączenia wartości null. Ponadto metody niestandardowe mogą oceniać metodę IsEnabled w celu określenia, czy jest włączone bieżące źródło zdarzeń.

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

Wskazówka

Liczniki warunkowe to liczniki, które są warunkowo tworzone jako forma mikrooptymalizacji. Środowisko uruchomieniowe stosuje ten wzorzec dla scenariuszy, w których liczniki są zwykle nieużytowane, aby zapisać ułamek milisekund.

Przykładowe liczniki środowiska uruchomieniowego platformy .NET Core

W środowisku uruchomieniowym platformy .NET Core istnieje wiele doskonałych przykładowych implementacji. Oto implementacja licznika środowiska uruchomieniowego, który śledzi rozmiar zestawu roboczego aplikacji.

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

Raportuje PollingCounter bieżącą ilość pamięci fizycznej przypisanej do procesu (zestaw roboczy) aplikacji, ponieważ przechwytuje metrykę w określonym punkcie czasu. Wywołanie zwrotne do odpytywania wartości to podane wyrażenie lambda, które jest po prostu wywołaniem interfejsu System.Environment.WorkingSet API. DisplayName i DisplayUnits są opcjonalnymi właściwościami, które można ustawić, aby ułatwić użytkownikowi wyświetlanie wartości po stronie licznika. Na przykład dotnet-counters używa tych właściwości do wyświetlania bardziej przystępnej wersji nazw liczników.

Ważne

Właściwości DisplayName nie są zlokalizowane.

W przypadku elementów PollingCounter, i IncrementingPollingCounter, nie należy wykonywać żadnych innych czynności. Obie odpytywują wartości samodzielnie z częstotliwością żądaną przez konsumenta.

Oto przykład licznika środowiska uruchomieniowego zaimplementowanego przy użyciu polecenia IncrementingPollingCounter.

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

Funkcja IncrementingPollingCounter używa interfejsu API Monitor.LockContentionCount do zgłaszania wzrostu ogólnej liczby konfliktów o blokady. Właściwość jest opcjonalna DisplayRateTimeScale , ale jeśli jest używana, może zapewnić wskazówkę dotyczącą interwału czasu, w jakim licznik jest najlepiej wyświetlany. Na przykład liczba rywalizacji o blokadę jest najlepiej wyświetlana jako liczba na sekundę, dlatego DisplayRateTimeScale ustawia się na jedną sekundę. Częstotliwość wyświetlania można dostosować dla różnych typów liczników szybkości.

Uwaga

Element DisplayRateTimeScalenie jest używany przez dotnet-counters, a dla odbiorników zdarzeń nie jest wymagane jego używanie.

Istnieje więcej implementacji liczników do użycia jako punkt odniesienia w repozytorium środowiska uruchomieniowego .NET.

Współbieżność

Wskazówka

Interfejs API EventCounters nie gwarantuje bezpieczeństwa wątków. Gdy delegaci przekazywani do PollingCounter lub IncrementingPollingCounter wystąpienia są wywoływani przez wiele wątków, twoim zadaniem jest zagwarantowanie bezpieczeństwa wątkowego delegatów.

Rozważmy na przykład następujące EventSource do śledzenia żądań.

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

Metodę AddRequest() można wywołać z programu obsługi żądań, a RequestRateCounter wartość sonduje w interwale określonym przez konsumenta licznika. AddRequest() Jednak metoda może być wywoływana przez wiele wątków jednocześnie, umieszczając warunek wyścigu na _requestCount. Bezpieczny wątkowo alternatywny sposób inkrementacji _requestCount to użycie Interlocked.Increment.

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

Aby zapobiec rozdartym odczytom (w architekturach 32-bitowych) pól long_requestCount, użyj Interlocked.Read.

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

Korzystanie z EventCounters

Istnieją dwa podstawowe sposoby korzystania z EventCounters: in-proc i out-of-proc. Użycie EventCounters można rozróżnić na trzy warstwy różnych technologii konsumpcyjnych.

  • Transportowanie zdarzeń w strumieniu nieprzetworzonym za pośrednictwem funkcji ETW lub EventPipe:

    Interfejsy API ETW są dostarczane z systemem operacyjnym Windows, a EventPipe jest dostępny jako interfejs API platformy .NET lub protokół diagnostyczny IPC.

  • Dekodowanie binarnego strumienia zdarzeń na zdarzenia

    Biblioteka TraceEvent obsługuje formaty strumieni ETW i EventPipe.

  • Narzędzia wiersza polecenia i graficznego interfejsu użytkownika:

    Narzędzia takie jak PerfView (ETW lub EventPipe), dotnet-counters (tylko EventPipe) i dotnet-monitor (tylko EventPipe).

Korzystanie z funkcji out-of-proc

Korzystanie z EventCounters w trybie out-of-proc jest powszechnym podejściem. Za pomocą dotnet-counters można ich używać międzyplatformowo przez EventPipe. Narzędzie dotnet-counters jest wieloplatformowym globalnym narzędziem dotnet CLI, które może służyć do monitorowania wartości liczników. Aby dowiedzieć się, jak korzystać z dotnet-counters do monitorowania liczników, zobacz dotnet-counters lub przejdź przez samouczek Measure performance using EventCounters.

Azure Application Insights

Liczniki zdarzeń mogą być konsumowane przez usługę Azure Monitor, w szczególności Azure Application Insights. Liczniki można dodawać i usuwać, a ty możesz określić niestandardowe liczniki lub dobrze znane liczniki. Aby uzyskać więcej informacji, zobacz Dostosowywanie liczników do zbierania danych.

dotnet-monitor

Narzędzie dotnet-monitor ułatwia dostęp do diagnostyki z procesu platformy .NET w sposób zdalny i zautomatyzowany. Oprócz śladów może monitorować metryki, zbierać zrzuty pamięci i zbierać zrzuty GC. Jest dystrybuowany zarówno jako narzędzie CLI (interfejs wiersza polecenia), jak i obraz platformy Docker. Uwidacznia interfejs API REST, a kolekcja artefaktów diagnostycznych odbywa się za pośrednictwem wywołań REST.

Aby uzyskać więcej informacji, zobacz dotnet-monitor.

Korzystanie z narzędzia in-proc

Za pośrednictwem interfejsu EventListener API można odczytać wartości liczników. Element EventListener to sposób korzystania ze wszystkich zdarzeń zapisywanych przez wszystkie wystąpienia EventSource obiektu w aplikacji. Aby uzyskać więcej informacji na temat korzystania z interfejsu EventListener API, zobacz EventListener.

Najpierw należy włączyć EventSource, które odpowiada za generowanie wartości licznika. Zastąp metodę EventListener.OnEventSourceCreated, aby uzyskać powiadomienie, kiedy tworzony jest EventSource, a jeśli jest to właściwy EventSource dla Twoich EventCounters, możesz wywołać na nim EventListener.EnableEvents. Oto przykład override:

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

Przykładowy kod

Oto przykładowa EventListener klasa, która drukuje wszystkie nazwy liczników i wartości z środowiska uruchomieniowego platformy .NET EventSource, aby co sekundę publikować jej liczniki wewnętrzne (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);
    }
}

Jak pokazano powyżej, należy upewnić się, że "EventCounterIntervalSec" argument jest ustawiony w argumencie filterPayload podczas wywoływania elementu EnableEvents. W przeciwnym razie liczniki nie będą w stanie opróżniać wartości, ponieważ nie wiedzą, w jakim interwale powinny być opróżniane.

Zobacz też