Tomcat 线程池最大 200,压测发现 150 即拒绝,如何定位是队列长度还是连接等待
解读
面试官把“最大线程数 200”当成已知条件,却观察到 150 并发就触发拒绝,目的是考察候选人能否把“线程池模型”与“Tomcat 网络层”两条线拆开排查。
国内生产环境普遍用 SpringBoot 内嵌 Tomcat,默认配置往往把 acceptCount、maxConnections 留在“看起来够用”的缺省值,结果压测时先碰到连接层瓶颈,却误判成线程池问题。
面试官想听的不是背参数,而是“先分层、再定量、最后拿日志/监控验证”的闭环思路,顺带验证你对 JDK 线程池、Linux 内核队列、Tomcat 8.5/9/10 不同版本差异的掌握程度。
知识点
- Tomcat 线程池与 JDK ThreadPoolExecutor 的映射关系:maxThreads 对应 corePoolSize=0、maximumPoolSize=maxThreads,队列长度由 maxQueueSize(默认 Integer.MAX_VALUE)控制,但 Tomcat 自己用 TaskQueue hack 了 offer 逻辑,队列未满不会创建到 maxThreads。
- 网络层两条队列:
a) OS 层全连接队列(syn backlog)→ 由 server.xml 的 acceptCount 决定,缺省 100;
b) Tomcat 内部 Poller 的 maxConnections(NIO 默认 10000,APR/native 默认 8192)。 - 拒绝时机:
- 连接层拒绝:acceptCount 满,内核直接丢 SYN 包,客户端报 Connection timeout/refused;
- 线程池拒绝:线程数已达 maxThreads 且队列满,抛出 RejectedExecutionException,Tomcat 返回 500/503。
- 国内常用监控:
- 应用日志:catalina.out 里是否出现 RejectedExecutionException;
- access log:%D(耗时)与 %s(状态码)组合,出现 503 且耗时极短,大概率线程池拒;
- ss -lnt | grep 8080 看 Send-Q 值与 drops;
- arthas:thread --state | grep BLOCKED 是否卡在 TaskQueue;
- JDK JMX:TomcatThreadPool|currentThreadsBusy、queueSize。
- 压测端现象区分:
- 连接层拒:JMeter/Locust 报 Non HTTP response code: java.net.ConnectException;
- 线程池拒:成功建立 TCP,但收到 503,且 Response time 几乎为 0。
答案
回答时分三步,先给结论再给验证命令,体现“可落地”。
-
先判断拒绝发生在哪一层
a) 压测端抓异常:- 若 JMeter 大量 ConnectException/Connection refused,且服务端 CPU 不高,基本可定在全连接队列满;
- 若全部 TCP 三次握手成功,但接口返回 503/500,且 Tomcat 日志出现 RejectedExecutionException,则是线程池拒。
b) 服务端快速验证: ss -lnt | grep :8080看 Send-Q 值是否持续等于 acceptCount,且 drops 列递增;dmesg | grep "TCP: drop open request"有计数则确认全连接队列溢出。
-
若是线程池层,再区分是“线程数真到 200”还是“队列未满导致线程提前停涨”
a) 用 JMX 或 arthas:
java -jar arthas-boot.jar→thread --state统计 RUNNABLE 数量;
ognl @org.apache.tomcat.util.threads.ThreadPoolExecutor@getMaximumPoolSize()与getActiveCount()、getQueue().size()对比。
b) 若 activeCount 远小于 200 且队列已积压,说明 TaskQueue 的 hack 逻辑生效,此时把 maxQueueSize 显式配小(如 0 或 50),线程就会涨到 200;再压测,若 200 线程仍拒,则瓶颈在 maxThreads,需调大或优化代码。 -
若是连接层,直接调大 acceptCount(如 2000)并确认内核 net.core.somaxconn ≥ 2048,再复测;若 maxConnections 先到顶,则调大 server.xml 中 maxConnections(或改用 APR 协议)。
一句话总结:
“先看客户端异常类型,再看服务端日志和队列监控,最后用 arthas/JMX 量化线程、队列、连接三条指标,就能一分钟定位是 acceptCount、maxConnections 还是 maxThreads 的锅。”
拓展思考
- 如果线程池调到 500 仍然 150 并发就拒,下一步如何继续缩小范围?
提示:检查数据库连接池、Redis 连接池是否同步阻塞,导致 Tomcat 线程被挂住,表象像“线程不够”,实为“线程被等待”。 - 在 Kubernetes 环境,Pod 级别还有 ingress-controller 的 upstream-keepalive、service 的 conntrack 表上限,如何把“Tomcat 拒”与“Sidecar 拒”区分开?
提示:在 Pod 内 tcpdump 看收到 SYN 但无 ACK,则问题在 Pod 前层;若三次握手完成但无 HTTP 响应,则问题在容器内。 - 国内金融场景常开 JVM 的 -XX:+UseContainerSupport,但 cgroup 的 cpu quota 只有 2 核,导致线程上下文切换暴涨,TPS 提前雪崩,如何量化是“CPU 饥饿”还是“线程池/连接池”瓶颈?
提示:用perf top -p <pid>看 _spin_lock 占比,同时对比vmstat 1的 cs 列,若 cs > 150K 且 sys% 高,优先缩线程数而非继续加。