Partilhar via


Contadores de eventos no .NET

Este artigo aplica-se a: ✔️ SDK do .NET Core 3.0 e versões posteriores

Nota

Para desenvolver novos projetos .NET, a Microsoft recomenda o uso das APIs System.Diagnostics.Metrics mais recentes. As APIs System.Diagnostics.Metrics oferecem maior funcionalidade, padronização e integração com um ecossistema mais amplo de ferramentas. Consulte a comparação da API de métricas para obter mais informações.

EventCounters são APIs .NET usadas para a recolha de métricas de desempenho leves, multiplataforma e quase em tempo real. EventCounters foram adicionados como uma alternativa multiplataforma para os "contadores de desempenho" do .NET Framework no Windows. Neste artigo, você aprenderá o que são EventCounters, como implementá-los e como consumi-los.

O runtime do .NET e algumas bibliotecas .NET publicam informações básicas de diagnóstico usando os EventCounters a partir do .NET Core 3.0. Além dos EventCounters que são fornecidos pelo tempo de execução do .NET, você pode optar por implementar seus próprios EventCounters. Os contadores de eventos podem ser usados para controlar várias métricas. Saiba mais sobre eles em EventCounters bem conhecidos no .NET.

Os EventCounters vivem como parte de um EventSource, e são automaticamente enviados para ferramentas de ouvinte regularmente. Como todos os outros eventos em um EventSource, eles podem ser consumidos dentro e fora do proc via EventListener e EventPipe. Este artigo se concentra nos recursos de plataforma cruzada de EventCounters e exclui intencionalmente PerfView e ETW (Event Tracing for Windows) - embora ambos possam ser usados com EventCounters.

Imagem do diagrama in-proc e out-of-proc dos EventCounters

Visão geral da API EventCounter

Há duas categorias principais de EventCounters. Alguns contadores são para valores de "taxa", como número total de exceções, número total de GCs e número total de solicitações. Outros contadores são valores de "instantâneo", como uso de heap, uso da CPU e tamanho do conjunto de trabalho. Dentro de cada uma dessas categorias de contadores, existem dois tipos de contadores que variam de acordo com a forma como obtêm seu valor. Os contadores de sondagem recuperam o valor por meio de uma função de retorno, e os contadores sem sondagem têm os seus valores definidos diretamente na instância do contador.

Os contadores são representados pelas seguintes implementações:

Um listener de eventos especifica qual é a duração dos intervalos de medição. No final de cada intervalo, um valor é transmitido ao ouvinte para cada contador. As implementações de um contador determinam quais APIs e cálculos são usados para produzir o valor de cada intervalo.

  • O EventCounter registra um conjunto de valores. O EventCounter.WriteMetric método adiciona um novo valor ao conjunto. Com cada intervalo, um resumo estatístico para o conjunto é calculado, como o min, max e média. A ferramenta dotnet-counters sempre exibirá o valor médio. O EventCounter é útil para descrever um conjunto discreto de operações. O uso comum pode incluir o monitoramento do tamanho médio em bytes de operações recentes de E/S ou do valor monetário médio de um conjunto de transações financeiras.

  • O IncrementingEventCounter registra um total de execução para cada intervalo de tempo. O IncrementingEventCounter.Increment método aumenta o total. Por exemplo, se Increment() for chamado três vezes durante um intervalo com valores 1, 2 e 5, então o total acumulado de 8 será relatado como o valor do contador para este intervalo. A ferramenta dotnet-counters exibirá a taxa como o total / tempo gravado. O IncrementingEventCounter é útil para medir a frequência com que uma ação está ocorrendo, como o número de solicitações processadas por segundo.

  • O PollingCounter usa uma função de retorno para determinar o valor relatado. Com cada intervalo de tempo, a função de retorno de chamada fornecida pelo usuário é invocada e o valor de retorno é usado como o valor do contador. A PollingCounter pode ser usado para consultar uma métrica de uma fonte externa, por exemplo, obtendo os bytes livres atuais em um disco. Ele também pode ser usado para relatar estatísticas personalizadas que podem ser calculadas sob demanda por um aplicativo. Os exemplos incluem relatar o percentil 95 de latências de solicitação recentes ou a taxa atual de acertos ou erros de um cache.

  • O IncrementingPollingCounter utiliza um callback para determinar o valor do incremento relatado. A cada intervalo de tempo, a função de retorno é invocada e, em seguida, a diferença temporal entre a invocação atual e a última invocação é o valor relatado. A ferramenta dotnet-counters sempre exibirá a diferença como uma taxa, o valor / tempo relatado. Esse contador é útil quando não é viável chamar uma API em cada ocorrência, mas é possível consultar o número total de ocorrências. Por exemplo, você pode relatar o número de bytes gravados em um arquivo por segundo, mesmo sem uma notificação cada vez que um byte é gravado.

