EF Core 慢查询排查实战:TagWith、OpenTelemetry、执行计划,30 分钟定位性能瓶颈

EF Core 性能问题里,最折磨人的不是"慢",而是"慢得没规律",线上卡,测试又无法复现。

很多小D、小W同学都经历过这种现场:

  • 压测数据很好看
  • 数据库 CPU 没打满
  • 业务代码看起来也没什么大问题

你改了几个 Include,可能短期有效,但过两周又抖回来。根因往往不是某一行 LINQ 写错,而是整条排查链路没打通。

这篇文章就做一件事:给你一套能线上落地的 EF Core 慢查询定位闭环,从应用日志一路追到数据库执行计划,不靠猜。

问题背景:为什么本地快、线上慢

真实场景:订单列表接口平时 80ms 左右,高峰时段 P95 抬到 1s。你的第一反应是"数据库是不是扛不住了",但监控显示:

  • 数据库 CPU 长期低于 50%
  • 连接池没有打满
  • 磁盘 IO 没明显异常

排查下来,又遇到两个组合问题:

  1. 列表查询没有打 SQL 标签,日志里几百条 SQL 根本分不出谁是谁
  2. 某些筛选条件线上本地索引匹配不同,SQL 文本相同但参数分布不同,执行计划差异很大

也就是说,问题不是"不会优化 SQL",而是"看不见慢点在哪"。

原理解析:慢查询定位为什么总是卡在一半

EF Core 的查询链路大致是:

  1. LINQ 表达式翻译成 SQL
  2. 命令发送到数据库执行
  3. 结果集回传并在应用层物化

很多排查第一步都把 SQL 抓出来看看,忽略了第 2、3 步的上下文信息,比如:

  • 这条 SQL 是哪个接口触发的
  • 这次慢是数据库执行慢,还是返回数据太大导致物化慢
  • 慢的是固定 SQL,还是同模板下某些参数更慢

要拿到这些信息,最实用的组合就是:

  • TagWith:给 SQL 打业务标签
  • OpenTelemetry:采集耗时、TraceId、SQL 标签并统一上报
  • 执行计划:确认索引命中、回表、扫描和 Key Lookup

三者合起来,才能形成可服用、可验证的排查闭环。

示例代码:从"日志能看见"到"瓶颈可复盘"

第一步:先在关键查询上打标签

csharp 复制代码
public sealed record OrderListItemDto(
    long Id,
    string OrderNo,
    string CustomerName,
    decimal TotalAmount,
    DateTime CreatedAtUtc);

public async Task<IReadOnlyList<OrderListItemDto>> QueryOrdersAsync(
    AppDbContext db,
    DateTime from,
    DateTime to,
    CancellationToken ct)
{
    return await db.Orders
        .TagWith("OrderListPage:v2")
        .AsNoTracking()
        .Where(x => x.CreatedAtUtc >= from && x.CreatedAtUtc < to)
        .OrderByDescending(x => x.CreatedAtUtc)
        .Take(100)
        .Select(x => new OrderListItemDto(
            x.Id,
            x.OrderNo,
            x.Customer.Name,
            x.TotalAmount,
            x.CreatedAtUtc))
        .ToListAsync(ct);
}

TagWith 会把注释写进 SQL。你在数据库侧和日志侧都能直接看到 OrderListPage:v2,定位会快很多。

第二步:用 OpenTelemetry 采集慢 SQL 关键字段

csharp 复制代码
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text.RegularExpressions;
using OpenTelemetry;
using OpenTelemetry.Trace;

public sealed class EfSqlTagEnricher : BaseProcessor<Activity>
{
    private static readonly Regex EfTagLineRegex =
        new(@"^\s*--\s*(?<tag>.+?)\s*$", RegexOptions.Compiled);

