12.Netty如何解决空轮询的

总结
  • JDK epoll 实现有 bug:无事件时 Selector.select() 本应阻塞,但会意外唤醒,导致 CPU 空转到 100%
  • 该 bug 在 JDK 1.6 u18 声称修复,但 JDK 1.7、1.8 仍然存在,只是概率降低
  • Netty 没有从根本解决,而是用"计数 + 重建 Selector"的方式规避:空轮询超过 512 次就重建 Selector
  • 重建过程:新建 Selector → 把旧 Selector 的 SelectionKey 全部迁移过去 → 替换成员变量引用

1. NIO 空轮询 bug 是什么?

JDK 1.5 引入了基于 epoll 的事件响应机制来优化 NIO。epoll 把事件处理交给操作系统内核(硬中断)来处理,解决了 select/poll 模型无效遍历的问题。

但 JDK 的 epoll 实现有个著名的 bug:理论上没有 I/O 事件时,Selector.select() 应该一直阻塞,但 bug 会导致它不断地被意外唤醒,程序陷入死循环,不断向 CPU 申请资源,最终 CPU 飙到 100%。

问题代码

官方声称在 JDK 1.6 update 18 修复了这个问题,但实际上 JDK 1.7、1.8 中该 bug 仍然存在,只是触发概率降低了,并没有被根本解决

2. Netty 怎么解决的?

Netty 底层同样调用了 NIO 的 Selector,但它用计数 + 重建 Selector 的方式规避了这个问题。

netty解决代码

核心逻辑在 NioEventLoop 的 select 流程里,详见 5.Netty的EventLoop

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) {
    // 空轮询次数超过阈值(默认 512),触发 Selector 重建
    selector = selectRebuildSelector(selectCnt);
    selectCnt = 1;
    break;
}

判断逻辑:

  1. 每次 select 前记录当前时间 currentTimeNanos
  2. select 返回后,检查实际耗时是否达到预期的 timeoutMillis
  3. 达到预期 → 正常唤醒,selectCnt 重置为 1
  4. 没达到预期 → 可能触发了空轮询,selectCnt 累加
  5. selectCnt 超过阈值 512 → 触发 Selector 重建

阈值

3. Selector 重建过程

重建分三步:

第一步:新建一个 Selector

新Selector

开启一个线程,创建全新的 Selector 实例。

第二步:迁移 SelectionKey

注册新selcector

把旧 Selector 上已注册的所有 SelectionKey(即所有 Channel 的监听事件)全部迁移到新 Selector 上,保证已有连接不受影响。

第三步:替换引用

赋值selector

把成员变量 this.selector 指向新 Selector,旧 Selector 等待 GC 回收。

flowchart LR
    A[selectCnt 累加] -->|超过 512 次| B[新建 Selector]
    B --> C[迁移所有 SelectionKey]
    C --> D[this.selector 指向新 Selector]
    D --> E[旧 Selector 等待 GC]

4. 本质是规避,不是修复

Netty 的方案是"用计数检测异常,用重建规避问题",并没有从根本上修复 JDK epoll 的 bug。

空轮询在达到阈值之前仍然会发生,只是超过 512 次后强制重建,把影响控制在可接受范围内。这是一种权衡:重建 Selector 有一定开销,但比 CPU 100% 要好得多。