JDK Epoll空轮询bug

本文探讨了epoll机制下JDK NIO出现的一个常见Bug,即Selector.select()在无事件发生时异常唤醒的问题。分析了此Bug的原因及两种解决方案:不完善的临时修复方法与重建Selector的完整解决方案,并介绍了Jetty与Netty的具体实践。

前面讲到了epoll的一些机制,与select和poll等传统古老的IO多路复用机制的一些区别,这些区别实质可以总结为一句话,就是epoll将重要的基于事件的fd集合放在了内核中来完成,因为内核是高效的,所以很多关于fd事件监听集合的操作也是高效的,不方便的就是,因为在内核中,所以我们需要通过系统调用来调用关于fd操作集合,而不是直接自己攒一个。如果在linux中,epoll在JDK6中还需要配置,在后续的版本中为JDK的NIO提供了默认的实现,但是epoll在JDK中的实现却是漏洞百出的,bug非常的多,比较容易复现并且被众多人诟病的就是epoll轮询的处理方法。

1. Bug说明

  • 正常情况下,selector.select()操作是阻塞的,只有被监听的fd有读写操作时,才被唤醒
  • 但是,在这个bug中,没有任何fd有读写请求,但是select()操作依旧被唤醒
  • 很显然,这种情况下,selectedKeys()返回的是个空数组
  • 然后按照逻辑执行到while(true)处,循环执行,导致死循环。

2. Bug分析

JDK bug列表中有两个相关的bug报告:

  1. JDK-6670302 : (se) NIO selector wakes up with 0 selected keys infinitely
  2. JDK-6403933 : (se) Selector doesn't block on Selector.select(timeout) (lnx)

JDK-6403933的bug说出了实质的原因:

This is an issue with poll (and epoll) on Linux. If a file descriptor for a connected socket is polled with a request event mask of 0, and if the connection is abruptly terminated (RST) then the poll wakes up with the POLLHUP (and maybe POLLERR) bit set in the returned event set. The implication of this behaviour is that Selector will wakeup and as the interest set for the SocketChannel is 0 it means there aren't any selected events and the select method returns 0.

具体解释为:在部分Linux的2.6的kernel中,poll和epoll对于突然中断的连接socket会对返回的eventSet事件集合置为POLLHUP,也可能是POLLERR,eventSet事件集合发生了变化,这就可能导致Selector会被唤醒。

这是与操作系统机制有关系的,JDK虽然仅仅是一个兼容各个操作系统平台的软件,但很遗憾在JDK5和JDK6最初的版本中(严格意义上来将,JDK部分版本都是),这个问题并没有解决,而将这个帽子抛给了操作系统方。

3. 解决办法

3.1 不完善的解决办法

grizzly的commiteer们最先进行修改的,并且通过众多的测试说明这种修改方式大大降低了JDK NIO的问题。

if (SelectionKey != null)  {  // the key you registered on the temporary selector
   SelectionKey.cancel();   // cancel the SelectionKey that was registered with the temporary selector
   // flush the cancelled key
   temporarySelector.selectNow();
} 

但是,这种修改仍然不是可靠的,一共有两点:

  1. 多个线程中的SelectionKey的key的cancel,很可能和下面的Selector.selectNow同时并发,如果是导致key的cancel后运行很可能没有效果
  2. 与其说第一点使得NIO空转出现的几率大大降低,经过Jetty服务器的测试报告发现,这种重复利用Selector并清空SelectionKey的改法很可能没有任何的效果

3.2 完善的解决办法

最终的终极办法是创建一个新的Selector:

Trash wasted Selector, creates a new one.

3.3 各应用具体解决方法

3.3.1 Jetty

Jetty首先定义两了-D参数:

  • JVMBUG_THRESHHOLD

org.mortbay.io.nio.JVMBUG_THRESHHOLD, defaults to 512 and is the number of zero select returns that must be exceeded in a period.

  • threshhold

org.mortbay.io.nio.MONITOR_PERIOD defaults to 1000 and is the period over which the threshhold applies.

第一个参数是select返回值为0的计数,第二个是多长时间,整体意思就是控制在多长时间内,如果Selector.select不断返回0,说明进入了JVM的bug的模式。

做法是:

  • 记录select()返回为0的次数(记做jvmBug次数)
  • 在MONITOR_PERIOD时间范围内,如果jvmBug次数超过JVMBUG_THRESHHOLD,则新创建一个selector

3.3.2 Netty

思路和Jetty的处理方式几乎是一样的,就是netty讲重建Selector的过程抽取成了一个方法。

long currentTimeNanos = System.nanoTime();
for (;;) {
    // 1.定时任务截止事时间快到了,中断本次轮询
    ...
    // 2.轮询过程中发现有任务加入,中断本次轮询
    ...
    // 3.阻塞式select操作
    selector.select(timeoutMillis);
    // 4.解决jdk的nio bug
    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) {

        rebuildSelector();
        selector = this.selector;
        selector.selectNow();
        selectCnt = 1;
        break;
    }
    currentTimeNanos = time; 
    ...
 }

