ARTICLE · 1002787
Netty 原生传输源码:Epoll 与 KQueue 到底快在哪里(源码系列第13篇)
Netty Native Transport 的价值不只是“比 JDK NIO 快”。它绕过 Java Selector 的适配层,直接使用 epoll/kqueue,减少部分转换和系统调用开销,更重要的是暴露
TCP_CORK、SO_REUSEPORT、Unix Domain Socket、边沿触发等平台能力。是否切换应由压测和功能需求决定,而不是看到 Linux 就默认替换。
同一套 Bootstrap 和 Pipeline 代码,只替换 EventLoopGroup 与 Channel 类,就能从 NIO 切到 Epoll 或 KQueue。这种 API 兼容很容易制造错觉:底层只是换了个 Selector 实现。实际上 native transport 从 Channel、EventLoop、事件数组到 JNI 调用都是另一条实现链,只在上层保持相同抽象。
源码基线:
• 项目:Netty • 版本: netty-4.1.135.Final• Commit: f05f765d81460799c53123a207f665bf3b465171• 重点模块: transport-classes-epoll、transport-native-epoll、transport-classes-kqueue、transport-native-kqueue• 重点类: EpollEventLoop、AbstractEpollChannel、KQueueEventLoop、AbstractKQueueChannel、Native• 官方源码:https://github.com/netty/netty/tree/netty-4.1.135.Final/transport-classes-epoll

图中上半部分保持一致:Bootstrap、ChannelPipeline、Handler 和 ByteBuf 不需要感知平台。差异发生在 EventLoop 与 Channel 以下:NIO 进入 JDK Selector,Epoll/KQueue 则通过 Netty 的 Native 封装和 JNI 调用操作系统事件接口。这正是 Netty 能让业务少改代码、同时获得平台能力的关键。
1. Native Transport 是完整传输实现,不是一个开关
NIO 服务端常用:
EventLoopGroup group = new NioEventLoopGroup();bootstrap.group(group) .channel(NioServerSocketChannel.class);Linux Epoll 对应:
EventLoopGroup group = new EpollEventLoopGroup();bootstrap.group(group) .channel(EpollServerSocketChannel.class);macOS/BSD KQueue 对应:
EventLoopGroup group = new KQueueEventLoopGroup();bootstrap.group(group) .channel(KQueueServerSocketChannel.class);替换看似只有两行,背后却同时改变:事件循环实现、Socket Channel 实现、配置项、就绪事件表示、文件描述符管理和 native 库加载。Pipeline 之所以能复用,是因为这些实现仍遵循 Channel、EventLoop 和 ChannelConfig 契约。
2. EpollEventLoop 直接等待事件数组和 timerfd
EpollEventLoop 的等待路径会根据是否存在定时任务、是否需要立即轮询,选择不同的 epollWait 方式。核心调用可概括为:
private long epollWait(long deadlineNanos) throws IOException { if (deadlineNanos == NONE) { return Native.epollWait(epollFd, events, timerFd, -1, -1); } int delaySeconds = ...; int delayNanos = ...; return Native.epollWait(epollFd, events, timerFd, delaySeconds, delayNanos, EPOLL_WAIT_MILLIS_THRESHOLD);}Native.epollWait 再进入 JNI 和 C 实现。EventLoop 持有 epollFd、eventFd、timerFd 和 EpollEventArray:
• epollFd管理被监听的文件描述符;• eventFd用于跨线程唤醒 EventLoop;• timerFd协调下一次定时任务 deadline;• EpollEventArray批量接收就绪事件。
它避免了 JDK SelectionKey 对象模型中的部分映射与兼容路径,Netty 可以针对 Linux 行为直接优化。但 JNI 仍有边界成本,native 也不是“零开销”。最终收益取决于连接数、事件密度、报文大小和 JDK 版本。
3. KQueue 的结构相似,但事件语义不是 Epoll 的复制品
KQueueEventLoop 使用 kevent 等待变化列表与事件列表:
int numEvents = Native.keventWait( kqueueFd.intValue(), changeList, eventList, timeoutSec, timeoutNs);KQueue 通过 filter 表达读、写、用户事件等变化;Epoll 使用 interest mask 和 event flag。Netty 在上层统一为 Channel 读写事件,但底层注册、删除、EOF 与错误处理并不相同。
因此不要假设 Epoll 参数能原样复制到 KQueue。二者共同点是 native、事件驱动和平台特性;差异来自操作系统接口本身。
Selector.select | epoll_wait | kevent | |
4. 真正有确定价值的常常是平台能力
单纯说“Epoll 更快”缺少条件。更容易形成确定收益的是 NIO 无法或不方便提供的能力,例如:
• SO_REUSEPORT:多个 Socket 绑定同一地址,由内核分配连接;• TCP_CORK:控制小包聚合时机;• TCP_QUICKACK:影响延迟确认策略;• Unix Domain Socket:同机进程通信; • 边沿触发模式; • 更丰富的 TCP keepalive 参数; • splice 等 Linux 零拷贝相关能力。
这些能力通过 EpollChannelOption、专用 Channel 或 native API 暴露。若业务恰好需要它们,选择 native transport 是功能决策;若只是普通 HTTP 服务,则必须用端到端压测证明收益。
5. 边沿触发能减少重复通知,也提高读尽责任
Epoll 支持水平触发与边沿触发。水平触发下,只要 fd 仍可读,就会继续通知;边沿触发更像状态变化通知,应用收到事件后应尽量读到 EAGAIN。
Netty 的读循环和 RecvByteBufAllocator.Handle.continueReading() 共同承担“是否继续读”。边沿触发并不意味着可以随意一次只读一个消息,否则剩余数据可能等不到新的边沿变化。
工程取舍如下:
不要把边沿触发当成一个孤立性能参数。它会和 AUTO_READ、读配额、单连接公平性、业务背压共同作用。
6. 新连接接入仍会进入同一套 Pipeline 契约
Epoll 服务端接入连接时,AbstractEpollServerChannel 从 native accept 得到 fd,再创建子 Channel:
int socket = socket.accept(acceptedAddress);if (socket >= 0) { allocHandle.lastBytesRead(socket); pipeline.fireChannelRead( newChildChannel(socket, acceptedAddress, 1, acceptedAddress[0])); return 1;}这里和第 3 篇 NIO Accept 主链形成对应:底层 accept 方式不同,但新子 Channel 仍作为入站消息进入父 Pipeline,随后由 ServerBootstrapAcceptor 配置并注册到 Worker。上层扩展点没有因为 native 而消失。
这也是迁移时通常无需重写业务 Handler 的原因。真正需要审计的是 ChannelOption、native 依赖、平台行为和运维指标。
7. 零拷贝能力必须结合 TLS 和协议层判断
Native Transport 可能提供 FileRegion、splice 等减少数据复制的路径,但“使用 native 就自动零拷贝”是错误结论。
典型限制包括:
• TLS 需要加密,明文文件通常不能直接绕过用户态; • 协议需要压缩或改写数据时必须处理内容; • 小文件的系统调用优化可能被调度和对象成本抵消; • 容器、内核版本和文件系统会影响结果。
应按真实链路压测:静态大文件、明文 TCP、低业务处理比例,更可能看到明显收益;小 JSON 请求、复杂编解码和数据库占主导时,传输层差异可能不是瓶颈。

