Diagnosi delle prestazioni

Questa sezione illustra i modi per rilevare i problemi di prestazioni nell'applicazione EF e, dopo aver identificato un'area problematica, come analizzarli ulteriormente per identificare il problema radice. È importante diagnosticare e analizzare attentamente eventuali problemi prima di passare a qualsiasi conclusione ed evitare di presupporre dove si trova la radice del problema.

Identificazione di comandi lenti del database tramite la registrazione

Al termine del giorno, Entity Framework prepara ed esegue i comandi da eseguire sul database; con il database relazionale, ciò significa eseguire istruzioni SQL tramite l'API del database ADO.NET. Se una determinata query richiede troppo tempo (ad esempio, perché manca un indice), è possibile individuarla esaminando i log di esecuzione dei comandi e osservando quanto tempo effettivamente richiedono.

EF semplifica l'acquisizione dei tempi di esecuzione dei comandi tramite registrazione semplice o Microsoft.Extensions.Logging:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder
        .UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Blogging;Trusted_Connection=True;ConnectRetryCount=0")
        .LogTo(Console.WriteLine, LogLevel.Information);
}

Quando il livello di registrazione è impostato su LogLevel.Information, EF genera un messaggio di log per ogni esecuzione del comando con il tempo impiegato:

info: 06/12/2020 09:12:36.117 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (4ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT [b].[Id], [b].[Name]
      FROM [Blogs] AS [b]
      WHERE [b].[Name] = N'foo'

Il comando precedente ha richiesto 4 millisecondi. Se un determinato comando richiede più del previsto, è stato rilevato un possibile colpevole per un problema di prestazioni e può ora concentrarsi su di esso per capire perché è in esecuzione lentamente. La registrazione dei comandi può anche rivelare casi in cui vengono effettuati round trip imprevisti del database; questo verrebbe visualizzato come più comandi in cui è previsto un solo comando.

Avviso

Lasciare abilitata la registrazione dell'esecuzione dei comandi nell'ambiente di produzione è in genere un'idea errata. La registrazione stessa rallenta l'applicazione e può creare rapidamente file di log di grandi dimensioni che possono riempire il disco del server. È consigliabile mantenere l'accesso solo per un breve intervallo di tempo per raccogliere i dati, monitorando attentamente l'applicazione, o per acquisire i dati di registrazione in un sistema di pre-produzione.

Correlazione dei comandi di database alle query LINQ

Un problema con la registrazione dell'esecuzione dei comandi è che a volte è difficile correlare query SQL e query LINQ: i comandi SQL eseguiti da EF possono essere molto diversi dalle query LINQ da cui sono state generate. Per risolvere questa difficoltà, è possibile usare la funzionalità tag di query di Entity Framework, che consente di inserire un piccolo commento che identifica nella query SQL:

var myLocation = new Point(1, 2);
var nearestPeople = (from f in context.People.TagWith("This is my spatial query!")
                     orderby f.Location.Distance(myLocation) descending
                     select f).Take(5).ToList();

Il tag viene visualizzato nei log:

-- This is my spatial query!

SELECT TOP(@__p_1) [p].[Id], [p].[Location]
FROM [People] AS [p]
ORDER BY [p].[Location].STDistance(@__myLocation_0) DESC

Spesso vale la pena contrassegnare le query principali di un'applicazione in questo modo, per rendere i log di esecuzione dei comandi più immediatamente leggibili.

Altre interfacce per l'acquisizione dei dati sulle prestazioni

Esistono diverse alternative alla funzionalità di registrazione di Entity Framework per l'acquisizione dei tempi di esecuzione dei comandi, che possono essere più potenti. I database in genere sono dotati di propri strumenti di analisi delle prestazioni e di traccia, che in genere forniscono informazioni più avanzate e specifiche del database oltre i tempi di esecuzione semplici; la configurazione effettiva, le funzionalità e l'utilizzo variano notevolmente in base ai database.

Ad esempio, SQL Server Management Studio è un potente client in grado di connettersi all'istanza di SQL Server e fornire informazioni preziose sulla gestione e sulle prestazioni. Oltre l'ambito di questa sezione, è possibile esaminare i dettagli, ma due funzionalità che vale la pena menzionare sono Monitoraggio attività, che fornisce un dashboard live dell'attività del server (incluse le query più costose) e la funzionalità Eventi estesi (XEvent), che consente di definire sessioni di acquisizione di dati arbitrarie che possono essere personalizzate in base alle esigenze esatte. La documentazione di SQL Server sul monitoraggio fornisce altre informazioni su queste funzionalità, nonché su altre.

Un altro approccio per l'acquisizione dei dati sulle prestazioni consiste nel raccogliere automaticamente le informazioni generate da Entity Framework o dal driver di database tramite l'interfaccia DiagnosticSource e quindi analizzare i dati o visualizzarli in un dashboard. Se si usa Azure, app Azure lication Insights offre un monitoraggio così potente, integrando le prestazioni del database e i tempi di esecuzione delle query nell'analisi della velocità con cui vengono gestite le richieste Web. Altre informazioni sono disponibili nell'esercitazione sulle prestazioni di Application Insights e nella pagina Analisi SQL di Azure.

Controllo dei piani di esecuzione delle query

Dopo aver rilevato una query problematica che richiede l'ottimizzazione, il passaggio successivo è in genere l'analisi del piano di esecuzione della query. Quando i database ricevono un'istruzione SQL, in genere producono un piano di come deve essere eseguito tale piano; ciò a volte richiede un processo decisionale complicato basato su quali indici sono stati definiti, sulla quantità di dati presenti nelle tabelle e così via. (per inciso, il piano stesso deve essere in genere memorizzato nella cache nel server per ottenere prestazioni ottimali). I database relazionali offrono in genere agli utenti un modo per visualizzare il piano di query, insieme ai costi calcolati per parti diverse della query; questo è prezioso per migliorare le query.

Per iniziare a usare SQL Server, vedere la documentazione sui piani di esecuzione delle query. Il flusso di lavoro di analisi tipico consiste nell'usare SQL Server Management Studio, incollando il codice SQL di una query lenta identificata tramite uno dei mezzi precedenti e producendo un piano di esecuzione grafico:

Display a SQL Server execution plan

Anche se i piani di esecuzione possono sembrare complicati in un primo momento, vale la pena dedicare un po ' di tempo a acquisire familiarità con loro. È particolarmente importante notare i costi associati a ogni nodo del piano e identificare il modo in cui gli indici vengono usati (o meno) nei vari nodi.

Anche se le informazioni precedenti sono specifiche di SQL Server, altri database in genere forniscono lo stesso tipo di strumenti con una visualizzazione simile.

Importante

I database talvolta generano piani di query diversi a seconda dei dati effettivi nel database. Ad esempio, se una tabella contiene solo poche righe, un database può scegliere di non usare un indice in tale tabella, ma di eseguire un'analisi completa della tabella. Se si analizzano piani di query in un database di test, assicurarsi sempre che contenga dati simili al sistema di produzione.

Contatori di eventi

Le sezioni precedenti sono incentrate su come ottenere informazioni sui comandi e sul modo in cui questi comandi vengono eseguiti nel database. Oltre a questo, Entity Framework espone un set di contatori eventi che forniscono informazioni di livello più basso su ciò che accade all'interno di Entity Framework stesso e sul modo in cui viene usata dall'applicazione. Questi contatori possono essere molto utili per diagnosticare problemi di prestazioni e anomalie delle prestazioni specifici, ad esempio problemi di memorizzazione nella cache delle query che causano la ricompilazione costante, perdite dbContext non predisposte e altri.

Per altre informazioni, vedere la pagina dedicata sui contatori eventi di ENTITY.

Benchmarking con EF Core

Alla fine del giorno, a volte è necessario sapere se un particolare modo di scrivere o eseguire una query è più veloce di un altro. È importante non presupporre o specificare la risposta ed è estremamente facile mettere insieme un rapido benchmark per ottenere la risposta. Quando si scrivono benchmark, è consigliabile usare la libreria BenchmarkDotNet nota, che gestisce molti problemi riscontrati dagli utenti durante il tentativo di scrivere i propri benchmark: sono state eseguite alcune iterazioni di riscaldamento? Quante iterazioni viene effettivamente eseguito il benchmark e perché? Si esaminerà ora l'aspetto di un benchmark con EF Core.

Suggerimento

Il progetto benchmark completo per l'origine seguente è disponibile qui. Si consiglia di copiarlo e usarlo come modello per i propri benchmark.

Come scenario di benchmark semplice, è possibile confrontare i diversi metodi seguenti per calcolare la classificazione media di tutti i blog nel database:

  • Caricare tutte le entità, sommare le singole classificazioni e calcolare la media.
  • Come in precedenza, usare solo una query non di rilevamento. Questa operazione dovrebbe essere più veloce, poiché la risoluzione delle identità non viene eseguita e le entità non vengono snapshotzzate ai fini del rilevamento delle modifiche.
  • Evitare di caricare tutte le istanze di entità blog, proiettando solo la classificazione. L'oggetto consente di trasferire le altre colonne non richieste del tipo di entità Blog.
  • Calcolare la media nel database rendendola parte della query. Questo dovrebbe essere il modo più rapido, poiché tutto viene calcolato nel database e solo il risultato viene trasferito al client.

Con BenchmarkDotNet il codice viene scritto come metodo semplice, proprio come uno unit test, e BenchmarkDotNet esegue automaticamente ogni metodo per un numero sufficiente di iterazioni, misurando in modo affidabile il tempo necessario e la quantità di memoria allocata. Ecco i diversi metodi (il codice di benchmark completo è disponibile qui):

[Benchmark]
public double LoadEntities()
{
    var sum = 0;
    var count = 0;
    using var ctx = new BloggingContext();
    foreach (var blog in ctx.Blogs)
    {
        sum += blog.Rating;
        count++;
    }

    return (double)sum / count;
}

I risultati sono riportati di seguito, come stampato da BenchmarkDotNet:

Method Valore medio Errore StdDev Mediana Ratio RatioSD Generazione 0 Gen1 Gen2 Allocato
LoadEntities 2.860.4 54.31 93.68 2.844.5 4.55 0.33 210.9375 70.3125 - 1309,56 KB
LoadEntitiesNoTracking 1.353.0 21.26 18.85 1.355.6 2.10 0,14 87.8906 3.9063 - 540,09 KB
ProjectOnlyRanking 910.9 20.91 61.65 892.9 1,46 0,14 41.0156 0.9766 - 252,08 KB
CalculateInDatabase 627.1 14.58 42.54 noi 626.4 1.00 0,00 4.8828 - - 33,27 KB

Nota

Poiché i metodi creano un'istanza e eliminano il contesto all'interno del metodo, queste operazioni vengono conteggiate per il benchmark, anche se in modo rigoroso non fanno parte del processo di query. Questo non dovrebbe importare se l'obiettivo è confrontare due alternative l'una con l'altra (poiché la creazione e l'eliminazione del contesto sono le stesse) e fornisce una misura più olistica per l'intera operazione.

Una limitazione di BenchmarkDotNet è che misura le prestazioni semplici e a thread singolo dei metodi forniti e pertanto non è particolarmente adatta per gli scenari simultanei di benchmarking.

Importante

Assicurarsi sempre di avere dati nel database simili ai dati di produzione durante il benchmarking. In caso contrario, i risultati del benchmark potrebbero non rappresentare prestazioni effettive nell'ambiente di produzione.