Partilhar via


Diagnóstico de Desempenho

Esta seção discute maneiras de detetar problemas de desempenho em seu aplicativo EF e, uma vez identificada uma área problemática, como analisá-los ainda mais para identificar o problema raiz. É importante diagnosticar e investigar cuidadosamente quaisquer problemas antes de tirar conclusões precipitadas, e evitar assumir onde está a raiz do problema.

Identificando comandos lentos do banco de dados por meio do registro em log

No final do dia, o EF prepara e executa comandos a serem executados em seu banco de dados; com o banco de dados relacional, isso significa executar instruções SQL por meio da API do banco de dados ADO.NET. Se uma determinada consulta está demorando muito tempo (por exemplo, porque um índice está faltando), isso pode ser visto descoberto inspecionando logs de execução de comando e observando quanto tempo eles realmente demoram.

O EF facilita muito a captura de tempos de execução de comandos, por meio de log simples ou 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 o nível de log é definido em LogLevel.Information, o EF emite uma mensagem de log para cada execução de comando com o tempo necessário:

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'

O comando acima levou 4 milissegundos. Se um determinado comando demorar mais do que o esperado, você encontrou um possível culpado por um problema de desempenho e agora pode se concentrar nele para entender por que ele está sendo executado lentamente. O registro de comandos também pode revelar casos em que interações inesperadas com o banco de dados estão a ocorrer; isso se manifesta como vários comandos quando apenas um era esperado.

Advertência

Deixar o log de execução de comandos ativado em seu ambiente de produção geralmente é uma má ideia. O registo em si torna a sua aplicação mais lenta e pode criar rapidamente enormes ficheiros de registo que podem encher o disco do seu servidor. Recomenda-se manter o logon apenas por um curto intervalo de tempo para coletar dados - enquanto monitora cuidadosamente seu aplicativo - ou para capturar dados de log em um sistema de pré-produção.

Correlacionando comandos de banco de dados a consultas LINQ

Um problema com o log de execução de comandos é que às vezes é difícil correlacionar consultas SQL e consultas LINQ: os comandos SQL executados pelo EF podem parecer muito diferentes das consultas LINQ a partir das quais foram gerados. Para ajudar com essa dificuldade, você pode usar o recurso de tags de consulta do EF, que permite injetar um pequeno comentário de identificação na consulta SQL:

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

A tag aparece nos logs:

-- 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

Muitas vezes, vale a pena marcar as principais consultas de um aplicativo dessa maneira, para tornar os logs de execução de comando mais imediatamente legíveis.

Outras interfaces para capturar dados de desempenho

Existem várias alternativas ao recurso de registro em log do EF para capturar tempos de execução de comandos, que podem ser mais poderosos. Os bancos de dados geralmente vêm com suas próprias ferramentas de rastreamento e análise de desempenho, que geralmente fornecem informações muito mais ricas e específicas do banco de dados além de simples tempos de execução; A configuração, os recursos e o uso reais variam consideravelmente entre os bancos de dados.

Por exemplo, o SQL Server Management Studio é um cliente poderoso que pode se conectar à sua instância do SQL Server e fornecer informações valiosas de gerenciamento e desempenho. Está além do escopo desta seção entrar nos detalhes, mas dois recursos que merecem ser mencionados são o Monitor de Atividade, que fornece um painel ao vivo da atividade do servidor (incluindo as consultas mais caras), e o recurso Eventos Estendidos (XEvent), que permite definir sessões arbitrárias de captura de dados que podem ser adaptadas às suas necessidades exatas. A documentação do SQL Server sobre monitoramento fornece mais informações sobre esses recursos, bem como outros.

Outra abordagem para capturar dados de desempenho é coletar informações emitidas automaticamente pelo EF ou pelo driver de banco de dados por meio da DiagnosticSource interface e, em seguida, analisar esses dados ou exibi-los em um painel. Se você estiver usando o Azure, o Azure Application Insights fornece um monitoramento tão poderoso pronto para uso, integrando o desempenho do banco de dados e os tempos de execução de consultas na análise da rapidez com que suas solicitações da Web estão sendo atendidas. Mais informações sobre isso estão disponíveis no tutorial de desempenho do Application Insights e na página de análise SQL do Azure.

Verificação de planos de execução de consultas

Depois de identificar uma consulta problemática que requer otimização, a próxima etapa geralmente é analisar o plano de execução da consulta. Quando os bancos de dados recebem uma instrução SQL, eles normalmente produzem um plano de como esse plano deve ser executado; Isso às vezes requer uma tomada de decisão complicada com base em quais índices foram definidos, quantos dados existem em tabelas, etc. (aliás, o plano em si geralmente deve ser armazenado em cache no servidor para um desempenho ideal). Os bancos de dados relacionais geralmente fornecem uma maneira para os usuários verem o plano de consulta, juntamente com o custo calculado para diferentes partes da consulta; Isso é inestimável para melhorar suas consultas.

Para começar a usar o SQL Server, consulte a documentação sobre planos de execução de consulta. O fluxo de trabalho de análise típico seria usar o SQL Server Management Studio, colar o SQL de uma consulta lenta identificada por um dos meios acima e produzir um plano de execução gráfica:

