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

张开发
2026/4/6 21:26:03 15 分钟阅读

分享文章

EF Core 慢查询排查实战:TagWith、OpenTelemetry、执行计划,30 分钟定位性能瓶颈
很多小D、小W同学都经历过这种现场压测数据很好看数据库 CPU 没打满业务代码看起来也没什么大问题你改了几个Include可能短期有效但过两周又抖回来。根因往往不是某一行 LINQ 写错而是整条排查链路没打通。这篇文章就做一件事给你一套能线上落地的 EF Core 慢查询定位闭环从应用日志一路追到数据库执行计划不靠猜。问题背景为什么本地快、线上慢真实场景订单列表接口平时 80ms 左右高峰时段 P95 抬到 1s。你的第一反应是“数据库是不是扛不住了”但监控显示数据库 CPU 长期低于 50%连接池没有打满磁盘 IO 没明显异常排查下来又遇到两个组合问题列表查询没有打 SQL 标签日志里几百条 SQL 根本分不出谁是谁某些筛选条件线上本地索引匹配不同SQL 文本相同但参数分布不同执行计划差异很大也就是说问题不是“不会优化 SQL”而是“看不见慢点在哪”。原理解析慢查询定位为什么总是卡在一半EF Core 的查询链路大致是LINQ 表达式翻译成 SQL命令发送到数据库执行结果集回传并在应用层物化很多排查第一步都把 SQL 抓出来看看忽略了第 2、3 步的上下文信息比如这条 SQL 是哪个接口触发的这次慢是数据库执行慢还是返回数据太大导致物化慢慢的是固定 SQL还是同模板下某些参数更慢要拿到这些信息最实用的组合就是TagWith给 SQL 打业务标签OpenTelemetry采集耗时、TraceId、SQL 标签并统一上报执行计划确认索引命中、回表、扫描和 Key Lookup三者合起来才能形成可服用、可验证的排查闭环。示例代码从“日志能看见”到“瓶颈可复盘”第一步先在关键查询上打标签public sealed record OrderListItemDto(long Id,string OrderNo,string CustomerName,decimal TotalAmount,DateTime CreatedAtUtc);public async TaskIReadOnlyListOrderListItemDto 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 关键字段using System.Collections.Generic;using System.Diagnostics;using System.IO;using System.Text.RegularExpressions;using OpenTelemetry;using OpenTelemetry.Trace;public sealed class EfSqlTagEnricher : BaseProcessorActivity{private static readonly Regex EfTagLineRegex new(^\s*--\s*(?tag.?)\s*$, RegexOptions.Compiled);public override void OnEnd(Activity activity){if (activity.Kind ! ActivityKind.Client)return;// 只处理数据库调用 Spanvar 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 IReadOnlyListstring ExtractAllEfTags(string sql){var tags new Liststring();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;}}这段代码落地后你就有了最关键的三类信息耗时TraceIdSQL 标签来自 TagWith后面不管去日志平台还是数据库审计排查效率都会提升一个量级。第三步注册 OTel 并打通上报链路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).AddProcessorEfSqlTagEnricher().AddOtlpExporter(o {o.Endpoint new Uri(http://localhost:4318/v1/traces);o.Protocol OtlpExportProtocol.HttpProtobuf;}));第四步把 SQL 拉到数据库侧看执行计划当你在日志里找到慢 SQL 后下一步是确认执行计划。分析问题本质再相应的设计和实施解决方案。总结EF Core 慢查询排查不能每次都盯着 LINQ 本身看。从工程实践角度来讲我建议你把把链路打通用TagWith把 SQL 和业务场景绑定起来用OpenTelemetry把耗时、TraceId、SQL 标签统一上报用执行计划确认瓶颈到底在扫描、回表还是排序

更多文章