支付链路慢,难的是从“整条链路慢”缩小到“哪条 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 给你的是仪表盘给不了的东西:确切的父子结构和每一跳的耗时,让你不用再猜该甩锅给哪个服务。
点开慢的 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,日志解释连接池耗尽这种连带伤害是怎么把问题放大的。
这类问题有固定套路:
修复后重建索引,该接口 P99 回到 300ms 以内。整个定位过程不到半小时,靠的就是链路追踪和日志的关联,而不是在数据库慢查询日志里大海捞针。删索引本身只是一行代码的失误;真正费时的从来不是找到慢 SQL,而是知道该去哪找。