Nio空轮询bug在哪里体现,并如何解决?
在NioEventLoop中,run方法中是一个死循环,所以需要通过调用selector来进行阻塞。如果这个bug发生了,即使没有超时,也会不断进行循环,导致cpu占用率会到达100%。
@Override
protected void run() {
int selectCnt = 0;
...
for(;;){
...
// 可能发生空轮询,无法阻塞NIO线程
strategy = select(curDeadlineNanos);
...
if(...) {
...
} else if (unexpectedSelectorWakeup(selectCnt) ){
// 通过unexpectedSelectorWakeup方法中的rebuildSelector重建selector
// 并将selectCnt重置为0
selectCnt = 0;
}
}
}
Netty中的解决方案也很简单,通过selectCnt变量来检测select方法是否发生空轮询BUG。selectCnt初始值为0,每次for循环一次,selectCnt就自增1。如果bug空轮询发生,selectCnt会快速增长。Netty中认为如果selectCnt的值大于默认大小:512,则可以判断发生了空轮询的bug。int selectorAutoRebuildThreshold = SystemPropertyUtil.getInt("io.netty.selectorAutoRebuildThreshold", 512);
可以得出,这里的阈值大小可以通过参数进行配置,如果没有参数就把它定义为512大小。
if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 && selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {
// The selector returned prematurely many times in a row.
// Rebuild the selector to work around the problem.
logger.warn("Selector.select() returned prematurely {} times in a row; rebuilding Selector {}.",selectCnt, selector);
// 重建selector,将原selector的配置信息传给新selector
// 再用新selector覆盖旧selector
rebuildSelector();
return true;
}
如果发生了bug,Netty会调用rebuildSelector()
或者selectRebuildSelector()
来进行修复。会重新创建一个selector,并将旧的selector上的信息比如selectionKey等拷贝到新的selector上去,替换了原来的selector。也就是以新换旧。
这个bug只有可能在linux系统才是有可能出现,因为Linux系统对Selector的支持不是很好。
iaRatio控制的是什么,设置为100有什么作用?
// 处理IO事件时间比例,默认为50%
final int ioRatio = this.ioRatio;
// 如果IO事件时间比例设置为100%
if (ioRatio == 100) {
try {
// 如果需要去处理IO事件
if (strategy > 0) {
// 先处理IO事件
processSelectedKeys();
}
} finally {
// Ensure we always run tasks.
// 剩下的时间都去处理普通任务和定时任务
ranTasks = runAllTasks();
}
} else if (strategy > 0) { // 如果需要去处理IO事件
// 记录处理IO事件前的时间
final long ioStartTime = System.nanoTime();
try {
// 去处理IO事件
processSelectedKeys();
} finally {
// Ensure we always run tasks.
// ioTime为处理IO事件耗费的事件
final long ioTime = System.nanoTime() - ioStartTime;
// 计算出处理其他任务的事件
// 超过设定的时间后,将会停止任务的执行,会在下一次循环中再继续执行
ranTasks = runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
}
} else { // 没有IO事件需要处理
// This will run the minimum number of tasks
// 直接处理普通和定时任务
ranTasks = runAllTasks(0);
}
如果执行普通任务的时间过长,那么处理IO的事件的时间就会相对应减少。为了避免这种情况影响IO事件,所以创建了一个参数ioRatio用来控制NioEventLoop处理IO事件花费时间占执行所有操作的总时间的比例。默认值是50,代表处理IO事件时间比例为50%。
ioRatio控制任务执行的过程:
判断ioRatio是否为100
- 是,判断是否需要处理IO事件(也就是strategy > 0 对应情况),是则先处理IO,否则(也有可能是IO事件已经处理完成)去处理普通任务和定时任务,直到所有的任务被处理完。
- 否,先去处理IO事件,并记录所花费的时间,并保存在ioTime中,然后去处理其他的普通任务和定时任务,根据ioTime和ioRatio计算执行其他任务可用的时间。如果其他任务的处理时间一旦超过计算的可用时间,就是停止执行,并在下一次循环中再继续执行。
- 如果没有IO时间需要处理,就去执行最少数量(数量通过
ranTasks = runAllTasks(0);
获得)的普通任务和定时任务。
处理事件
IO事件是通过NioEventLoop.processSelectedKeys()
方法处理的
private void processSelectedKeys() {
// 如果selectedKeys是基于数组的
// 一般情况下都走这个分支
if (selectedKeys != null) {
// 处理各种IO事件
processSelectedKeysOptimized();
} else {
processSelectedKeysPlain(selector.selectedKeys());
}
}
processSelectedKeysOptimized方法
private void processSelectedKeysOptimized() {
for (int i = 0; i < selectedKeys.size; ++i) {
// 拿到SelectionKeyec
final SelectionKey k = selectedKeys.keys[i];
// null out entry in the array to allow to have it GC'ed once the Channel close
// See https://github.com/netty/netty/issues/2363
selectedKeys.keys[i] = null;
// 获取SelectionKey上的附件,即NioServerSocketChannel
final Object a = k.attachment();
if (a instanceof AbstractNioChannel) {
// 处理事件,传入附件NioServerSocketChannel
processSelectedKey(k, (AbstractNioChannel) a);
} else {
@SuppressWarnings("unchecked")
NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
processSelectedKey(k, task);
}
if (needsToSelectAgain) {
// null out entries in the array to allow to have it GC'ed once the Channel close
// See https://github.com/netty/netty/issues/2363
selectedKeys.reset(i + 1);
selectAgain();
i = -1;
}
}
}
该方法中通过fori的方法,遍历基于数组的SelectedKey,通过final SelectionKey k = selectedKeys.keys[i];
获取到SelectionKey,然后获取其再Register时添加的附件NioServerSocketChannel
// 获取SelectionKey上的附件,即NioServerSocketChannel
final Object a = k.attachment();
如果附件继承自AbstractNioChannel,则会调用
// 处理事件,传入附件NioServerSocketChannel
processSelectedKey(k, (AbstractNioChannel) a);
去处理各个事件
真正处理各种事件的方法processSelectedKey
获取SelectionKey的事件,然后进行相应处理
private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe();
if (!k.isValid()) {
final EventLoop eventLoop;
try {
eventLoop = ch.eventLoop();
} catch (Throwable ignored) {
// If the channel implementation throws an exception because there is no event loop, we ignore this
// because we are only trying to determine if ch is registered to this event loop and thus has authority
// to close ch.
return;
}
// Only close ch if ch is still registered to this EventLoop. ch could have deregistered from the event loop
// and thus the SelectionKey could be cancelled as part of the deregistration process, but the channel is
// still healthy and should not be closed.
// See https://github.com/netty/netty/issues/5125
if (eventLoop == this) {
// close the channel if the key is not valid anymore
unsafe.close(unsafe.voidPromise());
}
return;
}
try {
int readyOps = k.readyOps();
// We first need to call finishConnect() before try to trigger a read(...) or write(...) as otherwise
// the NIO JDK channel implementation may throw a NotYetConnectedException.
if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
// remove OP_CONNECT as otherwise Selector.select(..) will always return without blocking
// See https://github.com/netty/netty/issues/924
int ops = k.interestOps();
ops &= ~SelectionKey.OP_CONNECT;
k.interestOps(ops);
unsafe.finishConnect();
}
// Process OP_WRITE first as we may be able to write some queued buffers and so free memory.
if ((readyOps & SelectionKey.OP_WRITE) != 0) {
// Call forceFlush which will also take care of clear the OP_WRITE once there is nothing left to write
ch.unsafe().forceFlush();
}
// Also check for readOps of 0 to workaround possible JDK bug which may otherwise lead
// to a spin loop
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
unsafe.read();
}
} catch (CancelledKeyException ignored) {
unsafe.close(unsafe.voidPromise());
}
}