前言

线上服务性能排查、流量压测、容量评估,两个核心指标必不可少:

  • 当前正在执行业务的请求并发量(仅统计 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

线程池已创建线程总数

冷启动缓慢扩容,流量平稳后稳定

四、使用限制与注意事项

  1. 容器限制
    代码强依赖 Tomcat 内置类,若项目切换 Jetty/Undertow 内嵌容器,强转StandardThreadExecutor会抛类型转换异常,此时只能使用 Actuator 指标。
  2. 多端口服务兼容
    getConnectors()[0]仅读取第一个监听端口;多端口服务需循环遍历数组,分别采集每个 Connector 的线程池指标。
  3. 异步接口指标差异
    使用CallableWebAsyncTask异步接口时,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 统一监控。

五、线上调优建议

  1. waitQueueSize持续上涨、接口大量超时:
  • 优先优化慢接口(数据库慢查询、第三方远程调用),减少单请求耗时
  • 适度调大 Tomcat 最大线程数:

yaml
server:
  tomcat:
    threads:
      max: 400
      min-spare: 50
    accept-count: 200

  1. 限流阈值参考:以businessConcurrency作为限流判断依据,不要依赖 Tomcat 原生活跃线程(存在虚高干扰)。

六、总结

  • 拦截器计数优势:精准过滤非业务请求,异步场景数值不失真,最贴合业务层并发统计需求;
  • Tomcat 底层队列读取优势:直接获取排队堆积请求数,快速判断流量打满、请求阻塞;
  • 两套代码可共存,统一监控接口输出全维度指标,兼顾临时排查与线上容量评估场景。

更多推荐