Esercitazione: Misurare le prestazioni usando EventCounters in .NET Core
Questo articolo si applica a: ✔️ .NET Core 3.0 SDK e versioni successive
In questa esercitazione si apprenderà come usare un EventCounter per misurare le prestazioni con una frequenza elevata di eventi. È possibile usare i contatori disponibili pubblicati da vari pacchetti .NET Core ufficiali, provider di terze parti o creare metriche personalizzate per il monitoraggio.
Questa esercitazione illustra come:
- Implementare EventSource.
- Monitorare i contatori con dotnet-counters.
Prerequisiti
L'esercitazione usa:
- .NET Core 3.1 SDK o versione successiva.
- dotnet-counters per monitorare i contatori degli eventi.
- Un'app di destinazione di debug di esempio da diagnosticare.
Ottenere l'origine
L'applicazione di esempio verrà usata come base per il monitoraggio. È disponibile il repository ASP.NET Core di esempio nel browser degli esempi. Scaricare il file ZIP, estrarlo dopo averlo scaricato e aprirlo nell'IDE preferito. Compilare ed eseguire l'applicazione per assicurarsi che funzioni correttamente, quindi arrestare l'applicazione.
Implementare EventSource
Per gli eventi che si verificano ogni pochi millisecondi, è preferibile che l'overhead per evento sia basso (inferiore a un millisecondo). In caso contrario, l'impatto sulle prestazioni sarà significativo. Registrare un evento significa scrivere qualcosa su disco. Se il disco non è abbastanza veloce, andranno persi degli eventi. È necessaria una soluzione diversa dalla registrazione dell'evento stesso.
Quando si deve gestire un numero elevato di eventi, anche conoscere la misura per evento non è utile. Nella maggior parte dei casi serve semplicemente recuperare alcune statistiche. Si potrebbero quindi ottenere le statistiche all'interno del processo stesso e quindi scrivere un evento ogni tanto per comunicare le statistiche. Questo è quello che farà EventCounter.
Di seguito è riportato un esempio di come implementare System.Diagnostics.Tracing.EventSource. Creare un nuovo file denominato MinimalEventCounterSource.cs e usare il frammento di codice come origine:
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);
}
}
La riga EventSource.WriteEvent è la parte EventSource e non fa parte di EventCounter. È stata scritta per mostrare che è possibile registrare un messaggio insieme al contatore eventi.
Aggiungere un filtro azioni
Il codice sorgente di esempio è un progetto ASP.NET Core. È possibile aggiungere un filtro azioni a livello globale per registrare il tempo totale della richiesta. Creare un nuovo file denominato LogRequestTimeFilterAttribute.cs e usare il codice seguente:
using System.Diagnostics;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Mvc.Filters;
namespace DiagnosticScenarios
{
public class LogRequestTimeFilterAttribute : ActionFilterAttribute
{
readonly Stopwatch _stopwatch = new Stopwatch();
public override void OnActionExecuting(ActionExecutingContext context) => _stopwatch.Start();
public override void OnActionExecuted(ActionExecutedContext context)
{
_stopwatch.Stop();
MinimalEventCounterSource.Log.Request(
context.HttpContext.Request.GetDisplayUrl(), _stopwatch.ElapsedMilliseconds);
}
}
}
Il filtro azioni avvia uno Stopwatch quando inizia la richiesta e si arresta dopo il completamento, acquisendo il tempo trascorso. I millisecondi totali vengono registrati nell'istanza singleton di MinimalEventCounterSource
. Per applicare questo filtro, è necessario aggiungerlo alla raccolta di filtri. Nel file Startup.cs aggiornare il metodo ConfigureServices
per includere questo filtro.
public void ConfigureServices(IServiceCollection services) =>
services.AddControllers(options => options.Filters.Add<LogRequestTimeFilterAttribute>())
.AddNewtonsoftJson();
Monitorare il contatore eventi
Con l'implementazione in EventSource e il filtro azioni personalizzato, compilare e avviare l'applicazione. La metrica è stata registrata in EventCounter, ma a meno che non si acceda alle statistiche dal contatore, non è utile. Per ottenere le statistiche, è necessario abilitare EventCounter tramite la creazione di un timer che viene attivato con la frequenza con cui si vogliono recuperare gli eventi, nonché un listener per acquisire gli eventi. A tale scopo, è possibile usare dotnet-counters.
Usare il comando dotnet-counters ps per visualizzare un elenco di processi .NET che possono essere monitorati.
dotnet-counters ps
Usando l'identificatore del processo dell'output del comando dotnet-counters ps
, è possibile avviare il monitoraggio del contatore eventi con il comando dotnet-counters monitor
seguente:
dotnet-counters monitor --process-id 2196 --counters Sample.EventCounter.Minimal,Microsoft.AspNetCore.Hosting[total-requests,requests-per-second],System.Runtime[cpu-usage]
Mentre il comando dotnet-counters monitor
è in esecuzione, tenere premuto F5 nel browser per avviare l'invio di richieste continue all'endpoint https://localhost:5001/api/values
. Dopo pochi secondi arrestare l'operazione premendo q
Press p to pause, r to resume, q to quit.
Status: Running
[Microsoft.AspNetCore.Hosting]
Request Rate / 1 sec 9
Total Requests 134
[System.Runtime]
CPU Usage (%) 13
[Sample.EventCounter.Minimal]
Request Processing Time (ms) 34.5
Il comando dotnet-counters monitor
è molto utile per il monitoraggio attivo. Tuttavia, potrebbe essere necessario raccogliere queste metriche di diagnostica per la post-elaborazione e l'analisi. A tale scopo, usare il comando dotnet-counters collect
. Il comando switch collect
è simile al comando monitor
, ma accetta alcuni parametri aggiuntivi. È possibile specificare il nome e il formato del file di output desiderati. Per un file JSON denominato diagnostics.json usare il comando seguente:
dotnet-counters collect --process-id 2196 --format json -o diagnostics.json --counters Sample.EventCounter.Minimal,Microsoft.AspNetCore.Hosting[total-requests,requests-per-second],System.Runtime[cpu-usage]
Ancora una volta, mentre il comando è in esecuzione, tenere premuto F5 nel browser per avviare l'invio di richieste continue all'endpoint https://localhost:5001/api/values
. Dopo pochi secondi arrestare l'operazione premendo q. Verrà scritto il file diagnostics.json. Il file JSON scritto non è tuttavia rientrato; come lo è in questo articolo per una migliore leggibilità.
{
"TargetProcess": "DiagnosticScenarios",
"StartTime": "8/5/2020 3:02:45 PM",
"Events": [
{
"timestamp": "2020-08-05 15:02:47Z",
"provider": "System.Runtime",
"name": "CPU Usage (%)",
"counterType": "Metric",
"value": 0
},
{
"timestamp": "2020-08-05 15:02:47Z",
"provider": "Microsoft.AspNetCore.Hosting",
"name": "Request Rate / 1 sec",
"counterType": "Rate",
"value": 0
},
{
"timestamp": "2020-08-05 15:02:47Z",
"provider": "Microsoft.AspNetCore.Hosting",
"name": "Total Requests",
"counterType": "Metric",
"value": 134
},
{
"timestamp": "2020-08-05 15:02:47Z",
"provider": "Sample.EventCounter.Minimal",
"name": "Request Processing Time (ms)",
"counterType": "Metric",
"value": 0
},
{
"timestamp": "2020-08-05 15:02:47Z",
"provider": "System.Runtime",
"name": "CPU Usage (%)",
"counterType": "Metric",
"value": 0
},
{
"timestamp": "2020-08-05 15:02:48Z",
"provider": "Microsoft.AspNetCore.Hosting",
"name": "Request Rate / 1 sec",
"counterType": "Rate",
"value": 0
},
{
"timestamp": "2020-08-05 15:02:48Z",
"provider": "Microsoft.AspNetCore.Hosting",
"name": "Total Requests",
"counterType": "Metric",
"value": 134
},
{
"timestamp": "2020-08-05 15:02:48Z",
"provider": "Sample.EventCounter.Minimal",
"name": "Request Processing Time (ms)",
"counterType": "Metric",
"value": 0
},
{
"timestamp": "2020-08-05 15:02:48Z",
"provider": "System.Runtime",
"name": "CPU Usage (%)",
"counterType": "Metric",
"value": 0
},
{
"timestamp": "2020-08-05 15:02:50Z",
"provider": "Microsoft.AspNetCore.Hosting",
"name": "Request Rate / 1 sec",
"counterType": "Rate",
"value": 0
},
{
"timestamp": "2020-08-05 15:02:50Z",
"provider": "Microsoft.AspNetCore.Hosting",
"name": "Total Requests",
"counterType": "Metric",
"value": 134
},
{
"timestamp": "2020-08-05 15:02:50Z",
"provider": "Sample.EventCounter.Minimal",
"name": "Request Processing Time (ms)",
"counterType": "Metric",
"value": 0
}
]
}