图中应重点看“平台不同、上层归一”:EpollEventLoop 从事件数组取出 fd 和 flag,KQueueEventLoop 从 event list 取出 filter 与 ident,最终都定位 Channel 并触发读、写或关闭处理。业务 Handler 看到的仍是同一套 Channel 事件。
8. 可用性检测与回退必须在启动时完成
Native 库可能因为操作系统、CPU 架构、分类器、libc 或加载路径不匹配而不可用。启动时应先检查:
if (Epoll.isAvailable()) { // EpollEventLoopGroup + EpollServerSocketChannel} else { log.warn("epoll unavailable", Epoll.unavailabilityCause()); // 明确回退 NIO,或按部署要求拒绝启动}生产环境不要静默回退。若性能容量是按 Epoll 测出的,某个实例悄悄使用 NIO,会造成难解释的延迟和 CPU 差异。至少把 transport 类型、native 库版本和不可用原因暴露为启动日志与指标标签。
9. 依赖选择要匹配平台与打包方式
常见故障不是代码错误,而是依赖没有把正确 native artifact 打进镜像。需要检查:
1. Linux 是 glibc 还是 musl; 2. CPU 是 x86_64 还是 aarch64; 3. shaded/fat jar 是否保留 native 资源; 4. 容器运行平台是否和构建平台一致; 5. Netty 各模块版本是否统一; 6. 安全扫描是否要求升级 native 组件。
建议在 CI 中对目标镜像运行最小启动测试,实际执行 Epoll.isAvailable() 或 KQueue.isAvailable(),不要只靠 Maven 依赖树判断。
10. 是否切换应看四组证据
选择 Native Transport 时至少比较:
• 吞吐:请求数、消息数、网络带宽; • 延迟:P50、P99、P999 和最大停顿; • 资源:EventLoop CPU、系统 CPU、上下文切换、直接内存; • 稳定性:长时间连接抖动、fd 增长、native 崩溃和部署失败率。
测试必须使用真实协议、TLS 配置、连接数、消息大小和容器限制。只跑一个 echo benchmark 得出的结论,不能直接用于生产 RPC 或网关。
11. Native Transport 的最终判断
主链可以归纳为:
Epoll/KQueue EventLoop -> native wait -> event array -> fd / Channel mapping -> unsafe read/write/close -> ChannelPipeline -> business handler如果需要平台专属 Socket 能力,Native Transport 通常值得采用;如果只是追求“可能更快”,先压测再决定;如果团队无法稳定管理 native 依赖和多架构镜像,NIO 的可移植性本身就是收益。
下一篇回到 JDK NIO 的一个经典故障:Selector 在没有事件、没有任务时连续立即返回,会让 EventLoop 空转吃满 CPU。Netty 如何识别真正的异常空轮询,又如何迁移全部 SelectionKey 并安全重建 Selector?
参考资料
1. Netty Epoll Java 实现:https://github.com/netty/netty/tree/netty-4.1.135.Final/transport-classes-epoll/src/main/java/io/netty/channel/epoll 2. Netty Epoll Native 实现:https://github.com/netty/netty/tree/netty-4.1.135.Final/transport-native-epoll 3. Netty KQueue Java 实现:https://github.com/netty/netty/tree/netty-4.1.135.Final/transport-classes-kqueue/src/main/java/io/netty/channel/kqueue 4. Netty Native Transports Wiki:https://netty.io/wiki/native-transports.html