7.2.7 Netty解决JDK空轮询Bug
大家应该早就听说过臭名昭著的Java NIO epoll的Bug,它会导致Selector空轮询,最终导致CPU使用率达到100%。官方声称JDK 1.6的update18修复了该问题,但是直到JDK 1.7该问题仍旧存在,只不过该Bug发生概率降低了一些而已,并没有被根本解决。出现此Bug是因为当Selector轮询结果为空时,没有进行wakeup或对新消息及时进行处理,导致发生了空轮询,CPU使用率达到了100%。我们来看一下这个问题在issue中的原始描述。
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 Kernel 2.6中,poll和epoll对于突然中断的Socket连接会对返回的EventSet事件集合置为POLLHUP,也可能是POLLERR,EventSet事件集合发生了变化,这就可能导致Selector会被唤醒。
这是与操作系统机制有关系的,JDK虽然仅仅是一个兼容各个操作系统平台的软件,但遗憾的是在JDK 5和JDK 6最初的版本中,这个问题并没有得到解决,而将这个“帽子”抛给了操作系统方,这就是这个Bug一直到2013年才最终修复的原因。
在Netty中最终的解决办法是:创建一个新的Selector,将可用事件重新注册到新的Selector中来终止空轮询。我们来回顾一下事件轮询的关键代码。
protected void run() {
for (;;) {
switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) {
case SelectStrategy.CONTINUE:
continue;
case SelectStrategy.SELECT:
select(wakenUp.getAndSet(false));
//省略select的唤醒逻辑
default:
}
//事件轮询处理逻辑
}
}
前面我们提到select()方法解决了JDK空轮询的Bug,那么它到底是如何解决的呢?下面我们来一探究竟,先来看一下select()方法的源码。
public final class NioEventLoop extends SingleThreadEventLoop {
...
int selectorAutoRebuildThreshold = SystemPropertyUtil.getInt("io.netty. selectorAutoRebuildThreshold", 512);
//省略判断代码
SELECTOR_AUTO_REBUILD_THRESHOLD = selectorAutoRebuildThreshold;
...
private void select(boolean oldWakenUp) throws IOException {
Selector selector = this.selector;
long currentTimeNanos = System.nanoTime();
for (;;) {
//省略非关键代码
long timeoutMillis = (selectDeadLineNanos - currentTimeNanos + 500000L) / 1000000L;
int selectedKeys = selector.select(timeoutMillis);
selectCnt ++;
//省略非关键代码
long time = System.nanoTime();
if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) {
// timeoutMillis elapsed without anything selected.
selectCnt = 1;
} else if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 &&
selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {
//日志打印代码
rebuildSelector();
selector = this.selector;
// Select again to populate selectedKeys.
selector.selectNow();
selectCnt = 1;
break;
}
currentTimeNanos = time;
}
//省略非关键代码
}
}
...
}
从上面的代码中可以看出,Selector 每一次轮询都计数 selectCnt++,开始轮询会将系统时间戳赋值给timeoutMillis,轮询完成后再将系统时间戳赋值给time,这两个时间会有一个时间差,而这个时间差就是每次轮询所消耗的时间。从上面的逻辑可以看出,如果每次轮询消耗的时间为0s,且重复次数超过512次,则调用rebuildSelector()方法,即重构Selector,具体实现代码如下。
public void rebuildSelector() {
//省略判断语句
rebuildSelector0();
}
private void rebuildSelector0() {
final Selector oldSelector = selector;
final SelectorTuple newSelectorTuple;
newSelectorTuple = openSelector();
//省略非关键代码
// Register all channels to the new Selector.
int nChannels = 0;
for (SelectionKey key: oldSelector.keys()) {
//省略非关键代码和异常处理
key.cancel();
SelectionKey newKey = key.channel().register(newSelectorTuple.unwrappedSelector, interestOps, a);
}
//省略非关键代码
}
实际上,在rebuildSelector()方法中,主要做了以下三件事情。
(1)创建一个新的Selector。
(2)将原来Selector中注册的事件全部取消。
(3)将可用事件重新注册到新的Selector,并激活。
就这样,Netty完美解决了JDK的空轮询Bug。看到这里,是不是感觉没那么神秘了?