Implementar um Event Source

O código a seguir implementa um exemplo EventSource exposto como o provedor nomeado "Sample.EventCounter.Minimal" . Esta fonte contém um EventCounter que representa o tempo de processamento de solicitação. Este contador tem um nome (ou seja, o seu ID exclusivo na fonte) e um nome de exibição, ambos usados por ferramentas de escuta, como 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);
    }
}

Você usa dotnet-counters ps para exibir uma lista de processos .NET que podem ser monitorados:

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

Passe o EventSource nome para a opção --counters para começar a monitorizar o contador:

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

O exemplo a seguir mostra a saída do monitor:

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

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

Pressione q para parar o comando de monitoramento.

Contadores condicionais

Ao implementar um EventSource, os contadores associados podem ser instanciados condicionalmente quando o método EventSource.OnEventCommand é chamado com o valor Command de EventCommand.Enable. Para instanciar com segurança uma instância de contador somente se ela for null, use o operador de atribuição de coalescência nula. Além disso, os métodos personalizados podem avaliar o IsEnabled método para determinar se a fonte de eventos atual está habilitada ou não.

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

Gorjeta

Contadores condicionais são contadores que são instanciados condicionalmente, uma micro-otimização. O tempo de execução adota esse padrão para cenários em que os contadores normalmente não são usados, para salvar uma fração de milissegundo.

Contadores de exemplo de tempo de execução do .NET Core

Há muitos exemplos excelentes de implementações no tempo de execução do .NET Core. Aqui está a implementação de execução do contador que rastreia o tamanho do conjunto de trabalho da aplicação.

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

O PollingCounter relata a quantidade atual de memória física mapeada para o processo (conjunto de trabalho) do aplicativo, uma vez que captura uma métrica em um momento no tempo. A função de retorno para consultar um valor é a expressão lambda fornecida, que simplesmente realiza uma chamada para a API System.Environment.WorkingSet. DisplayName e DisplayUnits são propriedades opcionais que podem ser definidas para ajudar o lado do consumidor do contador a exibir o valor com mais clareza. Por exemplo, dotnet-counters usa essas propriedades para exibir a versão mais amigável dos nomes de contadores.

Importante

As DisplayName propriedades não foram localizadas.

Para o PollingCounter, e para o IncrementingPollingCounter, nada mais precisa ser feito. Ambos consultam os valores num intervalo solicitado pelo consumidor.

Aqui está um exemplo de um contador de tempo de execução implementado usando IncrementingPollingCounter.

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

O IncrementingPollingCounter utiliza a Monitor.LockContentionCount API para relatar o aumento na contagem total de contenções de bloqueio. A DisplayRateTimeScale propriedade é opcional, mas quando usada pode fornecer uma dica para qual intervalo de tempo o contador é melhor exibido. Por exemplo, a contagem de contenção de bloqueio é melhor exibida como contagem por segundo, portanto, é DisplayRateTimeScale definida como um segundo. A taxa de exibição pode ser ajustada para diferentes tipos de contadores de taxa.

Nota

O DisplayRateTimeScalenão é usado pelo dotnet-counters, e os ouvintes de eventos não são obrigados a utilizá-lo.

Há mais implementações de contador para usar como referência no repositório do runtime .NET.

Concorrência

Gorjeta

