本节摘要:Netty 的 NioEventLoopGroup 走 JDK 通用通道,EpollEventLoopGroup 直接通过 JNI 驱动 Linux epoll。本节比较两条传送带在系统调用路径、触发方式、独门特性上的差别,给出切换代码与选型判据:Linux 生产环境优先 Epoll,跨平台开发期用 NIO。
第一章到上一节用的都是 NioEventLoopGroup,它是 Netty 对 JDK NIO 的封装:底层调用 JDK 的 Selector,而 JDK 在 Linux 上最终也用 epoll 实现——但隔着 JVM 这层"统一抽象",很多平台特性被抹平了,还额外付出对象包装与 GC 的代价。
Netty 的另一条传送带 EpollEventLoopGroup 则完全绕开 JDK,用 JNI 直接调用 Linux 的 epoll 系列接口(macOS 上对应 KQueueEventLoopGroup)。听起来只是"抄了近路",实际差异比直觉大:
| 维度 | NIO(JDK Selector) | Epoll(Netty native) |
|---|---|---|
| 调用路径 | 应用 → JDK Selector → 内核 | 应用 → JNI → 内核 |
| 就绪通知 | 全量返回就绪集合,需遍历比对 | 只返回活跃事件,配合边缘触发 |
| 触发方式 | 水平触发为主 | 支持边缘触发,事件更少 |
| 独门特性 | 无平台特性 | SO_REUSEPORT、TCP_FASTOPEN、半关闭等 |
| 对象开销 | 每轮询产生较多临时对象 | 少量复用对象,GC 压力小 |
| 平台 | 任意有 JDK 的系统 | 仅 Linux(KQueue 对应 macOS、BSD) |

切传送带只动三处:加依赖、换 EventLoopGroup、换 Channel 类型。
<dependency> <groupId>io.netty</groupId> <artifactId>netty-transport-native-epoll</artifactId> <classifier>linux-x86_64</classifier> <version>4.1.108.Final</version> </dependency>
import io.netty.channel.epoll.Epoll; import io.netty.channel.epoll.EpollEventLoopGroup; import io.netty.channel.epoll.EpollServerSocketChannel; // 启动时判断可用性,别在非 Linux 环境硬来 boolean epollOk = Epoll.isAvailable(); EventLoopGroup boss = epollOk ? new EpollEventLoopGroup(1) : new NioEventLoopGroup(1); EventLoopGroup workers = epollOk ? new EpollEventLoopGroup() : new NioEventLoopGroup(); ServerBootstrap b = new ServerBootstrap(); b.group(boss, workers) .channel(epollOk ? EpollServerSocketChannel.class : NioServerSocketChannel.class) // Epoll 独门:端口复用,多个进程同抢一个端口,内核做负载均衡 .option(epollOk ? ChannelOption.SO_REUSEPORT : ChannelOption.SO_REUSEADDR, true) .childHandler(...);
生产上常见的写法是像上面这样双通道自适应:开发机(任意系统)走 NIO,容器与服务器(Linux)自动切 Epoll,一份代码两端跑。
SO_REUSEPORT 多进程抢端口。 传统上多个进程不能 bind 同一个端口,SO_REUSEPORT 允许多个 EventLoopGroup 各自 bind 成功,由内核把连接按哈希分给进程。网关类服务用它做多进程横向扩展,配合 CPU 亲和性效果更佳。
边缘触发(ET)与水平触发(LT)。 水平触发只要"还有货没搬完"就反复提醒;边缘触发只在"从无到有"那一瞬间提醒一次,之后必须一口气搬空。Netty 的 Epoll 传输默认水平触发(EpollMode.LEVEL_TRIGGERED),要发挥边缘触发的极限性能需显式设置并确保读循环把缓冲区掏空——这是把双刃剑,漏读会造成事件"失联"。
// 切换边缘触发(仅示意,务必确认读循环掏空缓冲区再启用) EpollChannelConfig config = (EpollChannelConfig) ch.config(); config.setEpollMode(EpollMode.EDGE_TRIGGERRED); // 注意:实际枚举名为 EDGE_TRIGGERED
上面这行注释藏了个彩蛋式提醒:枚举拼写是 EDGE_TRIGGERED,别凭记忆写成 EDGE_TRIGGERRED——这类编译错误还算友好,真正麻烦的是启用了边缘触发却没读空缓冲区,现象是连接"偶尔没反应",极难排查。
更少的 GC。 NIO 每轮 select 都会更新 SelectionKey 集合,高连接数下产生可观的临时对象;Epoll 路径复用少量结构体,对 GC 敏感的大流量服务差异可测。第七章的仪表盘会教你怎么用指标验证这一点。
⚠️ 常见坑:在容器里用 Epoll 却忘了基础镜像缺 glibc 兼容层,启动直接抛
UnsatisfiedLinkError。用 Alpine 镜像时要选带兼容库的版本,或退回 NIO 传输。
💡 关键直觉:NIO 与 Epoll 的差别不是"能不能用 epoll"(JDK 底层也用),而是"离内核有多近"。离得越近,特性越多、浪费越少,但平台绑定也越深。