← 返回文章列表
解决方案 4 分钟阅读 炬鲸团队

支付系统慢查询排查:用链路追踪定位到具体 SQL

支付链路慢,难的是从“整条链路慢”缩小到“哪条 SQL 慢”。本文用一个真实排查案例,讲怎么用链路追踪 + 日志关联把慢查询定位到具体语句和参数。

一个典型的支付慢查询

某支付系统大促期间,下单接口的 P99 从 300ms 涨到 2s。监控告警先响了:错误率没涨,但延迟超阈值。翻开慢 SQL 监控,数据库侧的平均耗时看着正常,可用户就是觉得慢。平均延迟这种指标,恰恰因为掩盖了离群点而误导人,这个案例就是典型。

这种“数据库平均正常、端到端慢”的情况,问题通常不在数据库本身,而在调用链中间的某个环节——连接池等待、序列化、或某条偶发的重查询。要找到它,得沿着整条链路看,而不是盯着某一个单点。

先看链路,定位慢在哪一段

在炬鲸 OBSERVE 的链路追踪里,用下单接口的 trace_id 找到一条慢请求,展开调用树:

POST /order/create                2000ms
├── auth-service /verify          45ms
├── inventory-service /lock       380ms
│   └── SELECT ... FOR UPDATE     360ms  ← 这里
└── pay-service /pay              1500ms
    ├── DB: INSERT INTO ...       60ms
    └── DB: SELECT ... FOR UPDATE 1400ms  ← 还有这里

两个 Span 明显异常:一个 SELECT ... FOR UPDATE 锁了 360ms,另一个锁了 1400ms。锁定这两个 Span,问题就从“整条链路慢”缩小到了“两条 SQL 慢”。Trace 给你的是仪表盘给不了的东西:确切的父子结构和每一跳的耗时,让你不用再猜该甩锅给哪个服务。

下钻到具体 SQL 和参数

点开慢的 Span,可以看到绑定的 SQL 模板和参数:

SELECT * FROM inventory WHERE sku_id = ? FOR UPDATE
-- params: sku_id = 100234

结合数据库侧这条 SQL 的执行计划,发现它走了全表扫描:sku_id 上的索引在大促前的一次变更里被误删了。这就是根因。拿到具体参数(而不只是模板)很重要——一条 SQL 可能对绝大多数输入都正常,只对某一个 SKU 病态地慢。

日志关联补齐上下文

光有 SQL 还不够,要看这条请求前后发生了什么。从 Span 直接跳到它打印的日志:

[order-service] 10:32:11.034 WARN  pool=inventory-ds active=20 max=20
[order-service] 10:32:11.040 WARN  pool=inventory-ds getConnection timeout

日志显示连接池被打满,getConnection 超时——全表扫描拖慢每条 SQL,慢 SQL 又占满连接池,形成恶性循环。日志和 Trace 合起来才讲得完整:Trace 指出慢 SQL,日志解释连接池耗尽这种连带伤害是怎么把问题放大的。

排查套路总结

这类问题有固定套路:

  1. 用错误率/延迟告警先圈定范围,别上来就翻数据库。
  2. 用 trace 把慢请求展开成调用树,找出耗时异常的 Span。
  3. 点开 Span 看 SQL 模板和参数,拿到具体语句。
  4. 关联日志看上下文,确认连接池、锁、GC 等附带现象。
  5. 修复后用同样的 trace 对比,确认 P99 回落。

修复后重建索引,该接口 P99 回到 300ms 以内。整个定位过程不到半小时,靠的就是链路追踪和日志的关联,而不是在数据库慢查询日志里大海捞针。删索引本身只是一行代码的失误;真正费时的从来不是找到慢 SQL,而是知道该去哪找。