介绍一下 Tomcat 的 IO 模型?

#Java#面试八股

介绍一下 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 实现

实现协议名模型状态
BIOHttp11Protocol一连接一线程Tomcat 8.5 起移除
NIOHttp11NioProtocolSelector 多路复用默认,主流
NIO2Http11Nio2ProtocolJDK AIO(异步回调)可用,Linux 上底层仍是 epoll,收益有限
APRHttp11AprProtocolnative(APR + OpenSSL)依赖本地库,新版本逐步弱化

protocol 不写时 Tomcat 会自动选择 NIO(Tomcat 9 起 protocol="HTTP/1.1" 也会解析为 NIO 实现)。

3. 核心参数

参数默认值含义
maxThreads200Worker 线程池上限,决定并发处理能力
minSpareThreads10最小空闲线程数
acceptCount100全连接队列长度,满了新连接被拒绝
maxConnections8192(NIO)同时保持的连接数上限
connectionTimeout20000ms建连/读请求头的超时
keepAliveTimeout= connectionTimeoutkeep-alive 空闲超时
maxKeepAliveRequests100单连接最多复用次数

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 这类全异步框架最本质的分野。

笔记加载中…