Java NIO epoll 空转问题 + Netty 解决方法
在 Java NIO 编程实践中,很多人都会选择 Netty 作为基础框架,而不是直接用 JDK 原生的 NIO API。
因为 JDK 原生的 NIO 框架内容过于繁杂、学习成本高、补齐可靠性的工作量和难度都很大、还有一些bug。
其中一个著名的bug就是 epoll Selector 空转问题。
相关Bug单
- 《JDK-6670302 : (se) NIO selector wakes up with 0 selected keys infinitely [lnx 2.4]》
- 《JDK-6403933 : (se) Selector doesn't block on Selector.select(timeout) (lnx)》
- 《JDK-2147719 : (se) Selector doesn't block on Selector.select(timeout) (lnx)》
问题表象
示例代码(仅做示例,未考虑异常处理):
while (true) { selector.select(); Iterator it = selector.selectedKeys().iterator(); while (it.hasNext()) { SelectionKey key = (SelectionKey) it.next(); int ops = key.interestOps(); if (0 != (ops & SelectionKey.OP_ACCEPT)) { // 处理新连接 } if (0 != (ops & SelectionKey.OP_READ)) { // 读取消息 } it.remove(); } }
在使用 JDK NIO 框架时我们通常会采用上述模式的代码来处理客户端请求。
执行 Selector.select() 方法时会一直阻塞,直到有 channel 就绪。
但是在实践中,可能:
- 没有 channel 就绪,该方法也会返回。(违反原来的阻塞行为设计)
- 因为没有 channel 就绪,所以内部的 while 循环不会执行。
- 继而不断执行外部的 while (true) 循环。
- 上述步骤不断重复就形成空转轮询,CPU占用率达到100%,无法执行其它任务,最终程序崩溃。
问题原因
在部分 Linux 内核中,在 poll 或 epoll 一个已连接的socket,且请求事件掩码为0 的情况下,如果连接被突然中断,那么 poll/epoll 会被唤醒,相应事件标识为 POLLHUP(或 POLLERR)。
继而Selector被唤醒,且 interest set 为0,没有相应的 Channel,select() 返回值也是0。
Netty 的解决方法
Netty 的解决方式是 重建一个新的 Selector,替代原来出错的 Selector。
大致方法如下:
- 在一个 select 周期中,统计 空select 操作 的次数。
- 当 空select 操作次数累计到阈值时,就认为触发了 epoll空转 bug。
- 然后重建 Selector:
新建一个 Selector;
将 原Selector 上的 Channel 注册到 新Selector;
关闭 原Selector。
上述判定阈值默认为 512。可通过JVM系统变量设置(io.netty.selectorAutoRebuildThreshold)。
可在 NioEventLoop 类中查看相关代码。
思考
可能因为此问题的根源在于底层Linux内核行为的不一致,所以Java官方一开始将其抛给了操作系统实现方,导致该bug存在了很久。也许Java官方相关决策者认为,此类bug最恰当的修复方案应该在底层操作系统,而不是由上层Java去糊一层,因为JDK有自己的设计原则,并不是无脑做得越多越好。
其实这种分层分治的思维方式是非常令人欣赏的。很多优秀产品的缔造者内心都秉持这个原则。
如,Robot Framework 的负责人 Pekka Klärck 认为 IronPython 处理全角空格的不同方式应该由 IronPython 实现者去修改,而不是让 Robot Framework 在上面糊一层:
《Support for 'IDEOGRAPHIC SPACE' (U+3000) in test data on IronPython》
反观很多产品经理,毫无原则,只会做附庸,客户说啥就是啥,领导说啥就是啥,自己也没有足够的知识和经验储备,尽出些拙劣的程序,根本不能称为产品。真的是“人人都是产品经理”。