NIO Endpoint 与线程模型
Tomcat 的 NIO 层把等待就绪事件和执行协议处理分开:Selector 发现事件,SocketProcessor 执行协议处理。
先给答案:NIO Endpoint 把“少量 I/O 线程”和“可扩展业务线程”分开
Section titled “先给答案:NIO Endpoint 把“少量 I/O 线程”和“可扩展业务线程”分开”Poller 负责等待 socket 就绪,Worker 线程负责处理协议和请求;这样线程不会为大量空闲连接各自阻塞等待。一个连接的 I/O 事件可以高效复用,而可能阻塞的 Servlet 执行又不会占住 Poller。
边界在于线程池仍然有限:如果业务处理阻塞,Worker 队列会堆积,连接虽然被 Poller 发现可读,整体响应仍会变慢。排查 NIO 性能时要区分 selector、socket read/write、Executor 排队和应用代码耗时。
Acceptor -> accept -> Selector -> processKey(read/write) -> SocketProcessor -> ProtocolHandlerNioEndpoint#startInternal 位于 java/org/apache/tomcat/util/net/NioEndpoint.java:514;Selector 事件在 :997 和 :1129 处理,最终 Handler 在 :2201 进入协议层。
为什么不用一个线程处理全部工作
Section titled “为什么不用一个线程处理全部工作”替代方案:每连接一个线程,或 Selector 线程直接执行 Servlet。 为什么不行:前者线程数和切换成本不可控,后者慢请求会阻塞新连接和其他连接的事件分发。Endpoint 解耦 readiness 与 processing,代价是任务排队和状态同步。
| 场景 | 现象 | 原因 | 规避 |
|---|---|---|---|
| Selector 空转 | CPU 高 | key 未正确移除或异常唤醒 | 检查 SelectionKey 生命周期 |
| Worker 饥饿 | 大量超时 | Servlet 阻塞线程池 | 隔离阻塞任务 |
| 写事件持续触发 | CPU 高 | 未写完数据仍注册 OP_WRITE | 仅 pending output 时注册 |
| 半关闭 | 状态异常 | 读写方向独立关闭 | 按 SocketState 清理 |
Reactor 的重点是把就绪检测、连接状态和业务执行分层,适用于代理服务器、消息客户端和数据库网络层。
面试锚点
- Acceptor、Poller、Worker 分别做什么?
- 为什么不能在 Selector 线程执行 Servlet?
- OP_WRITE 为什么不能永久注册?