Exibir um plano de execução do SQL Server

Embora os planos de execução possam parecer complicados no início, vale a pena gastar um pouco de tempo se familiarizando com eles. É particularmente importante observar os custos associados a cada nó do plano e identificar como os índices são usados (ou não) nos vários nós.

Embora as informações acima sejam específicas do SQL Server, outros bancos de dados normalmente fornecem o mesmo tipo de ferramentas com visualização semelhante.

Importante

Às vezes, os bancos de dados geram planos de consulta diferentes, dependendo dos dados reais no banco de dados. Por exemplo, se uma tabela contiver apenas algumas linhas, um banco de dados pode optar por não usar um índice nessa tabela, mas executar uma verificação completa da tabela. Se estiver analisando planos de consulta em um banco de dados de teste, sempre verifique se ele contém dados semelhantes ao seu sistema de produção.

Métricas

As seções acima se concentraram em como obter informações sobre seus comandos e como esses comandos são executados no banco de dados. Além disso, o EF expõe um conjunto de métricas que fornecem mais informações de nível inferior sobre o que está acontecendo dentro do próprio EF e como seu aplicativo está usando-o. Essas métricas podem ser muito úteis para diagnosticar problemas de desempenho específicos e anomalias de desempenho, como problemas de cache de consulta que causam recompilação constante, vazamentos de DbContext não descartados e outros.

Consulte a página dedicada às métricas da EF para obter mais informações.

Benchmarking com o EF Core

No final do dia, às vezes você precisa saber se uma determinada maneira de escrever ou executar uma consulta é mais rápida do que outra. É importante nunca assumir ou especular a resposta, e é extremamente fácil montar um benchmark rápido para obter a resposta. Ao escrever benchmarks, é altamente recomendável usar a conhecida biblioteca BenchmarkDotNet , que lida com muitas armadilhas que os usuários encontram ao tentar escrever seus próprios benchmarks: você realizou algumas iterações de aquecimento? Quantas iterações seu benchmark realmente executa e por quê? Vamos dar uma olhada em como é um benchmark com EF Core.

Sugestão

O projeto de referência completo para a fonte abaixo está disponível aqui. Você é encorajado a copiá-lo e usá-lo como um modelo para seus próprios benchmarks.

Como um cenário de referência simples, vamos comparar os seguintes métodos diferentes de calcular a classificação média de todos os Blogs em nosso banco de dados:

  • Carregue todas as entidades, some as suas classificações individuais e calcule a média.
  • O mesmo que acima, mas use só uma consulta sem rastreamento. Isso deve ser mais rápido, já que a resolução de identidade não é executada e as entidades não são capturadas para fins de controle de alterações.
  • Evite carregar todas as instâncias da entidade do Blog, projetando apenas a classificação. Isso nos salva de transferir as outras colunas desnecessárias do tipo de entidade Blog.
  • Calcule a média no banco de dados tornando-a parte da consulta. Esta deve ser a forma mais rápida, uma vez que tudo é calculado na base de dados e apenas o resultado é transferido de volta para o cliente.

Com o BenchmarkDotNet, você escreve o código a ser referenciado como um método simples - assim como um teste de unidade - e o BenchmarkDotNet executa automaticamente cada método para um número suficiente de iterações, medindo de forma confiável quanto tempo leva e quanta memória é alocada. Aqui estão os diferentes métodos (o código de referência completo pode ser visto aqui):

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

    return (double)sum / count;
}

Os resultados estão abaixo, conforme impresso pelo BenchmarkDotNet:

Método Média Erro StdDev Mediana Proporção RácioSD Geração 0 Geração 1 Geração 2 Atribuído
LoadEntities 2,860.4 US 54,31 EUA 93,68 USD 2,844.5 US 4,55 0,33 210.9375 70.3125 - 1309,56 KB
CarregarEntidadesSemRastreamento 1,353.0 US 21,26 EUA 18,85 EUA 1.355,6 US 2.10 0.14 87.8906 3,9063 - 540,09 KB
ClassificaçãoSomenteParaProjeto 910,9 EUA 20,91 EUA 61,65 dólares 892,9 EUA 1.46 0.14 41.0156 0.9766 - 252,08 KB
CalcularNaBaseDeDados 627,1 EUA 14,58 EUA 42,54 EUA 626,4 EUA 1,00 0.00 4.8828 - - 33,27 KB

Observação

Como os métodos instanciam e descartam o contexto dentro do próprio método, essas operações são contabilizadas para o benchmark, embora, estritamente falando, não façam parte do processo de consulta. Isso não deve importar se o objetivo é comparar duas alternativas entre si (já que o contexto instanciação e descarte são os mesmos), e dá uma medição mais holística para toda a operação.

Uma limitação do BenchmarkDotNet é que ele mede o desempenho simples de um único thread dos métodos que você fornece e, portanto, não é adequado para benchmarking de cenários concorrentes.

Importante

Certifique-se sempre de ter dados em seu banco de dados que sejam semelhantes aos dados de produção ao fazer benchmarking, caso contrário, os resultados do benchmark podem não representar o desempenho real na produção.