上周三下午,同事在群里甩了一张监控截图,某个订单查询接口P99延迟突然从80ms飙到3s+,偶尔还直接504。
当时第一反应是——谁又上了什么骚操作。
先看日志,别瞎猜
直接去ELK捞日志,按traceId串了一下调用链,发现大部分耗时都卡在一个查订单列表的RPC调用上。
这个接口平时QPS不高,大概200左右,平时响应很稳,突然炸了就很反常。
// 大致逻辑,简化版 List<Order> orders = orderMapper.selectByUserId(userId);SQL本身不复杂,就是按user_id查,加了索引,理论上不该慢。
看监控,排除网络抖动
先看了下网络层面的监控,丢包率正常,TCP重传也没异常。
然后看下游数据库的连接池状态,HikariCP的active连接数已经打满了,wait线程一堆。
到这里基本可以确认——不是网络问题,是数据库那边扛不住了。
慢SQL日志里捞线索
去数据库侧看了下慢查询日志,果然有一堆类似的SQL,执行时间从1s到5s不等。
但诡异的是,这条SQL在测试环境跑得飞快,explain出来也走的索引,type是ref,key_len也正常。
这时候有个同事提了一嘴:是不是数据量变了?
去线上库一查,那个user_id对应的订单数据量大概有12万条,而且没有分页,直接全量select。
SELECT * FROM t_order WHERE user_id = 10086;12万条,没limit,全量拉回来。
之前这个用户是个普通用户,数据量小,所以一直没暴露。这次是个大客户的测试账号,数据量一上来就炸了。
根因
说白了就是:
- 代码里没做分页,默认全量查
- 测试数据量太小,没覆盖到大用户场景
- 连接池被打满后,其他正常请求也被拖死了
怎么修的
改动不大,主要就两件事:
- 接口加上分页参数,默认page_size=20,最大不超过100
- 对全量查询的场景做了兜底,超过阈值直接拒绝并告警
if (count > MAX_ALLOWED) { log.warn("user_id={} order count={} exceed limit", userId, count); throw new BizException("数据量过大,请使用筛选条件"); }另外顺手把select *改成了指定字段,减少网络传输量。
几个教训
- 测试数据要贴近真实分布,不能全是几条数据的理想场景
- 分页是基本功,任何列表查询都应该有分页兜底
- 连接池打满是最危险的信号之一,一个慢请求能把整个服务拖垮
- 监控告警要配好,P99超阈值就报警,别等用户来反馈
最后
这种问题其实不算难排查,关键是要有一个清晰的排查路径:
日志 → 调用链 → 监控 → 慢SQL → 数据分布
一步一步缩小范围,别一上来就怀疑框架、怀疑JVM、怀疑玄学。
大部分线上问题,根因都很朴素。