当前位置: 首页 > news >正文

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

很多小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

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

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

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

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 关键字段

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 并打通上报链路

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. 用执行计划确认瓶颈到底在扫描、回表还是排序
http://www.jsqmd.com/news/592824/

相关文章:

  • AutoHotkey-v1.0:Windows自动化效率革命的极简解决方案
  • 利用快马平台快速构建openclaw机械臂抓取仿真原型
  • 双天线北斗接收机在机器人导航中的实战配置——以NC502-D为例
  • 基于模糊控制的改进DWA算法功能详解
  • 人生感悟 --- 为什么ld一直爱开会
  • AssetStudio资源处理指南:从教育素材提取到独立游戏开发的创新应用
  • MATLAB轴承动力学模拟:不同故障类型下的动力学行为分析及其图表输出
  • Windows系统优化与驱动管理完全指南:释放磁盘空间并解决驱动冲突
  • 波普尔证伪主义批判:看门狗悖论与物种隔离奖——人类科学与动物本能的终极划界
  • https://scrcpyapp.org/ scrcpy
  • 在PC上玩Switch游戏:Ryujinx模拟器终极指南与实用教程
  • 基于RISC-V五级流水线设计的32位CPU:支持多种特性与AXI总线接口,适合初学者学习并附...
  • 高效获取金融数据:pywencai驱动的量化投资新范式
  • CentOS7服务器流量飙升别慌!手把手教你用iftop+nload揪出‘吃流量’的进程
  • MP4视频修复难题终结者:untrunc开源工具全解析
  • 利用快马平台基于opencode官网描述快速构建个人博客系统原型
  • 萧邦官方售后服务中心新址实地考察报告(2026年4月最新地址电话) - 亨得利官方服务中心
  • 保姆级教程:用cam_lidar_calibration搞定激光雷达与相机标定(附避坑指南)
  • 医疗级光学检测方案拆解:如何用OPT101+单电源设计符合IEC60601标准的血氧探头前端
  • OpenClaw任务监控实战:Phi-3-vision-128k-instruct长流程管理
  • 雷达信号相干性:从理论到工程实践的关键解析
  • 推荐一个夸克网盘资源网站,大家找资料更方便点
  • SVG Editor终极指南:3分钟掌握免费在线矢量图形编辑
  • starlette - 轻量级ASGI Web框架
  • 基于STM32的激光测距传感器软件系统深度解析
  • Markor完整指南:如何在Android上使用这款终极轻量级文本编辑器提升效率
  • SpringMVC+MyBatis整合微信H5支付全流程实战(附避坑指南)
  • Pads Layout 高效工作流——库管理优化与文件转换实战
  • 从需求到部署:基于快马平台实战开发cmhhc在线应用
  • 30_泰勒级数