netty 会在每次进行 selector.select(timeoutMillis) 之前记录一下开始时间currentTimeNanos,在select之后记录一下结束时间,判断select操作是否至少持续了timeoutMillis秒(这里将time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos改成time - currentTimeNanos >= TimeUnit.MILLISECONDS.toNanos(timeoutMillis)或许更好理解一些),
如果持续的时间大于等于timeoutMillis,说明就是一次有效的轮询,重置selectCnt标志,否则,表明该阻塞方法并没有阻塞这么长时间,可能触发了jdk的空轮询bug,当空轮询的次数超过一个阀值的时候,默认是512,就开始重建selector。

<think>嗯,用户现在要求的是用图画演示JDK NIO的epoll bug导致Selector空轮询的机制。这个问题挺专业的,涉及到Java NIO底层和Linux epoll的交互细节。 从用户提供的引用来看,特别是引用4提到了Netty的解决方案,引用5解释了空轮询的原因,用户似乎已经对这个问题有一定了解,现在需要更直观的理解方式。考虑到技术问题的抽象性,用文字描述流程图可能是最合适的方案。 需要重点展现几个关键节点:Selector初始轮询状态、连接异常中断后的错误状态、Linux epoll的误通知、Selector的持续空轮询循环。每个环节都要对应到CPU使用率的变化上,特别是最后100%的那个状态。 在描述时需要特别注意: 1. 区分操作系统层(epoll)和JVM层(Selector)的交互 2. 强调异常中断时SelectionKey的状态异常 3. 展示Netty解决方案的介入点(rebuiltSelector) 用户可能是系统调优工程师或者中间件开发者,遇到实际性能问题才这么深入追究。ta连续追问NIO底层问题,说明不是初学者,回答需要保持技术严谨性。 要不要补充说明这个bug在后续JDK版本的变化?不过用户明确要求图示演示,这些文字说明可能作为备注更合适。最终决定把版本影响放在流程图说明里简单带过。 最后生成的问题要覆盖Netty替代方案、监控诊断、现代JDK情况等延伸方向,这样既解答当前问题又提供后续研究路径。 </think>以下是 JDK NIO 的 `epoll bug` 导致 `Selector` 空轮询及 CPU 100% 问题的图示化分析: --- ### **Selector 空轮询问题流程图** ```mermaid graph TD A[Selector 开启轮询] --> B{是否有 I/O 事件?} B -->|无事件| C[空轮询发生] B -->|有事件| D[正常处理] C --> E{检测轮询次数} E -->|连续空轮询 > 阈值| F[触发 epoll bug] E -->|未达阈值| A F --> G[Selector 持续空轮询] G --> H[线程占用 CPU 100%] ``` --- ### **关键步骤解析** 1. **Selector 初始轮询** - 当应用程序调用 `selector.select()` 时,底层通过 `epoll_wait` 监听 I/O 事件。 - 正常情况:若无事件,线程应阻塞(进入休眠状态)[^1][^2]。 2. **异常中断触发空轮询** - **问题根源**:当 TCP 连接被异常中断(如 RST 报文)时,Linux epoll 可能错误地唤醒 `Selector`,但实际无就绪事件[^3][^4]。 - **结果**:`selector.select()` 立即返回 `0`(表示无事件),却未阻塞线程。 3. **CPU 100% 形成机制** ```mermaid flowchart LR S[Selector 空轮询] --> L{轮询循环} L -->|select 立即返回 0| L L --> T[线程持续占用 CPU] ``` - 由于 `Selector` 未进入阻塞状态,线程进入死循环调用 `select()`,CPU 时间被无限占用[^1][^5]。 4. **JDK 未彻底修复的原因** - JDK 1.6 update18 后仅降低了触发频率,但未解决 epoll 内核层的误唤醒问题[^2][^4]。 - `SelectionKey` 未定义“连接异常中断事件”,导致内核异常无法被应用层捕获[^5]。 --- ### **Netty 的解决方案** ```plaintext 原始流程: Selector.select() → 空轮询 → CPU 100% Netty 改进: Selector.select() → 检测空轮询次数 → 若超阈值 ↓ 重建 Selector (替换旧实例) ↓ 迁移 Channel 到新 Selector ``` - **核心逻辑**:统计 `select()` 调用次数与返回 `0` 的次数,若空轮询比例超过阈值(如 512 次),触发 `rebuildSelector()` [^3][^4]。 - **优势**:通过重建 `Selector` 重置内核事件表,规避持续空轮询。 --- ### **影响范围** | 系统/场景 | 触发概率 | 后果 | |--------------------|---------------|--------------------| | 高并发 TCP 服务器 | 高(连接频繁中断) | 单线程 CPU 100% | | 长连接服务 | 中 | 线程池资源耗尽 | | UDP 服务 | 低 | 几乎不触发 | --- ### **规避建议** 1. **升级 JDK**:JDK 17+ 优化了 `Selector` 实现,显著降低触发概率[^5]。 2. **使用 Netty**:其内置的 `NioEventLoop` 自动处理空轮询(默认阈值 512 次)[^3][^4]。 3. **监控指标**:检测线程 CPU 使用率及 `selector.select()` 返回次数。 ---
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值