介绍一下 Tomcat 的 IO 模型?
结论先行:Tomcat 8.5 之后默认使用 NIO(Http11NioProtocol):一个 Acceptor 线程负责 accept 新连接,把 SocketChannel 注册到 Poller(基于 Selector 的 IO 多路复用)上,就绪的读写事件再交给 Worker 线程池处理业务。此外还有 NIO2(JDK AIO)与 APR(native,逐步退场),而最早的 BIO 已在 8.5 中被移除。
1. 一次请求在 Tomcat 内的流转
客户端连接
│
▼
Acceptor 线程(循环 accept)
│ socketChannel 注册 OP_READ
▼
Poller 线程(Selector 轮询)
│ 有事件 → 封装 SocketProcessor 投递
▼
Executor / Worker 线程池(默认 maxThreads=200)
│ 解析 HTTP → 交给 Servlet 容器(CoyoteAdapter → Engine)
▼
业务代码(Spring MVC)
关键点:Acceptor 与 Poller 只做 IO 事件的分发,不执行业务;业务全部在 Worker 线程里跑。所以「连接数」与「线程数」是两个维度:NIO 下可以有几万连接,但只有 200 个线程在处理。
2. 四种 Connector 实现
| 实现 | 协议名 | 模型 | 状态 |
|---|---|---|---|
| BIO | Http11Protocol | 一连接一线程 | Tomcat 8.5 起移除 |
| NIO | Http11NioProtocol | Selector 多路复用 | 默认,主流 |
| NIO2 | Http11Nio2Protocol | JDK AIO(异步回调) | 可用,Linux 上底层仍是 epoll,收益有限 |
| APR | Http11AprProtocol | native(APR + OpenSSL) | 依赖本地库,新版本逐步弱化 |
protocol 不写时 Tomcat 会自动选择 NIO(Tomcat 9 起 protocol="HTTP/1.1" 也会解析为 NIO 实现)。
3. 核心参数
| 参数 | 默认值 | 含义 |
|---|---|---|
maxThreads | 200 | Worker 线程池上限,决定并发处理能力 |
minSpareThreads | 10 | 最小空闲线程数 |
acceptCount | 100 | 全连接队列长度,满了新连接被拒绝 |
maxConnections | 8192(NIO) | 同时保持的连接数上限 |
connectionTimeout | 20000ms | 建连/读请求头的超时 |
keepAliveTimeout | = connectionTimeout | keep-alive 空闲超时 |
maxKeepAliveRequests | 100 | 单连接最多复用次数 |
acceptCount 满了表现为客户端收到 Connection refused 或连接超时;maxConnections 满了则新连接排队等待。
4. server.xml 配置示例
<Connector port="8080"
protocol="org.apache.coyote.http11.Http11NioProtocol"
maxThreads="400"
minSpareThreads="50"
acceptCount="200"
maxConnections="10000"
connectionTimeout="8000"
keepAliveTimeout="15000"
maxKeepAliveRequests="200"
compression="on"
compressionMinSize="2048"
compressibleMimeType="application/json,text/html,text/plain"
URIEncoding="UTF-8" />
Spring Boot 内嵌 Tomcat 的等价配置:
server:
tomcat:
threads:
max: 400
min-spare: 50
accept-count: 200
max-connections: 10000
connection-timeout: 8s
keep-alive-timeout: 15s
port: 8080
5. NIO 与 Netty 的区别
Tomcat 的 NIO 是「多路复用 + 线程池」:Poller 负责事件,Worker 负责阻塞式业务。Netty 是「Reactor + 事件驱动全链路」:业务回调也在 EventLoop 里执行(或用业务线程池),没有 Servlet 那层阻塞语义。
所以 Tomcat 的 NIO 并不是「全异步」:业务代码若阻塞(查库、调下游),Worker 线程依然被占着。这正是虚拟线程(Loom)要解决的问题——Tomcat 10.1 支持 VirtualThreadExecutor,用虚拟线程替代平台线程池,阻塞时自动让出载体线程。
6. 常见追问
追问一:NIO 下为什么还需要 200 个线程? 因为业务是阻塞的。Selector 只负责「数据到了没有」,解析请求、执行 Controller、写数据库都在 Worker 里同步完成,慢业务会占住线程,所以仍需要线程池,只是线程不再与连接一一对应。
追问二:maxConnections 和 maxThreads 是什么关系?
maxConnections 限制同时保持的连接(含 keep-alive 空闲连接),maxThreads 限制同时处理业务的线程。NIO 下 maxConnections 可以远大于 maxThreads,多出来的连接在 Poller 里等待 Worker。
追问三:为什么 AIO(NIO2)没有流行起来? Linux 上 JDK 的 AIO 底层仍是 epoll 模拟,并没有真正的内核异步 IO 优势;而回调式编程模型复杂、调试难。Netty 也明确不主推 AIO 就是这个原因。
追问四:keep-alive 会占用 Worker 线程吗? 不会。keep-alive 的空闲连接停留在 Poller 的 Selector 上,不占 Worker 线程,只有新请求到达才分配线程处理。
追问五:Tomcat 能承受多少并发连接?
取决于 maxConnections、文件描述符上限(ulimit -n)、内存与业务耗时。NIO 下几万连接很常见,但「连接数」不等于「QPS」,真正的瓶颈通常是 Worker 线程与下游依赖。
小结
Tomcat 的 IO 模型可以记成三层:Acceptor 收连接、Poller 等事件、Worker 跑业务。它用 NIO 把「连接」和「线程」解耦,但仍然保留阻塞式的 Servlet 编程模型,这也是它与 Netty 这类全异步框架最本质的分野。