A API EventCounters não garante a segurança do thread. Quando os delegados passados para as instâncias PollingCounter ou IncrementingPollingCounter são invocados por múltiplos threads, é sua responsabilidade garantir a segurança em termos de threads dos delegados.

Por exemplo, considere o seguinte EventSource para acompanhar as solicitações.

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

O AddRequest() método pode ser chamado a partir de um manipulador de solicitação, e o RequestRateCounter sonda o valor do contador ao intervalo especificado pelo utilizador do contador. No entanto, o método AddRequest() pode ser chamado por vários encadeamentos ao mesmo tempo, colocando uma condição de corrida em _requestCount. Uma maneira alternativa segura para as threads para incrementar o _requestCount é usar o Interlocked.Increment.

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

Para evitar leituras rasgadas (em arquiteturas de 32 bits) do campo long, _requestCount utilize Interlocked.Read.

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

Consumir EventCounters

Há duas maneiras principais de consumir EventCounters: in-proc e out-of-proc. O consumo de EventCounters pode ser distinguido em três camadas de várias tecnologias de consumo.

  • Transporte de eventos num fluxo cru através de ETW ou EventPipe.

    As APIs ETW vêm com o sistema operacional Windows e o EventPipe é acessível como uma API .NET ou o protocolo IPC de diagnóstico.

  • Decodificando o fluxo de eventos binários em eventos:

    A biblioteca TraceEvent lida com os formatos de fluxo ETW e EventPipe.

  • Ferramentas de linha de comando e GUI:

    Ferramentas como PerfView (ETW ou EventPipe), dotnet-counters (somente EventPipe) e dotnet-monitor (somente EventPipe).

Consumir fora de processo

Consumir EventCounters fora do proc é uma abordagem comum. Você pode usar dotnet-counters para consumi-los de forma transversal a plataformas por meio de um EventPipe. A dotnet-counters ferramenta é uma ferramenta global da CLI dotnet multiplataforma que pode ser usada para monitorar os valores do contador. Para saber como usar dotnet-counters para monitorizar os contadores, consulte dotnet-counters ou siga o tutorial Medir desempenho usando Contadores de Eventos.

Azure Application Insights

Os Contadores de Eventos podem ser consumidos pelo Azure Monitor, especificamente pelo Azure Application Insights. Contadores podem ser adicionados e removidos, e você é livre para especificar contadores personalizados ou contadores conhecidos. Para obter mais informações, consulte Personalização de contadores a recolher.

Dotnet-Monitor

A dotnet-monitor ferramenta facilita o acesso ao diagnóstico de um processo .NET de forma remota e automatizada. Além de rastreamentos, ele pode monitorar métricas, coletar despejos de memória e coletar despejos de GC. Ele é distribuído como uma ferramenta CLI e uma imagem docker. Ele expõe uma API REST e a coleção de artefatos de diagnóstico ocorre por meio de chamadas REST.

Para obter mais informações, consulte dotnet-monitor.

Consumir in-processo

Você pode consumir os valores do contador por meio da EventListener API. Um EventListener é uma maneira in-proc de consumir quaisquer eventos escritos por todas as instâncias de um EventSource na sua aplicação. Para obter mais informações sobre como usar a EventListener API, consulte EventListener.

Primeiro, o EventSource que produz o valor do contador precisa ser habilitado. Substituir o método EventListener.OnEventSourceCreated para obter uma notificação quando um EventSource for criado e, se este for o correto EventSource com os seus EventCounters, então você pode chamar EventListener.EnableEvents sobre ele. Aqui está um exemplo de substituição:

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

Código de exemplo

Aqui está uma classe de exemplo EventListener que imprime todos os nomes de contadores e valores do tempo de execução EventSource do .NET, para publicar os seus contadores internos (System.Runtime) em cada segundo.

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

Como mostrado acima, você deve certificar-se de que o argumento "EventCounterIntervalSec" está definido no argumento filterPayload ao invocar EnableEvents. Caso contrário, os contadores não serão capazes de esvaziar os valores, uma vez que não sabem em que intervalo eles devem ser esvaziados.

Consulte também