Bootstrap

Netty源码分析-EventLoop(下)

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());
    }
}
;