接口超时排查案例

wen java案例 1

从一次“偶发”故障到系统根因的深度剖析

目录导读

  1. 故障现象:一场突如其来的“用户恐慌”
  2. 初步排查:网络层与基础监控的“盲区”
  3. 深入分析:线程池、连接池与GC的“三角恋”
  4. 根因定位:一次“锁等待”引发的雪崩效应
  5. 解决方案:超时治理的三级火箭
  6. 复盘与总结:超时问题的“体检清单”
  7. 常见问答(FAQ)

故障现象:一场突如其来的“用户恐慌”

某天下午2点,线上订单服务突然出现大量接口超时告警,P99延迟从平时的200ms飙升到5秒以上,部分请求直接抛出Read timed out异常,业务方反馈:用户提交订单后页面一直转圈,部分支付回调失败,更诡异的是,该现象持续了约15分钟后自动恢复,但每隔2小时又复现一次。

接口超时排查案例

从监控面板看,CPU、内存、磁盘IO均处于正常水位,网络入出流量也无异常,这让初期的“流量突刺”假设不攻自破。

初步排查:网络层与基础监控的“盲区”

排查动作:

  • 检查Nginx访问日志:确认上游响应时间(upstream_response_time)普遍超过3秒,而网络传输时间(request_time - upstream_response_time)微乎其微。
  • 使用pingtelnet验证网络连通性:延迟<1ms,TCP三次握手正常。
  • 查看Redis、MySQL慢查询:均无异常慢日志,但注意到MySQL的Threads_connected在故障时段从50飙升到300。

网络不是瓶颈,问题集中在应用层与数据库交互层。

深入分析:线程池、连接池与GC的“三角恋”

1 线程池“饥饿”假象

通过jstack抓取线程快照,发现大量http-nio-8080-exec-*线程处于WAITING (parking)状态,但并非阻塞在业务代码上,而是阻塞在FutureTask.get(),这暗示下游依赖响应变慢,导致上游线程全部被占满。

2 连接池耗尽真凶

检查HikariCP连接池监控,发现active连接数恒等于maximum-pool-size(默认10),且pending队列积压了数千请求,进一步抓取MySQL SHOW PROCESSLIST,发现数十个Sleep状态连接,且State列为Waiting for table metadata lock

3 GC“假停顿”背锅

虽然Full GC次数不多,但有3次CMS的“Concurrent Mode Failure”触发了Serial Old GC,单次STW达到2.1秒,虽然不直接导致超时,但加剧了连接池的排队效应。

根因定位:一次“锁等待”引发的雪崩效应

通过阿里云DAS(数据库自治服务)的“全量SQL洞察”,定位到一条诡异的慢SQL:

UPDATE inventory SET stock = stock - 1 WHERE product_id = ? AND stock > 0

该语句执行计划显示走了主键索引,但Rows_examined却高达200万,原因是该表存在大量“幽灵行”(因历史Bug产生的未提交事务残留),导致MySQL的MVCC版本链过长,每次更新操作都需要遍历版本链判断可见性,性能急剧下降。

更致命的是,该表上有一个长期未提交的事务持有METADATA LOCK,导致所有针对该表的DDL(如定时任务中的ALTER TABLE)和DML(如上述UPDATE)互相阻塞,最终形成: 一条慢SQL → 连接池耗尽 → 线程池饥饿 → 全站接口超时 的雪崩链路。

解决方案:超时治理的三级火箭

第一级:快速止血(故障期)

  • 在数据库端执行KILL掉长时间sleep的连接和持有锁的事务。
  • 在应用层动态调整连接池maximum-pool-size至50,并设置connection-timeout为3秒(原30秒)。
  • 对核心链路启用熔断降级:使用Sentinel对库存接口设置QPS阈值,超限直接返回“库存繁忙”。

第二级:根治优化(恢复后)

  • 数据库侧:清理“幽灵行”数据(通过pt-online-schema-change重建表),并设置innodb_lock_wait_timeout=2,斩断锁等待链条。
  • 应用侧:将“扣库存”改成“预扣+异步对账”模式,使用Redis原子操作(DECRBY)替代数据库行锁,最终一致性由MQ消息保证。
  • 超时分级:设定DNS解析<100ms,TCP连接<500ms,接口响应<2s,数据库语句<500ms的四级标准,超过即打印告警并降级。

第三级:预防性治理(长期)

  • 引入全链路压测(如阿里云PTS),每季度一次模拟高并发场景。
  • 在代码层植入动态超时配置(通过Apollo),支持按接口路由、按用户等级调整超时时间。
  • 建立根因知识库:每次故障后自动生成“事件时间线”,关联Metrics、Logs、Traces三组数据。

复盘与总结:超时问题的“体检清单”

本次案例暴露了三个日常容易忽视的点:

  1. “等待锁”是隐形杀手——数据库监控不能只看慢查询,还要看锁等待和未提交事务。
  2. 连接池参数不能“一劳永逸”——需要结合压测结果动态调整,connection-timeout必须小于业务容忍的最终超时时间。
  3. 异常恢复不等于根因消除——15分钟的自动恢复其实是MySQL的lock_wait_timeout默认50秒 + 应用重试机制叠加的结果,掩盖了真凶。

给同行的自查清单:

  • [ ] 你的SQL执行计划是否扫描了过多“已删除版本”?
  • [ ] 每个服务是否都配置了合理的ReadTimeoutConnectTimeout
  • [ ] 连接池的空闲回收策略与慢SQL的峰值是否匹配?
  • [ ] 是否监控了ThreadPoolExecutorRejectedExecutionException

常见问答(FAQ)

Q1:为什么CPU很低但接口超时? A:大概率是线程阻塞在IO(数据库、Redis、外部HTTP)上,CPU处于空闲等待状态,建议用jstack抓线程状态,重点看WAITINGBLOCKED

Q2:设置connectTimeout为1秒,但实际耗时3秒? A:connectTimeout只控制TCP建连时间,不包含读取数据时间,如果需要整体超时,需使用readTimeout,或者用OkHttpcallTimeout(涵盖DNS、连接、写入、读取全过程)。

Q3:连接池增大一倍是否一定有效? A:不一定,若瓶颈在数据库锁,连接池再大只会加重数据库压力,建议先压测确认数据库最大并发连接数,再反向调整应用侧连接池。

Q4:如何模拟“锁等待”场景测试? A:在一个会话中执行BEGIN; SELECT * FROM t WHERE id=1 FOR UPDATE;然后不提交,在另一会话执行相同行更新,并设置innodb_lock_wait_timeout=2,观察超时表现。


专栏推荐: 如果你正在为系统稳定性发愁,欢迎关注我的技术专栏《分布式系统稳定性实践》,每周更新一篇真实故障案例,从现象到根因,从代码到架构,帮你建立完整的排障思维。

抱歉,评论功能暂时关闭!