跳到主要内容

线程池饱和与任务堆积

线程池饱和表示任务到达或执行时间已经超过当前服务能力。排查时要先确认压力来自流量增长、任务变慢还是线程无法取得下游资源,再决定限流、隔离还是扩容。

1. 用一组指标确认饱和

单看线程数不够。对每个执行器至少记录:

  • poolSizeactiveCount 与最大线程数。
  • 队列长度、容量和最老任务等待时间。
  • 提交、开始、完成、失败和拒绝速率。
  • 任务执行时间与端到端时间分位数。
  • 线程池关闭状态和 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. 恢复顺序先阻止继续恶化

常见恢复顺序:

  1. 限制新流量,启用显式拒绝或降级。
  2. 隔离慢依赖或异常任务类型。
  3. 取消已过期且可安全放弃的任务。
  4. 修复下游、锁竞争或任务实现。
  5. 有证据表明真实容量不足时再调整池和实例数量。

临时扩容要同时检查数据库、缓存和第三方接口的总并发,否则会扩大故障。

6. 常见问题

6.1 队列长度为零为什么仍可能饱和

使用 SynchronousQueue 时本来就不保存任务;或者所有 Worker 都在慢任务中,提交直接被拒绝。还要看 active、最大线程和拒绝数。

6.2 任务完成数还在增长,说明没有问题吗

不说明。完成速率可能低于到达速率,积压与最老任务年龄仍会持续增长。

7. 面试题

7.1 线程池任务持续堆积时怎样排查

出现公司:美团、阿里巴巴

考察重点

  • 到达速率、完成速率与任务耗时。
  • 线程栈和下游资源等待。
  • 同池父子任务饥饿与恢复顺序。

相关内容:第 1 节“用一组指标确认饱和”至第 5 节“恢复顺序先阻止继续恶化”。

参考回答

先确认 active 是否到上限、队列和最老任务年龄是否持续增长,以及提交速率是否超过完成速率。然后采集多份线程栈,按任务类型聚合,结合数据库连接等待、下游延迟、锁和 CPU 时间线判断是流量增加还是服务时间变长。

还要检查池内任务是否同步等待同池子任务。恢复时先限流或拒绝新任务,隔离慢依赖并放弃已过期工作;只有证据表明确实缺少可用计算容量时才扩池,并同时核对下游上限。