Netty 如何解决 JDK NIO 中的空轮询 Bug?
Netty 如何解决 JDK NIO 中的空轮询 Bug?
回答重点
Netty 实际上并没有解决 JDK 原生 NIO 中空轮询 bug,而是通过其他途径绕开了这个错误。
具体操作如下:
1)统计空轮询次数:Netty 通过 selectCnt 计数器来统计连续空轮询的次数。每次执行 Selector.select() 方法后,如果发现没有 I/O 事件,selectCnt 就会递增。
2)设置阈值:Netty 定义了一个阈值 SELECTOR_AUTO_REBUILD_THRESHOLD,默认值为 512。当空轮询次数达到这个阈值时,Netty 会触发重建 Selector 的操作。
3)重建 Selector:当达到空轮询的阈值时,Netty 会创建一个新的 Selector,并将所有注册的 Channel 从旧的 Selector 转移到新的 Selector 上。这一过程涉及到取消旧 Selector 上的注册,并在新 Selector 上重新注册 Channel。
4)关闭旧的 Selector:在成功重建 Selector 并将 Channel 重新注册后,Netty 会关闭旧的 Selector,从而避免继续在旧的 Selector 上发生空轮询。
总结来看,就是通过 selectCnt 统计没有 I/O 事件的次数来判断当前是否发生了空轮询,如果发生了就重建一个 Selector 替换之前出问题的 Selector,所以说 Netty 实际上没解决空轮询的 bug,只是绕开了这个问题。
扩展知识
The NIO selector wakes up infinitely in this situation..(以下情形可以复现)
- server waits for connection
- client connects and write message
- server accepts and register OP_READ
- server reads message and remove OP_READ from interest op set
- client close the connection
- server write message (without any reading.. surely OP_READ is not set)
- server’s select wakes up infinitely with return value 0
空轮询 bug 原因:
当连接的 Socket 被突然中断(如对端异常关闭)时,epoll 会将该 Socket 的事件标记为 EPOLLHUP 或 EPOLLERR,导致 Selector 被唤醒。 然而,SelectionKey 并未定义处理这些异常事件的类型,导致 Selector 被唤醒后,无法处理这些异常事件,从而进入空轮询状态,导致 CPU 占用率过高。
Netty 解决空轮询源码解析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97
| private void select(boolean oldWakenUp) throws IOException { Selector selector = this.selector; try { int selectCnt = 0; long currentTimeNanos = System.nanoTime(); long selectDeadLineNanos = currentTimeNanos + delayNanos(currentTimeNanos);
long normalizedDeadlineNanos = selectDeadLineNanos - initialNanoTime(); if (nextWakeupTime != normalizedDeadlineNanos) { nextWakeupTime = normalizedDeadlineNanos; }
for (;;) { long timeoutMillis = (selectDeadLineNanos - currentTimeNanos + 500000L) / 1000000L; if (timeoutMillis <= 0) { if (selectCnt == 0) { selector.selectNow(); selectCnt = 1; } break; }
if (hasTasks() && wakenUp.compareAndSet(false, true)) { selector.selectNow(); selectCnt = 1; break; }
int selectedKeys = selector.select(timeoutMillis); selectCnt++;
if (selectedKeys != 0 || oldWakenUp || wakenUp.get() || hasTasks() || hasScheduledTasks()) { break; }
if (Thread.interrupted()) { if (logger.isDebugEnabled()) { logger.debug("Selector.select() prematurely returned due to thread interruption. " + "Use NioEventLoop.shutdownGracefully() to shut down the NioEventLoop."); } selectCnt = 1; break; }
long time = System.nanoTime(); if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) { selectCnt = 1; } else if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 && selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) { selector = selectRebuildSelector(selectCnt); selectCnt = 1; break; }
currentTimeNanos = time; }
if (selectCnt > MIN_PREMATURE_SELECTOR_RETURNS) { if (logger.isDebugEnabled()) { logger.debug("Selector.select() prematurely returned {} times in a row for Selector {}.", selectCnt - 1, selector); } } } catch (CancelledKeyException e) { if (logger.isDebugEnabled()) { logger.debug(CancelledKeyException.class.getSimpleName() + " raised by a Selector {} - JDK bug?", selector, e); } } }
|