Tomcat 到底能扛多少请求?从 Servlet、线程和队列开始讲清楚
前言
很多初学者第一次看到“Tomcat 最大线程数”“连接数”和“等待队列”时,会直接陷入参数细节。其实在讨论容量之前,更重要的是先回答三个问题:浏览器请求怎样到达 Controller?Tomcat 和 Servlet 是什么关系?请求为什么会排队?
本文从一条请求的完整旅程讲起,再用一个具体数字的例子解释 maxThreads、maxConnections 和 acceptCount。文中的实验用于理解机制,不代表所有机器和项目都有相同的性能。
目录
文章目录
一个请求怎样到达 Controller
假设我们写了一个 Spring Boot 接口:
@RestController
public class DemoController {
@GetMapping("/hello")
public String hello() {
return "hello";
}
}
浏览器访问 /hello 时,请求并不是直接跳进这个 Java 方法,而是经过下面的链路:
浏览器发送 HTTP 请求
↓
Tomcat 在端口上收到请求
↓
Tomcat 安排请求处理线程
↓
调用 Spring MVC 的 DispatcherServlet
↓
DispatcherServlet 找到 /hello 对应的 Controller
↓
Controller 执行业务代码
↓
结果沿原路返回浏览器
要理解这条链路,首先需要分清 Tomcat、Servlet 和 Spring MVC。
Tomcat、Servlet 和 Spring MVC 到底是什么关系
Servlet 是规范
Servlet 是 Java Web 领域的一套规范。它规定了 Java 程序如何接收请求、处理请求和生成响应。
规范更像一份合同或接口定义,它告诉开发者“应该提供哪些能力”,但规范本身不会监听 8080 端口,也不会独立运行。
Tomcat 是 Servlet 容器
Tomcat 实现了 Servlet 规范,因此被称为 Servlet 容器。同时,Tomcat 也具备 HTTP 服务器能力,可以监听端口、解析 HTTP 请求并发送响应。
可以把二者类比成:
Servlet 规范:餐厅应该遵守的服务标准
Tomcat:按照标准运营的餐厅
JDK 本身不内置 Tomcat。常见的 Spring Boot Web 项目会通过依赖引入内嵌 Tomcat,执行 java -jar app.jar 时,Tomcat 和业务代码运行在同一个 Java 进程中。
Spring MVC 使用了一个核心 Servlet
Spring MVC 的核心入口叫 DispatcherServlet。它是一个 Servlet,由 Tomcat 管理。
我们的 Controller 通常不是 Servlet。Tomcat 先调用 DispatcherServlet,再由 DispatcherServlet 根据请求路径找到对应的 Controller。
Tomcat
└─ 管理 DispatcherServlet
└─ DispatcherServlet 分发请求
├─ /user → UserController
├─ /order → OrderController
└─ /hello → DemoController
所以可以记住:
Servlet 是规范。
Tomcat 是实现 Servlet 规范的容器。
DispatcherServlet 是 Spring MVC 的统一入口。
Controller 是我们编写的业务入口。
Connector 是 Tomcat 的“HTTP 大门”
Tomcat 可以配置一个或多个 Connector。本文讨论的 HTTP Connector,主要负责:
- 监听端口,例如 8080;
- 接受客户端连接;
- 解析 HTTP 请求;
- 调度请求处理线程;
- 把请求交给后面的 Servlet 容器处理。
可以暂时把它理解成 Tomcat 的 HTTP 大门:
客户端
↓
HTTP Connector
↓
请求处理线程
↓
DispatcherServlet
↓
Controller
maxThreads、maxConnections 和 acceptCount 都与这扇大门的承接能力有关,但它们限制的不是同一件事。
Nginx 和 Tomcat 有什么区别
生产环境中经常把 Nginx 放在多个 Java 服务前面:
用户
↓
Nginx
├─ HTTPS
├─ 反向代理
├─ 限流
└─ 负载均衡
↓
Java 服务 A(内嵌 Tomcat)
Java 服务 B(内嵌 Tomcat)
Java 服务 C(内嵌 Tomcat)
| 对比项 | Nginx | Tomcat |
|---|---|---|
| 常见位置 | Java 服务前方 | Java 服务内部或独立部署 |
| 主要职责 | 接收入口流量并转发 | 运行 Servlet Web 应用 |
| 是否调用 Controller | 通常不调用 | 通过 Spring MVC 间接调用 |
| 负载均衡 | 可以把请求分给多个实例 | 主要管理当前实例 |
最简单的记法是:
Nginx:这次请求转给哪台 Java 服务?
Tomcat:这台 Java 服务怎样接收并处理请求?
Nginx 不是必选组件。本地开发时通常是:
浏览器 → Spring Boot 内嵌 Tomcat → DispatcherServlet → Controller
请求数、QPS 和并发数
理解容量前,还需要分清三个基本概念。
请求数
请求数是一段时间内收到或完成的请求总量,例如一分钟处理了 3000 个请求。
QPS
QPS 表示每秒处理多少个请求。例如一分钟稳定完成 3000 个请求,平均吞吐量约为 50 QPS。
并发数
并发数表示某一时刻系统中还有多少个请求没有完成,包括正在执行业务代码和正在等待资源的请求。
在系统处于稳定状态、统计口径一致时,可以近似使用:
平均并发数 ≈ 平均 QPS × 平均响应时间(秒)
例如平均有 10 个在途请求,平均响应时间为 0.5 秒:
QPS ≈ 10 ÷ 0.5 = 20
这只是帮助理解吞吐、并发和响应时间的关系,不是 Tomcat 的固定容量公式。
TCP 连接、HTTP 请求和工作线程不是一回事
客户端通常先建立 TCP 连接,再通过这条连接发送 HTTP 请求。
TCP 连接:通信通道
HTTP 请求:通道中传输的一次任务
工作线程:执行这次任务的 Java 线程
一条 HTTP Keep-Alive 连接可以先后承载多个请求。HTTP/2 还可以在一条连接上复用多个请求流。因此:
TCP 连接数 ≠ 在途请求数 ≠ Tomcat 活动线程数
这也是理解下面三个参数时最容易踩坑的地方。
三个关键参数分别限制什么
maxThreads:最多有多少个请求处理线程
maxThreads 控制 Connector 最多创建多少个请求处理线程。对于传统的同步 Servlet 请求,一个请求在执行期间通常会占用一个请求处理线程。
假设:
maxThreads = 2
那么同一时刻最多有 2 个请求处理线程工作:
线程 1:处理请求 A
线程 2:处理请求 B
请求 C:暂时没有请求处理线程可用
如果 Connector 绑定了共享 Executor,Connector 自己配置的 maxThreads 会被忽略,实际线程上限由 Executor 的配置决定。
maxConnections:Tomcat 最多维持多少条连接
maxConnections 限制 Tomcat 同时接受和处理的连接数量。
假设:
maxConnections = 5
表示 Tomcat 最多同时维持 5 条已接受的连接。这里限制的是连接,不是正在执行的请求。
即使只有 2 个请求处理线程,Tomcat 也可能已经接受 5 条连接:2 条连接上的请求正在执行,其余连接等待可用线程。
acceptCount:Tomcat 暂时不能接入时,系统还能排多少连接
当已接受的连接数达到 maxConnections 后,Tomcat 暂时不再接受更多连接。新的连接会在操作系统提供的连接队列中等待。
acceptCount 用于设置这个操作系统连接队列的最大长度。例如:
acceptCount = 3
表示达到 maxConnections 后,希望还能有最多 3 条新连接在操作系统队列中等待。
需要注意:这是操作系统层面的队列,操作系统可能根据自身实现和限制采用不同的实际长度。当这个队列也满了,后续连接可能被拒绝或最终超时。
用一个完整例子看懂三个参数
假设配置如下:
maxThreads = 2
maxConnections = 5
acceptCount = 3
为了让数字容易理解,这个例子暂时假设:
每个客户端新建一条连接
每条连接只发送一个慢请求
请求响应后立即关闭连接
所有请求几乎同时到达
现在有 9 个客户端同时连接 Tomcat。
第 1、2 条连接:立即取得线程
Tomcat 有 2 个请求处理线程,因此前两个请求立即执行:
连接 1 → 线程 1 正在执行
连接 2 → 线程 2 正在执行
此时:
活动线程:2 / 2
已接受连接:2 / 5
系统连接队列:0 / 3
第 3~5 条连接:Tomcat 已接受,但等待线程
Tomcat 还能接受 3 条连接,但已经没有空闲请求处理线程:
连接 3 ┐
连接 4 ├─ 已被 Tomcat 接受,等待请求处理线程
连接 5 ┘
此时:
活动线程:2 / 2
已接受连接:5 / 5
系统连接队列:0 / 3
这里达到了 maxConnections,但并不意味着 5 个请求都在同时执行业务代码。真正执行的仍然只有 2 个。
第 6~8 条连接:进入操作系统连接队列
Tomcat 已经达到 maxConnections,暂时不能接受更多连接。接下来的 3 条连接在操作系统提供的队列中等待:
连接 6 ┐
连接 7 ├─ 操作系统连接队列
连接 8 ┘
此时:
活动线程:2 / 2
已接受连接:5 / 5
系统连接队列:3 / 3
第 9 条连接:可能被拒绝或超时
请求处理线程已满、已接受连接达到上限、操作系统连接队列也达到上限。第 9 条连接可能无法继续排队,表现为连接被拒绝或等待后超时。
把整个场景画在一起:
maxThreads = 2
┌───────────────────────────────┐
│ 连接 1 → 线程 1 正在执行 │
│ 连接 2 → 线程 2 正在执行 │
│ │ maxConnections = 5
│ 连接 3 → 等待可用线程 │
│ 连接 4 → 等待可用线程 │
│ 连接 5 → 等待可用线程 │
└───────────────────────────────┘
┌───────────────────────────────┐
│ 连接 6 → 操作系统队列 │
│ 连接 7 → 操作系统队列 │ acceptCount = 3
│ 连接 8 → 操作系统队列 │
└───────────────────────────────┘
连接 9 → 可能被拒绝或超时
当线程 1 完成请求,并且连接 1 按本例假设关闭后:
- 连接 3 可以取得释放出来的线程;
- Tomcat 的已接受连接数降到上限以下;
- 操作系统队列中的一条连接可以被 Tomcat 接受;
- 后面的连接继续依次向前移动。
Keep-Alive 下为什么不能照搬这个人数图
真实项目通常会使用 Keep-Alive。一个请求完成后,TCP 连接可能不会立即关闭,而是保持空闲等待后续请求。
这时即使请求处理线程已经释放,连接仍可能计入连接数。因此不能简单认为“一个请求完成,就一定释放一个 maxConnections 名额”。
上面的 2、5、3 例子是为了解释三个参数处在不同层次,不能把它当作所有协议和配置下完全一致的内部实现图。
慢请求为什么会让用户等待更久
假设有一个同步接口:
/slow?ms=500
它执行 Thread.sleep(500) 模拟慢 SQL 或阻塞式下游调用。sleep 几乎不消耗 CPU,但会占用当前请求处理线程。
10 个线程处理 20 个慢请求
最大工作线程:10
同时到达请求:20
每个请求等待:约 500ms
前 10 个请求可以立即取得线程,后 10 个请求要等待第一批释放线程:
第 1 批:约 500ms 返回
第 2 批:约 1000ms 返回
5 个线程处理 20 个慢请求
如果只保留 5 个工作线程,20 个请求会近似分成 4 批:
第 1 批:约 500ms 返回
第 2 批:约 1000ms 返回
第 3 批:约 1500ms 返回
第 4 批:约 2000ms 返回
因此:
用户响应时间
= 等待时间
+ 业务执行时间
+ 框架和网络开销
业务代码只等待 500ms,不代表用户一定只等待 500ms。
在“请求耗时接近、主要是阻塞等待、线程持续繁忙”的实验条件下,可以用下面的除法粗略理解吞吐上界:
10 个线程 ÷ 0.5 秒 ≈ 20 QPS
5 个线程 ÷ 0.5 秒 ≈ 10 QPS
这不是通用容量公式。真实接口还会受到 CPU、数据库连接池、锁、JVM 和下游服务影响。
为什么不能只增加 maxThreads
假设 Tomcat 有 10 个工作线程,但数据库连接池只有 5 条连接:
10 个请求进入业务代码
├─ 5 个取得数据库连接
└─ 5 个等待数据库连接
此时把 maxThreads 增加到 100,不会让数据库突然拥有更多处理能力,只会让更多线程进入等待。
对于 CPU 密集型任务,过多线程还可能增加线程切换和内存开销。因此调优时要查看整条链路:
Tomcat 线程
↓
Java 代码与 JVM
↓
数据库连接池
↓
Redis、MQ 和其他下游服务
最先达到极限的资源,才是当前系统的主要瓶颈。
第一次压测为什么可能更慢
服务刚启动后的第一批请求可能包含额外开销,例如:
- 类加载和 JIT 编译;
- Spring 组件的懒初始化;
- 数据库或 HTTP 连接池首次建连;
- 缓存尚未填充;
- 压测客户端、网络或宿主机抖动。
只看两轮响应时间,不能断定唯一原因。正式压测通常需要:
启动服务
↓
小流量预热
↓
等待指标稳定
↓
逐级增加压力
↓
稳态运行并重复验证
冷启动性能也值得测试,但应和稳定运行时的容量分开记录。
容量测试应该看什么
Tomcat 能扛多少请求,不能只看某一个参数。压测时至少要观察:
- QPS;
- 平均响应时间;
- P95 和 P99;
- 错误率和超时率;
- CPU、内存和 GC;
- Tomcat 活动线程与连接数;
- 数据库连接池使用率和等待时间;
- Redis、MQ 和其他下游服务;
- 压测客户端是否先成为瓶颈。
真正的容量边界是:
在响应时间和错误率满足 SLA、资源仍保留合理余量时,系统能够长期稳定维持的最大负载。
最后记住七句话
1. Servlet 是规范,Tomcat 是实现 Servlet 规范的容器。
2. DispatcherServlet 是 Spring MVC 的统一入口,Controller 由它分发调用。
3. Nginx 常负责入口和转发,Tomcat 负责运行 Java Web 应用。
4. TCP 连接、HTTP 请求和请求处理线程不是同一个概念。
5. maxThreads 限制请求处理线程数。
6. maxConnections 限制 Tomcat 同时维持的连接数。
7. acceptCount 限制达到连接上限后,操作系统还能排队的连接数。
参考资料
- Apache Tomcat HTTP Connector 官方文档:https://tomcat.apache.org/tomcat-10.0-doc/config/http.html
- Spring Boot 内嵌 Web Server 官方文档:https://docs.spring.io/spring-boot/how-to/webserver.html
- Nginx HTTP Load Balancing 官方文档:https://nginx.org/en/docs/http/load_balancing.html
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)