做微服务的人,基本都见过这样一条报错:java.util.concurrent.RejectedExecutionException: Thread pool is EXHAUSTED。我第一次遇到时,第一反应是服务器扛不住了,赶紧让运维加机器。结果机器翻了一倍,问题只是从“重度打满”变成“轻度排队”,该超时的照样超时。后来才明白,微服务架构下,Dubbo线程池配置从来不是填几个数字那么简单——它决定了请求在峰值时是排队、拒绝还是优雅地扛下来,也决定了一个慢接口会不会把整个服务拖垮。这篇文章把我这些年调Dubbo线程池踩过的坑、验证过的计算公式、应急排查的完整路径一次讲清楚,适合正在做微服务拆分、或者已经被线程池问题折磨过几轮的后端同学。
1. 从一个“Thread pool is EXHAUSTED”事故说起
1.1 事故表象与第一反应
那次事故发生在一次促销活动预热期,订单服务的QPS大概比平时涨了4倍。consumer端监控先开始报警,接口TP99从30ms直接飙到1200ms,紧接着大量调用超时;provider端日志里刷屏的就是开篇那句Thread pool is EXHAUSTED。当时团队的第一反应是扩容,以为把provider从2台加到6台就能顶住。结果确实缓解了半小时,但下一个流量波峰一来,同样的报错再次出现。
后来我们把这6台机器的线程池状态拉出来看,才发现每台机器的200个工作线程全部处于RUNNABLE或WAITING状态,队列已经满了,新请求直接被拒绝。此时CPU其实只有40%左右,内存也很健康,问题的核心根本不在机器资源,而在线程池参数跟业务模型完全不匹配——200个线程被一部分慢接口占着不放,快接口连执行的机会都没有。
1.2 排查结论:参数基本照搬默认值
翻代码的时候我更头疼,因为provider的dubbo:protocol配置里几乎没有写线程池相关的参数,也就是说全部用的是Dubbo默认值:线程池类型fixed,线程数200,队列为0,拒绝策略是AbortPolicy。
这套默认值在低并发下非常好用,简单粗暴:200个线程同时干活,超出部分直接拒绝,让consumer端快速感知失败并走降级逻辑。但一旦碰到两类典型场景就会炸:第一,有个别接口耗时特别长(比如倒计时批量查询、导出),长时间霸占工作线程;第二,流量有短时突刺,200个线程瞬间用完,队列又是0,没有任何缓冲,请求全部被挡在门外。
我把默认配置改成eager模式,核心线程调到100,最大线程拉到300,队列给到500之后,同样6台机器,TP99从1200ms降回80ms,拒绝报错彻底消失。这次改动真正的价值不是某个数字,而是让我意识到:在微服务架构里,线程池是每个provider实例的“并发上限”,理解它的工作模型,比抄一个网上推荐的配置重要得多。
1.3 为什么微服务架构下线程池问题更容易被放大
单体应用时代,线程池打满的影响通常局限于某个模块,用户最多觉得某个功能变慢。但微服务拆分之后,一次调用链路往往要经过3到5个服务,provider线程池被打满,consumer端会立刻超时,而Dubbo默认的重试机制会把这个超时请求再发两遍——一个请求瞬间变成三个请求,对下游线程池形成二次冲击,雪崩就是这么来的。
所以线程池配置在微服务架构下不只是“性能调优”,而是稳定性设计的一部分。它要和超时时间、重试次数、限流降级策略一起通盘考虑。下面我先把Dubbo线程池的底层模型讲清楚,再给具体的参数配置和场景建议。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Dubbo线程池模型拆解:请求到底在哪里排队
2.1 Provider端的双线程模型:IO线程与业务线程
很多人对Dubbo线程模型的理解比较模糊,以为请求进来之后就是在某个线程池里跑业务逻辑。实际上,Dubbo provider端有两层线程:底层Netty的IO线程(EventLoop)和上层业务线程池。
Netty IO线程负责网络读写、协议编解码,它收到完整请求后会通过ExecutorRepository拿到对应的业务线程池,把任务提交进去执行。这样设计的好处是IO线程不能被业务逻辑阻塞,否则会导致大量连接无法处理新请求。业务线程池才是我们今天的主角,它的任务就是执行真正的方法调用。
默认情况下,Dubbo的dispatcher是all,也就是连接建立、请求派发、响应返回等所有事件都会走业务线程池。绝大多数场景不需要改这个配置,但理解它很重要:当业务线程池被打满时,不仅是业务方法执行不了,连TCP连接的元数据事件也在排队,client端看到的现象就是连接有,但请求永远没有响应。
2.2 内置线程池类型对比:fixed、cached、limited、eager
Dubbo通过SPI机制提供了四种内置线程池:fixed、cached、limited、eager,它们的核心差异在“线程数上限”和“排队策略”上。
| 线程池类型 | 核心线程数 | 最大线程数 | 队列策略 | 适用场景 |
|---|---|---|---|---|
| fixed | 默认200,可配置 | 等于核心线程数 | 默认SynchronousQueue | 常规稳定流量,不接收突发 |
| cached | 0 | Integer.MAX_VALUE | 默认SynchronousQueue | 短任务、低并发、需要弹性回收 |
| limited | 0 | 默认200 | 有界队列 | 控制最大并发,较冷门 |
| eager | 默认200,可配置 | 默认 |
