性能诊断

本部分介绍检测 EF 应用程序中的性能问题的方法,以及在确定有问题的区域后,如何进一步分析这些问题以确定根本问题。 在得出任何结论之前,请务必仔细诊断和调查任何问题,并避免臆断问题的根本原因。

通过日志记录识别速度慢的数据库命令

最终,EF 准备并执行要针对数据库执行的命令;对于关系数据库,这意味着通过 ADO.NET 数据库 API 执行 SQL 语句。 如果某个查询花费了太多时间(例如,因为缺少索引),则可以通过检查命令执行日志并观察它们实际花费的时间来发现这一点。

EF 通过简单的日志记录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);
}

当日志记录级别设置为 LogLevel.Information 时,EF 会为每个命令执行发出一条日志消息,其中包含所花费的时间:

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'

上述命令需要花 4 毫秒。 如果某个命令所花费的时间超出了预期,则你已找到性能问题的可能原因,你现在可以关注这一点,了解运行缓慢的原因。 命令日志记录还可以揭示发生意外数据库往返的情况;这将显示为多个命令,但本应该只有一个。

警告

最好不要在生产环境中启用命令执行日志记录。 日志记录本身会减慢应用程序的速度,并可能快速创建可填满服务器磁盘的大量日志文件。 建议仅启用日志记录一小段时间以收集数据(同时仔细监视应用程序)或在预生产系统上捕获日志记录数据。

将数据库命令与 LINQ 查询关联

命令执行日志记录的一个问题是,有时很难将 SQL 查询与 LINQ 查询相关联:EF 执行的 SQL 命令看起来可能与生成它们的 LINQ 查询有很大不同。 为了帮助解决此难题,建议你使用 EF 的查询标记功能,该功能使你能够在 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();

标记显示在日志中:

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

通常值得以这种方式标记应用程序的主要查询,以使命令执行日志更直接可读。

用于捕获性能数据的其他接口

EF 的日志记录功能有多种替代方法可用于捕获命令执行时间,这些方法可能更强大。 数据库通常带有自己的跟踪和性能分析工具,这些工具通常提供更丰富的数据库特定信息,而不仅仅是简单的执行时间;实际的设置、功能和使用情况因数据库而异。

例如,SQL Server Management Studio 是一个功能强大的客户端,可以连接到 SQL Server 实例并提供有价值的管理和性能信息。 本部分不会介绍细节,但值得一提的两个功能是活动监视器,它提供了服务器活动(包括最昂贵的查询)的实时仪表板,以及扩展事件 (XEvent) 功能,它允许定义可根据你的确切需求进行定制的任意数据捕获会话。 有关监视的 SQL Server 文档提供了有关这些功能以及其他功能的详细信息。

捕获性能数据的另一种方法是通过 DiagnosticSource 接口收集 EF 或数据库驱动程序自动发出的信息,然后分析该数据或将其显示在仪表板上。 如果你使用的是 Azure,则 Azure Application Insights 提供现成的强大监视功能,在分析 Web 请求的处理速度时集成了数据库性能和查询执行时间。 有关此问题的详细信息,请参阅 Application Insights 性能教程Azure SQL 分析页

检查查询执行计划

确定需要优化的有问题的查询后,下一步通常是分析查询的执行计划。 当数据库收到 SQL 语句时,它们通常会生成如何执行该计划的规划;这有时需要根据已定义的索引,表中存在的数据量等进行复杂的决策(顺便说一句,计划本身通常应在服务器上缓存以获得最佳性能)。 关系数据库通常为用户提供一种查看查询计划以及查询的不同部分的计算成本的方法;这对于改进查询非常有用。

若要开始使用 SQL Server,请参阅有关查询执行计划的文档。 典型的分析工作流使用 SQL Server Management Studio,粘贴通过上述方法之一识别的慢速查询的 SQL,然后生成图形执行计划

Display a SQL Server execution plan

虽然执行计划初看似乎很复杂,但值得花一点时间去熟悉它们。 特别重要的是要注意与计划的每个节点关联的成本,并确定索引是如何在各个节点中使用(或不使用)的。

虽然上述信息针对 SQL Server,但其他数据库通常提供具有类似可视化效果的相同类型的工具。

重要

数据库有时会生成不同的查询计划,具体取决于数据库中的实际数据。 例如,如果某个表只包含几行,则数据库可能会选择不对该表使用索引,而是执行完全的表扫描。 如果在测试数据库上分析查询计划,请始终确保它包含与生产系统类似的数据。

事件计数器

以上各部分重点介绍如何获取有关命令的信息,以及如何在数据库中执行这些命令。 除此之外,EF 还公开了一组事件计数器,这些计数器提供了有关 EF 本身内部发生的情况以及应用程序如何使用它的更多大致信息。 这些计数器对于诊断特定的性能问题和性能异常非常有用,例如查询缓存问题,该问题导致不断的重新编译、未处理的 DbContext 泄漏等。

有关详细信息,请参阅 EF 事件计数器上的专用页。

EF Core 的基准测试

最终,你有时需要知道某种特定的编写或执行查询的方式是否比另一种方式快。 请勿臆断或推测答案,将快速的基准组合在一起获得答案是非常容易的。 在编写基准测试时,强烈建议使用众所周知的 BenchmarkDotNet 库,该库可以处理用户在尝试编写自己的基准测试时遇到的许多陷阱:你是否执行了一些预热迭代? 基准实际运行多少次迭代?为什么? 让我们看一下 EF Core 的基准是什么样的。

提示

此处提供以下源的完整基准项目。 建议你复制它并将其用作你自己的基准的模板。

在一个简单的基准场景中,让我们比较以下计算数据库中所有博客的平均排名的不同方法:

  • 加载所有实体,对各排名求和,并计算平均值。
  • 与上面相同,仅使用非跟踪查询。 这应该更快,因为不会执行标识解析,并且不会出于更改跟踪的目的对实体拍摄快照。
  • 通过仅投影排名,避免加载整个博客实体实例。 这样可以避免传输博客实体类型的其他不需要的列。
  • 通过将数据库中的平均值作为查询的一部分来计算该平均值。 这应该是最快的方法,因为所有内容都在数据库中计算,只有结果被传输回客户端。

使用 BenchmarkDotNet,你可以将要进行基准测试的代码编写为一个简单的方法(就像单元测试一样),并且 BenchmarkDotNet 会自动运行每种方法以进行足够数量的迭代,从而可靠地测量需要多长时间以及分配了多少内存。 下面是不同的方法(可在此处查看完整的基准代码):

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

结果如下,由 BenchmarkDotNet 打印:

方法 平均值 错误 标准偏差 中值 比率 RatioSD Gen 0 Gen 1 Gen 2 已分配
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

注意

当方法实例化并释放方法中的上下文时,这些操作将计入基准测试,尽管严格来说它们不是查询过程的一部分。 如果目标是将两个替代方案相互比较(因为上下文实例化和处置是相同的),并且为整个操作提供更全面的度量,则这无关紧要。

BenchmarkDotNet 的一个限制是,它测量你提供的方法的简单单线程性能,因此不太适合对并发场景进行基准测试。

重要

在进行基准测试时,始终确保数据库中的数据与生产数据相似,否则基准测试结果可能无法表示生产中的实际性能。