Netty EventLoop
EventLoop 是 Netty 的事件执行线程。一个 Channel 会绑定到一个 EventLoop,之后该 Channel 的 IO 事件通常都由同一个线程处理。
线程模型
mermaid
flowchart TD
A[BossGroup] --> B[接收连接]
B --> C[注册到WorkerGroup]
C --> D[EventLoop 1]
C --> E[EventLoop 2]
C --> F[EventLoop 3]
D --> G[处理Channel读写事件]
E --> G
F --> GEventLoop 做什么
- 监听 Channel 的读写事件。
- 执行 ChannelPipeline 中的 Handler。
- 执行用户提交的普通任务。
- 执行定时任务,例如心跳检测。
核心原理
EventLoop 可以理解成一个不断循环的事件处理器。它不是只处理网络读写,还会处理普通任务和定时任务,所以它既是 IO 线程,也是该 Channel 的任务执行入口。
mermaid
flowchart TD
A["EventLoop 循环开始"] --> B["Selector 等待 IO 就绪事件"]
B --> C["处理可读 / 可写 / 关闭事件"]
C --> D["触发 ChannelPipeline"]
D --> E["执行普通任务队列"]
E --> F["执行定时任务队列"]
F --> A为什么一个 Channel 要绑定同一个 EventLoop:同一个连接的读写事件按顺序进入同一个线程,可以减少锁竞争,也能避免同一条连接上的消息被多个线程同时处理导致顺序错乱。代价是这个线程一旦被阻塞,它负责的所有 Channel 都会被影响。
为什么不能阻塞
一个 EventLoop 会服务多个 Channel。如果在 Handler 中执行慢 SQL、远程调用或大文件处理,这个 EventLoop 上的其他连接也会被拖慢。
mermaid
flowchart TD
A[IO事件进入Handler] --> B{是否耗时任务}
B -- 否 --> C[EventLoop直接处理]
B -- 是 --> D[投递到业务线程池]
D --> E[异步写回结果]如果不这样做,常见后果是:CPU 不一定很高,但请求延迟突然升高;少量慢请求把同一 EventLoop 上的其他连接拖住;心跳包不能及时处理,客户端误以为服务端掉线。
开发建议
| 场景 | 建议 |
|---|---|
| 轻量协议解析 | 放在 EventLoop |
| 数据库查询 | 投递到业务线程池 |
| 远程 HTTP 调用 | 使用异步客户端或业务线程池 |
| 心跳超时 | 使用 Netty 定时任务 |
| 共享状态 | 尽量按 Channel 维度保存,减少锁 |
常见问题
如果 CPU 不高但请求延迟很大,重点检查 EventLoop 是否被阻塞。可以通过线程栈查看 nioEventLoopGroup 线程是否卡在业务方法、日志输出或同步 IO 上。
排查顺序建议:
- 先看线程栈,确认
nioEventLoopGroup是否卡在业务类、日志类、数据库驱动或 HTTP 客户端。 - 再看单个 Handler 是否执行了阻塞调用。
- 再看写队列是否堆积,尤其是客户端接收慢时。
- 最后看 EventLoop 数量和 CPU 核数是否匹配。
代码 Demo:耗时任务投递到业务线程池
java
public class BizHandler extends SimpleChannelInboundHandler<String> {
private final ExecutorService bizPool = Executors.newFixedThreadPool(16);
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) {
bizPool.submit(() -> {
String result = queryDatabaseOrCallRemoteApi(msg);
ctx.executor().execute(() -> ctx.writeAndFlush(result));
});
}
private String queryDatabaseOrCallRemoteApi(String msg) {
return "result: " + msg;
}
}耗时逻辑在线程池执行,写回结果时再切回 Channel 所属的 EventLoop。这样可以避免阻塞 EventLoop 上的其他连接。
