接口超时排查了3天,最后发现是“它“在捣鬼——一份可复用的排查清单
接口又超时了这是后端开发者最不愿听到的一句话。更令人崩溃的是日志正常、CPU正常、内存正常、网络正常——一切看起来都正常但接口就是超时。我们曾为一个接口超时问题排查了整整3天。最终发现根源不在网络不在数据库而在一个被忽视的配置项。本文复盘这次排查过程并提供一份可复用的接口超时排查清单。一、 事故场景一个诡异的超时背景我们的核心数据推送服务负责处理来自政策快报的实时政策数据流突然出现假死现象。症状进程还在端口还在监听但所有请求都超时P99延迟从50ms飙升至30s以上服务器CPU、内存、磁盘IO全部正常最诡异的是监控显示线程池的活跃线程数始终为0但任务队列却在持续增长。二、 排查过程从应用层到线程池的逐步回退第一阶段怀疑应用层代码检查了应用日志无异常检查了数据库连接池连接数正常检查了缓存命中率无变化第二阶段怀疑网络问题检查了服务间网络延迟RTT正常排除了网络抖动第三阶段定位到线程池在假死期间抓取了线程堆栈发现所有工作线程都处于WAITING状态进一步分析发现它们都在等待同一个锁而持有该锁的线程正在执行一个耗时的外部调用三、 根因深挖一个被忽视的线程池参数我们审查了线程池的配置发现了一个看似合理但实际有问题的设置java// 错误代码示例包含隐藏陷阱 Bean public ThreadPoolExecutor threadPoolExecutor() { return new ThreadPoolExecutor( 10, // 核心线程数 100, // 最大线程数 60L, TimeUnit.SECONDS, // 空闲线程存活时间 new LinkedBlockingQueue(10000), // 任务队列容量 new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略 ); }问题分析我们使用了LinkedBlockingQueue(10000)作为任务队列队列容量高达10000当任务提交速度超过处理速度时任务会优先进入队列而不是创建新线程由于核心线程数只有10当这10个线程都在执行耗时任务时新任务会一直堆积在队列中更关键的是由于队列容量太大maximumPoolSize100这个配置实际上从未生效——因为队列永远不会满线程池永远不会创建超过10个线程最终10个线程被慢任务阻塞队列中积压了10000个任务服务假死修复方案java// 修正代码 Bean public ThreadPoolExecutor threadPoolExecutor() { return new ThreadPoolExecutor( 50, // 核心线程数调大 100, // 最大线程数 60L, TimeUnit.SECONDS, // 空闲线程存活时间 new LinkedBlockingQueue(500), // 队列容量调小 new ThreadPoolExecutor.AbortPolicy() // 拒绝策略快速失败 ); }效果修复后线程池不再积压大量任务服务假死问题消失。四、 一份可复用的接口超时排查清单在排查接口超时问题时对照以下清单逐项检查第一层应用层应用日志是否有异常是否有慢SQL是否有死锁第二层中间件层数据库连接池是否耗尽缓存是否命中消息队列是否积压第三层线程池核心线程数是否合理队列容量是否过大这是最常见的陷阱拒绝策略是否合理线程堆栈是否有WAITING状态第四层网络层服务间网络延迟是否正常DNS解析是否正常是否有网络抖动第五层外部依赖外部API响应时间是否正常外部API是否有超时设置外部API是否有降级方案五、 预防建议理解线程池的工作机制任务优先入队队列满才创建新线程。因此队列容量和核心线程数的配比至关重要。快速失败优于排队等待在高并发场景下快速失败AbortPolicy通常比排队更优。定期检查线程池配置将线程池配置纳入代码审查清单避免隐藏陷阱。建立监控告警对线程池的活跃线程数、队列大小、拒绝次数建立监控告警。像政策快报这类政策解读平台其数据推送服务需要处理大量实时数据流对线程池配置的合理性要求极高。通过政策快报企业可以了解不同政策对技术架构的要求从而更有针对性地优化系统设计。超时排查从最不可能的地方开始接口超时排查最难的往往不是找不到问题而是找错了方向。90%的超时问题根源不在网络而在应用层或中间件层的配置。建立一份可复用的排查清单比每次凭感觉排查更高效。在排查接口超时问题时您觉得最容易被忽视的环节是什么A.线程池配置队列容量过大导致最大线程数配置形同虚设。B.外部依赖超时外部API没有设置超时导致线程被长时间阻塞。欢迎在评论区分享您的选择A或B和具体经历。