线程池饱和与任务堆积
线程池饱和表示任务到达或执行时间已经超过当前服务能力。排查时要先确认压力来自流量增长、任务变慢还是线程无法取得下游资源,再决定限流、隔离还是扩容。
1. 用一组指标确认饱和
单看线程数不够。对每个执行器至少记录:
poolSize、activeCount与最大线程数。- 队列长度、容量和最老任务等待时间。
- 提交、开始、完成、失败和拒绝速率。
- 任务执行时间与端到端时间分位数。
- 线程池关闭状态和 Worker 异常退出。
active 达到上限、队列持续增长、完成速率低于提交速率,才构成清晰的积压证据。瞬时队列峰值可能只是被正常吸收的突发。
2. 区分流量增加和服务时间变长
Little 定律说明,在吞吐不变时任务耗时增加也会推高在途量。排查时对齐时间线:
- 提交速率是否先升高。
- 数据库或 HTTP 依赖延迟是否先变差。
- GC、锁等待或 CPU 是否同步变化。
- 哪一类任务占据 Worker,是否出现新版本或大 key。
若所有线程都在等数据库连接,扩大线程池只会让更多线程进入同一等待队列。
3. 检查池内任务等待同池子任务
Future<Result> child = executor.submit(this::loadChild);
return child.get();
父任务占满全部 Worker,又同步等待提交到同一个池的子任务时,子任务只能留在队列,形成线程池饥饿。线程都可能是 WAITING,JVM 却不会报告 monitor 死锁。
可以拆分执行器、改为非阻塞组合,或让父任务直接完成工作。简单增加线程数只能推迟相同结构再次发生。
4. 队列里的任务可能已经失去价值
请求在队列等待时间超过客户端截止时间后,即使执行也可能只产生浪费和额外下游压力。任务应携带截止时间,在开始前检查剩余预算;幂等允许时可以放弃过期任务。
不能丢弃的异步工作应进入持久消息系统,并建立消费延迟、重试和死信策略,而不是依赖 JVM 内存队列承担可靠性。
5. 恢复顺序先阻止继续恶化
常见恢复顺序:
- 限制新流量,启用显式拒绝或降级。
- 隔离慢依赖或异常任务类型。
- 取消已过期且可安全放弃的任务。
- 修复下游、锁竞争或任务实现。
- 有证据表明真实容量不足时再调整池和实例数量。
临时扩容要同时检查数据库、缓存和第三方接口的总并发,否则会扩大故障。
6. 常见问题
6.1 队列长度为零为什么仍可能饱和
使用 SynchronousQueue 时本来就不保存任务;或者所有 Worker 都在慢任务中,提交直接被拒绝。还要看 active、最大线程和拒绝数。
6.2 任务完成数还在增长,说明没有问题吗
不说明。完成速率可能低于到达速率,积压与最老任务年龄仍会持续增长。
7. 面试题
7.1 线程池任务持续堆积时怎样排查
出现公司:美团、阿里巴巴
考察重点
- 到达速率、完成速率与任务耗时。
- 线程栈和下游资源等待。
- 同池父子任务饥饿与恢复顺序。
相关内容:第 1 节“用一组指标确认饱和”至第 5 节“恢复顺序先阻止继续恶化”。
参考回答
先确认 active 是否到上限、队列和最老任务年龄是否持续增长,以及提交速率是否超过完成速率。然后采集多份线程栈,按任务类型聚合,结合数据库连接等待、下游延迟、锁和 CPU 时间线判断是流量增加还是服务时间变长。
还要检查池内任务是否同步等待同池子任务。恢复时先限流或拒绝新任务,隔离慢依赖并放弃已过期工作;只有证据表明确实缺少可用计算容量时才扩池,并同时核对下游上限。