前言
线上服务性能排查、流量压测、容量评估,两个核心指标必不可少:
- 当前正在执行业务的请求并发量(仅统计 Controller 业务接口,过滤静态资源、健康检查、Actuator)
- Tomcat 线程池排队等待的请求堆积数量(线程打满后新请求阻塞队列长度,超时预警核心依据)
很多同学只会依赖 Actuator 指标,但存在两个痛点:
- tomcat.threads.busy 统计所有占用 Tomcat 线程的请求(静态资源、WebSocket、异步长连接都会计入,业务并发失真)
- 想本地接口实时查看、自定义告警逻辑,需要代码直接读取 Tomcat 底层线程池队列
本文提供两套无冲突方案:
- SpringMVC 拦截器精准统计业务 Controller 真实并发
- 读取内嵌 Tomcat 底层对象获取排队等待请求数
两套代码可共存,统一监控接口对外输出全量指标。
一、方案 1:拦截器精准统计业务请求并发(推荐,贴合业务视角)
实现原理
基于HandlerInterceptor,只拦截 Controller 业务方法(HandlerMethod):
- preHandle:进入 Controller 逻辑前,并发计数器 + 1
- afterCompletion:请求彻底结束(无论正常 / 异常),计数器 - 1
使用AtomicInteger保证并发计数线程安全,自动过滤静态资源、页面跳转、404 无效路径。
1. 并发统计拦截器
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| java import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.method.HandlerMethod; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.concurrent.atomic.AtomicInteger; @Slf4j @Component public class RequestConcurrencyInterceptor implements HandlerInterceptor { /** * 原子计数器:当前正在处理的Controller业务请求并发数 * 仅统计接口方法,静态资源、websocket、过滤器逻辑不计入 */ private final AtomicInteger concurrentReq = new AtomicInteger(0); /** * Controller执行前计数+1 */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { // 只拦截@RestController/@Controller中的方法 if (handler instanceof HandlerMethod) { concurrentReq.incrementAndGet(); } return true; } /** * 请求完全处理完毕(正常返回/抛出异常都会执行)计数-1 */ @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { if (handler instanceof HandlerMethod) { concurrentReq.decrementAndGet(); } } /** * 对外暴露获取实时业务并发 * @return 当前正在处理的Controller请求数量 */ public int getCurrentConcurrency() { return concurrentReq.get(); } } |
2. 注册拦截器到 SpringMVC
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| java import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import javax.annotation.Resource; @Configuration public class WebConfig implements WebMvcConfigurer { @Resource private RequestConcurrencyInterceptor interceptor; @Override public void addInterceptors(InterceptorRegistry registry) { // 拦截全部路径,内部自动过滤非Controller请求 registry.addInterceptor(interceptor) .addPathPatterns("/**"); } } |
指标说明(和 Tomcat 活跃线程区分)
拦截器getCurrentConcurrency():纯业务接口并发
- 只统计@RequestMapping标注的接口方法
- 静态资源、/actuator 健康检查、WebSocket 长连接不会累加计数
- 异步接口Callable/WebAsyncTask释放 Tomcat 线程后,计数同步下降,贴合真实业务并发
Tomcat getActiveCount():Tomcat 工作线程活跃数
- 包含所有占用线程的请求,容易虚高
- 长连接、异步场景指标失真
二、方案 2:代码读取 Tomcat 排队等待请求堆积数
实现原理
通过ServletWebServerApplicationContext获取内嵌 Tomcat 连接器,强转为StandardThreadExecutor,直接读取线程池任务队列当前长度:
getQueueSize() = 线程池全部繁忙后,排队等待分配线程的请求数量。
Tomcat 队列监控 Controller
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| java import org.apache.catalina.connector.Connector; import org.apache.catalina.core.StandardThreadExecutor; import org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; import java.util.HashMap; import java.util.Map; @RestController public class TomcatQueueMonitorController { @Resource private ServletWebServerApplicationContext webServerCtx; @Resource private RequestConcurrencyInterceptor concurrencyInterceptor; /** * 完整Tomcat监控指标接口 * 1. 业务真实并发(拦截器统计) * 2. Tomcat线程池排队等待请求数 * 3. Tomcat活跃工作线程数 * 4. 最大线程上限、当前线程池总大小 */ @GetMapping("/monitor/tomcat/stat") public Map<String, Object> getTomcatFullStat() { // 获取第一个端口的Tomcat连接器 Connector connector = webServerCtx.getWebServer().getConnectors()0; StandardThreadExecutor executor = (StandardThreadExecutor) connector.getProtocolHandler().getExecutor(); Map<String, Object> result = new HashMap<>(8); // 1. 拦截器统计:仅Controller业务请求并发(推荐业务监控使用) result.put("businessConcurrency", concurrencyInterceptor.getCurrentConcurrency()); // 2. Tomcat底层活跃工作线程(包含静态资源、长连接等) result.put("tomcatActiveThread", executor.getActiveCount()); // 3. 队列中排队未分配线程的请求堆积数(核心预警指标) result.put("waitQueueSize", executor.getQueueSize()); // 4. Tomcat配置最大工作线程 result.put("maxThreads", executor.getMaxThreads()); // 5. 当前线程池已创建总线程 result.put("currentPoolSize", executor.getPoolSize()); return result; } /** * 单独获取排队堆积请求数(兼容旧逻辑) */ @GetMapping("/monitor/tomcat/queue") public long getWaitRequestCount() { Connector connector = webServerCtx.getWebServer().getConnectors()0; StandardThreadExecutor executor = (StandardThreadExecutor) connector.getProtocolHandler().getExecutor(); return executor.getQueueSize(); } } |
三、关键指标释义与线上判断标准
访问接口 http://ip:port/monitor/tomcat/stat 得到 JSON,各字段含义:
|---------------------|-----------------------|------------------------------------------------------------------------------------------------------------|
| 字段 | 含义 | 线上风险判断 |
| businessConcurrency | 拦截器统计,正在处理的业务接口并发 | 业务流量真实水位,压测、限流参考值 |
| tomcatActiveThread | Tomcat 活跃工作线程总数 | 包含静态资源、websocket,数值通常大于 businessConcurrency |
| waitQueueSize | 排队等待线程的请求堆积数量 | 1. tomcatActiveThread == maxThreads 且 waitQueueSize 持续上涨 → 服务处理能力不足,大量请求排队超时 2. waitQueueSize 长期 > 50 需告警 |
| maxThreads | Tomcat 最大工作线程(默认 200) | 可通过server.tomcat.threads.max调大 |
| currentPoolSize | 线程池已创建线程总数 | 冷启动缓慢扩容,流量平稳后稳定 |
四、使用限制与注意事项
- 容器限制
代码强依赖 Tomcat 内置类,若项目切换 Jetty/Undertow 内嵌容器,强转StandardThreadExecutor会抛类型转换异常,此时只能使用 Actuator 指标。 - 多端口服务兼容
getConnectors()0仅读取第一个监听端口;多端口服务需循环遍历数组,分别采集每个 Connector 的线程池指标。 - 异步接口指标差异
使用Callable、WebAsyncTask异步接口时,Tomcat 工作线程会立刻释放:
- tomcatActiveThread 快速下降
- businessConcurrency 仍会正常计数,更贴合业务真实并发场景
生产长期监控推荐方案
代码接口适合临时排查、本地压测;长期监控大盘建议开启 Actuator 导出标准化指标对接 Prometheus:
|---------------------------------------------------------------------------------------------------------------------|
| yaml server: tomcat: mbeanregistry: enabled: true management: endpoints: web: exposure: include: metrics,prometheus |
核心指标:
- tomcat.threads.busy:Tomcat 活跃线程
- tomcat.threads.queue-size:排队队列长度
可结合自定义 Gauge 将拦截器businessConcurrency注册到 micrometer 统一监控。
五、线上调优建议
- 当waitQueueSize持续上涨、接口大量超时:
- 优先优化慢接口(数据库慢查询、第三方远程调用),减少单请求耗时
- 适度调大 Tomcat 最大线程数:
|------------------------------------------------------------------------|
| yaml server: tomcat: threads: max: 400 min-spare: 50 accept-count: 200 |
- 限流阈值参考:以businessConcurrency作为限流判断依据,不要依赖 Tomcat 原生活跃线程(存在虚高干扰)。
六、总结
- 拦截器计数优势:精准过滤非业务请求,异步场景数值不失真,最贴合业务层并发统计需求;
- Tomcat 底层队列读取优势:直接获取排队堆积请求数,快速判断流量打满、请求阻塞;
- 两套代码可共存,统一监控接口输出全维度指标,兼顾临时排查与线上容量评估场景。