Diagnóstico de Desempenho
Esta seção discute maneiras de detectar problemas de desempenho em seu aplicativo EF e, depois que uma área problemática for identificada, 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.
Identificar comandos de banco de dados lentos por meio de 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 de banco de dados ADO.NET. Se uma determinada consulta estiver demorando muito (por exemplo, porque um índice está ausente), isso pode ser visto inspecionando logs de execução de comando e observando quanto tempo eles realmente levam.
O EF facilita muito a captura de tempos de execução de comandos por meio do log simples ou do 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 registro em log é definido como 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 levar 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 log de comandos também pode revelar casos em que viagens de ida e volta inesperadas de banco de dados estão sendo feitas; isso apareceria como vários comandos em que apenas um é esperado.
Aviso
Deixar o log de execução de comando habilitado em seu ambiente de produção geralmente é uma má ideia. O log em si reduz a velocidade do aplicativo e pode criar rapidamente arquivos de log enormes que podem preencher o disco do servidor. É recomendável continuar fazendo logon apenas por um curto intervalo de tempo para coletar dados – enquanto monitora cuidadosamente seu aplicativo – ou capturar dados de log em um sistema de pré-produção.
Correlacionar 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 das quais foram geradas. Para ajudar com essa dificuldade, convém usar o recurso de marcas de consulta do EF, que permite injetar um comentário pequeno e identificador na consulta 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();
A marca 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
Geralmente, vale a pena marcar as consultas principais de um aplicativo dessa forma, para tornar os logs de execução de comando mais imediatamente legíveis.
Outras interfaces para capturar dados de desempenho
Há várias alternativas ao recurso de registro em log do EF para capturar tempos de execução de comando, que podem ser mais poderosos. Normalmente, os bancos de dados vêm com suas próprias ferramentas de análise de desempenho e rastreamento, que geralmente fornecem informações muito mais avançadas e específicas do banco de dados além dos tempos de execução simples; a configuração real, os recursos e o uso variam consideravelmente entre os bancos de dados.
Por exemplo, o SQL Server Management Studio é um cliente poderoso que pode se conectar à instância do SQL Server e fornecer informações valiosas de gerenciamento e desempenho. Está além do escopo desta seção para entrar nos detalhes, mas dois recursos que vale a pena mencionar são o Monitorde Atividade, que fornece um painel ao vivo da atividade do servidor (incluindo as consultas mais caras) e o recurso de 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 outras.
Outra abordagem para capturar dados de desempenho é coletar informações emitidas automaticamente pelo EF ou pelo driver de banco de dados por meio da interface DiagnosticSource
e, em seguida, analisar esses dados ou exibi-los em um painel. Se você estiver usando o Azure, o Azure Application Insights fornecerá um monitoramento tão poderoso pronto, integrando o desempenho do banco de dados e os tempos de execução de consulta 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 de SQL do Azure.
Inspecionando planos de execução de consulta
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 próprio plano geralmente deve ser armazenado em cache no servidor para um desempenho ideal). Os bancos de dados relacionais normalmente fornecem uma maneira de 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 os planos de execução de consulta. O fluxo de trabalho de análise típico seria usar oSQL Server Management Studio, colar o SQL de uma consulta lenta identificada por meio de um dos meios acima e produzir um plano de execução gráfica:
Embora os planos de execução possam parecer complicados no início, vale a pena passar 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
Os bancos de dados às vezes 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 poderá optar por não usar um índice nessa tabela, mas executar uma verificação de tabela completa. Se estiver analisando planos de consulta em um banco de dados de teste, 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 no 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 expostos e outros.
Consulte a página dedicada em Métricas do EF para obter mais informações.
Benchmarking com EF Core
No final do dia, às vezes você precisa saber se uma maneira específica 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 parâmetro de comparação rápido para obter a resposta. Ao escrever parâmetros de comparação, é altamente recomendável usar a conhecida biblioteca BenchmarkDotNet, que lida com muitas armadilhas que os usuários encontram ao tentar escrever seus próprios parâmetros de comparação: você já realizou algumas iterações de aquecimento? Quantas iterações seu parâmetro de comparação realmente é executado e por quê? Vamos dar uma olhada na aparência de um parâmetro de comparação com o EF Core.
Dica
O projeto de parâmetro de comparação completo para a origem abaixo está disponível aqui. Você é incentivado a copiá-lo e usá-lo como um modelo para seus próprios parâmetros de comparação.
Como um cenário de parâmetro de comparação simples, vamos comparar os seguintes métodos diferentes de cálculo da classificação média de todos os Blogs em nosso banco de dados:
- Carregue todas as entidades, some suas classificações individuais e calcule a média.
- Da mesma forma que acima, use apenas uma consulta sem acompanhamento. Isso deve ser mais rápido, já que a resolução de identidade não é executada e as entidades não são instantâneos para fins de controle de alterações.
- Evite carregar todas as instâncias de entidade do Blog projetando apenas a classificação. Salva-nos da transferência das outras colunas desnecessárias do tipo de entidade blog.
- Calcule a média no banco de dados tornando-a parte da consulta. Essa deve ser a maneira mais rápida, já que tudo é calculado no banco de dados e apenas o resultado é transferido de volta para o cliente.
Com o BenchmarkDotNet, você escreve o código para 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 parâmetro de comparação completo pode ser visto aqui):
- Carregar entidades
- Carregar entidades, sem acompanhamento
- Classificação somente do projeto
- Calcular no banco de dados
[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;
}
Os resultados estão abaixo, conforme impresso por BenchmarkDotNet:
Método | Média | Erro | StdDev | Median | Proporção | RatioSD | Geração 0 | Gen 1 | Gen 2 | Alocado |
---|---|---|---|---|---|---|---|---|---|---|
LoadEntities | 2,860.4 us | 54.31 us | 93.68 us | 2,844.5 us | 4.55 | 0.33 | 210.9375 | 70.3125 | - | 1309,56 KB |
LoadEntitiesNoTracking | 1,353.0 us | 21.26 us | 18.85 us | 1,355.6 us | 2,10 | 0,14 | 87.8906 | 3.9063 | - | 540,09 KB |
ProjectOnlyRanking | 910.9 us | 20.91 us | 61.65 us | 892.9 us | 1.46 | 0,14 | 41.0156 | 0.9766 | - | 252,08 KB |
CalculateInDatabase | 627.1 us | 14.58 us | 42.54 us | 626.4 us | 1,00 | 0,00 | 4.8828 | - | - | 33.27 KB |
Observação
À medida que os métodos instanciam e descartam o contexto dentro do método, essas operações são contadas para o parâmetro de comparação, embora estritamente falando não façam parte do processo de consulta. Isso não deve importar se a meta é comparar duas alternativas umas com as outras (já que a instanciação de contexto e o descarte são iguais) e fornece uma medida mais holística para toda a operação.
Uma limitação do BenchmarkDotNet é que ele mede o desempenho simples e de thread único dos métodos fornecidos e, portanto, não é adequado para cenários simultâneos de benchmarking.
Importante
Sempre certifique-se de ter dados em seu banco de dados semelhantes aos dados de produção durante o benchmark, caso contrário, os resultados do parâmetro de comparação podem não representar o desempenho real na produção.