回答
Netty 是基于 Java NIO 的,其内部使用了 Java NIO 的 Selector
来实现非阻塞 IO。在 Java NIO 中,Selector
是基于操作系统的多路复用技术实现的,在某些情况下操作系统底层的多路复用机制可能会错误地通知 JVM,表明有 IO 事件准备就绪,而实际上并没有任何事件,导致 Java NIO 会不断地轮询检查,却始终没有事件处理,从而导致 "Selector 空轮询"。
Netty 为了解决这个 bug,采取了两个步骤:
- 检测 epoll bug:Netty 使用空轮训计数器
selectCnt
来记录空轮询的次数。在每次调用select()
后,如果返回的就绪事件数量为 0,则表示发生了一次空轮询,则计数器selectCnt
+ 1。如果该计数器到达设定的阈值(默认512),则 Netty 认为确实发生了空轮训问题。 - 解决 epoll bug:Netty 采用重建 Selector 的方式来解决这个问题。当 Netty 认为当前 Selector 已经可能发生了空轮询时,Netty 会创建一个新的 Selector 来替代这个出了问题的旧 Selector。然后将所有在旧 Selector 上注册的所有 Channel 注销,并将他们重新注册到新创建的 Selector 上,最后关闭旧的 Selector 释放资源。
详解
Netty 通过一种很巧妙的方式来解决了该 bug,解决该 bug 主要分为两步:
- 检测 epoll bug。
- 解决 epoll bug。
下面来看看 Netty 是通过哪种巧妙的方式来解决 epoll bug 的。
NioEventLoop#run()
:
protected void run() {
int selectCnt = 0;
for (;;) {
try {
// ...
selectCnt++;
// ...
if (ranTasks || strategy > 0) {
if (selectCnt > MIN_PREMATURE_SELECTOR_RETURNS && logger.isDebugEnabled()) {
logger.debug("Selector.select() returned prematurely {} times in a row for Selector {}.",
selectCnt - 1, selector);
}
selectCnt = 0;
} else if (unexpectedSelectorWakeup(selectCnt)) { // Unexpected wakeup (unusual case)
selectCnt = 0;
}
} catch (CancelledKeyException e) {
// ...
} catch (Error e) {
throw e;
} catch (Throwable t) {
handleLoopException(t);
} finally {
// ...
}
}
}