bug表现
epoll bug
- 正常情况下,
selector.select()
操作是阻塞的,只有被监听的fd有读写操作时,才被唤醒 - 但是,在这个bug中,没有任何fd有读写请求,但是
select()
操作依旧被唤醒 - 很显然,这种情况下,
selectedKeys()
返回的是个空数组 - 然后按照逻辑执行到
while(true)
处,循环执行,导致死循环。
bug原因
JDK bug列表中有两个相关的bug报告:
- JDK-6670302 : (se) NIO selector wakes up with 0 selected keys infinitely
- JDK-6403933 : (se) Selector doesn't block on Selector.select(timeout) (lnx)
JDK-6403933的bug说出了实质的原因:
具体解释为:在部分Linux的2.6的kernel中,poll和epoll对于突然中断的连接socket会对返回的eventSet事件集合置为POLLHUP,也可能是POLLERR,eventSet事件集合发生了变化,这就可能导致Selector会被唤醒。
这是与操作系统机制有关系的,JDK虽然仅仅是一个兼容各个操作系统平台的软件,但很遗憾在JDK5和JDK6最初的版本中(严格意义上来将,JDK部分版本都是),这个问题并没有解决,而将这个帽子抛给了操作系统方,这也就是这个bug最终一直到2013年才最终修复的原因,最终影响力太广。
解决办法
不完善的解决办法
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();
}
但是,这种修改仍然不是可靠的,一共有两点:
- 多个线程中的SelectionKey的key的cancel,很可能和下面的Selector.selectNow同时并发,如果是导致key的cancel后运行很可能没有效果
- 与其说第一点使得NIO空转出现的几率大大降低,经过Jetty服务器的测试报告发现,这种重复利用Selector并清空SelectionKey的改法很可能没有任何的效果,
完善的解决办法
最终的终极办法是创建一个新的Selector:
各应用具体解决方法
Jetty
Jetty首先定义两了-D参数:
- JVMBUG_THRESHHOLD
- threshhold
第一个参数是select返回值为0的计数,第二个是多长时间,整体意思就是控制在多长时间内,如果Selector.select不断返回0,说明进入了JVM的bug的模式。
做法是:
- 记录
select()
返回为0的次数(记做jvmBug次数) - 在MONITOR_PERIOD时间范围内,如果jvmBug次数超过JVMBUG_THRESHHOLD,则新创建一个selector
Jetty解决空轮询bug
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