    public override void OnEnd(Activity activity)
    {
        if (activity.Kind != ActivityKind.Client)
            return;

        // 只处理数据库调用 Span
        var dbSystem = activity.GetTagItem("db.system")?.ToString();
        if (string.IsNullOrWhiteSpace(dbSystem))
            return;

        var statement = activity.GetTagItem("db.statement")?.ToString();
        if (string.IsNullOrWhiteSpace(statement))
            return;

        var tags = ExtractAllEfTags(statement);
        if (tags.Count == 0)
            return;

        activity.SetTag("ef.tags", string.Join(" | ", tags));
        activity.SetTag("ef.primary_tag", tags[0]);
    }

    private static IReadOnlyList<string> ExtractAllEfTags(string sql)
    {
        var tags = new List<string>();

        using var reader = new StringReader(sql);
        while (true)
        {
            var line = reader.ReadLine();
            if (line is null)
                break;

            var trimmed = line.Trim();
            if (trimmed.Length == 0)
                continue;

            var match = EfTagLineRegex.Match(line);
            if (match.Success)
            {
                var tag = match.Groups["tag"].Value.Trim();
                if (!string.IsNullOrWhiteSpace(tag))
                    tags.Add(tag);
                continue;
            }

            // 遇到 SQL 正文后停止,避免把正文中的注释当成业务标签。
            break;
        }

        return tags;
    }
}

这段代码落地后,你就有了最关键的三类信息:

  • 耗时
  • TraceId
  • SQL 标签(来自 TagWith)

后面不管去日志平台还是数据库审计,排查效率都会提升一个量级。

第三步:注册 OTel 并打通上报链路

csharp 复制代码
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;

const string serviceName = "efcore-sql-traces";
builder.Services.AddOpenTelemetry()
    .ConfigureResource(r => r.AddService(serviceName))
    .WithTracing(tracing =>
        tracing
            .AddAspNetCoreInstrumentation()
            .AddEntityFrameworkCoreInstrumentation()
            .AddSource("MySqlConnector")
            .AddProcessor<EfSqlTagEnricher>()
            .AddOtlpExporter(o =>
            {
                o.Endpoint = new Uri("http://localhost:4318/v1/traces");
                o.Protocol = OtlpExportProtocol.HttpProtobuf;
            })
    );

第四步:把 SQL 拉到数据库侧看执行计划

当你在日志里找到慢 SQL 后,下一步是确认执行计划。分析问题本质,再相应的设计和实施解决方案。

总结

EF Core 慢查询排查,不能每次都盯着 LINQ 本身看。从工程实践角度来讲,我建议你把把链路打通:

  1. TagWith 把 SQL 和业务场景绑定起来
  2. OpenTelemetry 把耗时、TraceId、SQL 标签统一上报
  3. 用执行计划确认瓶颈到底在扫描、回表还是排序

如果你现在的线上排查还是"看见慢了就加索引",那就从这三步开始,先把可定位性建起来,再谈性能优化的上限。

相关推荐
墨10245 小时前
当 AI 助手开始管理多个项目:如何把“继续某项目”变成可联动机制
人工智能·ai·项目管理·架构设计·工程实践·openclaw
charlie1145141911 天前
2026年IMX6ULL正点原子Alpha开发板学习方案——U-Boot完全移植概览:从官方源码到你的自制板,这条路有多远
linux·学习·嵌入式·uboot·嵌入式linux·工程实践·编程指南
硅基喵3 天前
EF Core 并发冲突实战:乐观锁、RowVersion 与 DbUpdateConcurrencyException 怎么处理
ef core·工程实践
硅基喵4 天前
EF Core 写入链路深拆:从 ChangeTracker 到 SQL Batch 的性能诊断与优化
ef core·工程实践
小邓的技术笔记4 天前
ASP.NET Core 认证鉴权实战:JWT、Policy 与权限边界怎么落地
asp.net·工程实践
小邓的技术笔记4 天前
从 IApplicationBuilder 到 RequestDelegate:ASP.NET Core 请求管线的性能与可观测性实战
asp.net·工程实践
硅基喵5 天前
ASP.NET Core 认证鉴权实战:JWT、Policy 与权限边界怎么落地
asp.net core·工程实践
硅基喵7 天前
从 IApplicationBuilder 到 RequestDelegate:ASP.NET Core 请求管线的性能与可观测性实战
asp.net core·工程实践