Netty——解决Selector 空轮询BUG

一、前言

JDK NIO的BUG,例如臭名昭著的epoll bug,它会导致Selector空轮询,最终导致CPU 100%。官方声称在JDK1.6版本的update18修复了该问题,但是直到JDK1.7版本该问题仍旧存在,只不过该BUG发生概率降低了一些而已,它并没有被根本解决。

该BUG以及与该BUG相关的问题单可以参见以下链接内容:
https://bugs.java.com/bugdatabase/view_bug.do?bug_id=2147719

https://bugs.java.com/bugdatabase/view_bug.do?bug_id=6403933

二、Selector BUG出现的原因

若Selector的轮询结果为空,也没有wakeup或新消息处理,则发生空轮询,CPU使用率100%。

Netty的解决办法:

  • 1、对Selector的select操作周期进行统计,每完成一次空的select操作进行一次计数,
    若在某个周期内连续发生N次空轮询,则触发了epoll死循环bug。

  • 2、重建Selector,判断是否是其他线程发起的重建请求,若不是则将原SocketChannel从旧的Selector上去除注册,重新注册到新的Selector上,并将原来的Selector关闭。

Netty解决办法具体步骤:

  • 1、先定义当前时间currentTimeNanos。
  • 2、接着计算出一个执行最少需要的时间timeoutMillis。
  • 3、每次对selectCnt做++操作。
  • 4、进行判断,如果到达执行到最少时间,则seletCnt重置为1。
  • 5、一旦到达SELECTOR_AUTO_REBUILD_THRESHOLD这个阀值,就需要重建selector来解决这个问题。
  • 6、这个阀值默认是512。

三、相关配置

Netty在NioEventLoop中考虑了这个问题,并通过在select方法不正常返回(Netty源码注释称其为prematurely,即提前返回)超过一定次数时重新创建新的Selector来修复此bug。

Netty提供了配置参数io.netty.selectorAutoRebuildThreshold供用户定义select创建新Selector提前返回的次数阈值,超过该次数则会触发Selector自动重建,默认为512。

但是如果指定的io.netty.selectorAutoRebuildThreshold小于3在Netty中被视为关闭了该功能。

public final class NioEventLoop extends SingleThreadEventLoop {

    private static final int SELECTOR_AUTO_REBUILD_THRESHOLD;

    static {
        //......省略部分代码

        int selectorAutoRebuildThreshold = SystemPropertyUtil.getInt("io.netty.selectorAutoRebuildThreshold", 512);
        if (selectorAutoRebuildThreshold < 3) {
            selectorAutoRebuildThreshold = 0;
        }

        SELECTOR_AUTO_REBUILD_THRESHOLD = selectorAutoRebuildThreshold;
        if (logger.isDebugEnabled()) {
            logger.debug("-Dio.netty.noKeySetOptimization: {}", DISABLE_KEY_SET_OPTIMIZATION);
            logger.debug("-Dio.netty.selectorAutoRebuildThreshold: {}", SELECTOR_AUTO_REBUILD_THRESHOLD);
        }

    }
}

四、实现原理

Netty对Selector.select提前返回的检测和处理逻辑主要在NioEventLoop.select方法中:

public final class NioEventLoop extends SingleThreadEventLoop {

    private void select(boolean oldWakenUp) throws IOException {
        Selector selector = this.selector;

        try {
            //计数器置0
            int selectCnt = 0;
            long currentTimeNanos = System.nanoTime();
            
            //根据注册的定时任务,获取本次select的阻塞时间
            long selectDeadLineNanos = currentTimeNanos + this.delayNanos(currentTimeNanos);

            while(true) {
                //每次循环迭代都重新计算一次select的可阻塞时间
                long timeoutMillis = (selectDeadLineNanos - currentTimeNanos + 500000L) / 1000000L;
                
                //如果可阻塞时间为0,表示已经有定时任务快要超时
                //此时如果是第一次循环(selectCnt=0),则调用一次selector.selectNow,然后退出循环返回
                //selectorNow方法的调用主要是为了尽可能检测出准备好的网络事件进行处理
                if (timeoutMillis <= 0L) {
                    if (selectCnt == 0) {
                        selector.selectNow();
                        selectCnt = 1;
                    }
                    break;
                }
                
                //如果没有定时任务超时,但是有以前注册的任务(这里不限定是定时任务),
                //且成功设置wakenUp为true,则调用selectNow并返回
                if (this.hasTasks() && this.wakenUp.compareAndSet(false, true)) {
                    selector.selectNow();
                    selectCnt = 1;
                    break;
                }
                
                //调用select方法,阻塞时间为上面算出的最近一个将要超时的定时任务时间
                int selectedKeys = selector.select(timeoutMillis);
                
                //计数器加1
                ++selectCnt;
                
                //selectedKeys != 0:如果返回的准备好时间的selectedKeys个数不为0表示这次是因为确实有事件准备好的正常返回
                //oldWakenUp:表示进来时,已经有其他地方对selector进行了唤醒操作
                //wakenUp.get():也表示selector被唤醒
                //hasTasks() || hasScheduledTasks():表示有任务或定时任务要执行
                //发生以上几种情况任一种则直接返回
                if (selectedKeys != 0 || oldWakenUp || this.wakenUp.get() || this.hasTasks() || this.hasScheduledTasks()) {
                    break;
                }

                //如果线程被中断,计数器置零,直接返回
                if (Thread.interrupted()) {
                    if (logger.isDebugEnabled()) {
                        logger.debug("Selector.select() returned prematurely because Thread.currentThread().interrupt() was called. Use NioEventLoop.shutdownGracefully() to shutdown the NioEventLoop.");
                    }

                    selectCnt = 1;
                    break;
                }

                //这里判断select返回是否是因为计算的超时时间已过,
                //这种情况下也属于正常返回,计数器置1,进入下次循环
                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) {
                    //进入这个分支,表示启用了select bug修复机制,
                    //即配置的io.netty.selectorAutoRebuildThreshold
                    //参数大于3,且上面select方法提前返回次数已经大于
                    //配置的阈值,则会触发selector重建
                    
                    //进行selector重建
                    //重建完之后,尝试调用非阻塞版本select一次,并直接返回
                    selector = this.selectRebuildSelector(selectCnt);
                    selectCnt = 1;
                    break;
                }

                currentTimeNanos = time;
            }

            //这种是对于关闭select bug修复机制的程序的处理,
            //简单记录日志,便于排查问题
            if (selectCnt > 3 && logger.isDebugEnabled()) {
                logger.debug("Selector.select() returned prematurely {} times in a row for Selector {}.", selectCnt - 1, selector);
            }
        } catch (CancelledKeyException var13) {
            if (logger.isDebugEnabled()) {
                logger.debug(CancelledKeyException.class.getSimpleName() + " raised by a Selector {} - JDK bug?", selector, var13);
            }
        }

    }
    
    private Selector selectRebuildSelector(int selectCnt) throws IOException {
        logger.warn("Selector.select() returned prematurely {} times in a row; rebuilding Selector {}.", selectCnt, this.selector);
        //进行selector重建
        this.rebuildSelector();
        Selector selector = this.selector;
        //重建完之后,尝试调用非阻塞版本select一次,并直接返回
        selector.selectNow();
        return selector;
    }   
}

上面调用的this.rebuildSelector()源码如下:

public final class NioEventLoop extends SingleThreadEventLoop {

    public void rebuildSelector() {
        //如果不在该线程中,则放到任务队列中
        if (!this.inEventLoop()) {
            this.execute(new Runnable() {
                public void run() {
                    NioEventLoop.this.rebuildSelector0();
                }
            });
        } else {
            //否则表示在该线程中,直接调用实际重建方法
            this.rebuildSelector0();
        }
    }
    
    private void rebuildSelector0() {
        Selector oldSelector = this.selector;
        
        //如果旧的selector为空,则直接返回
        if (oldSelector != null) {
            NioEventLoop.SelectorTuple newSelectorTuple;
            try {
                //新建一个新的selector
                newSelectorTuple = this.openSelector();
            } catch (Exception var9) {
                logger.warn("Failed to create a new Selector.", var9);
                return;
            }

            int nChannels = 0;
            Iterator var4 = oldSelector.keys().iterator();
            
            //对于注册在旧selector上的所有key,依次重新在新建的selecor上重新注册一遍
            while(var4.hasNext()) {
                SelectionKey key = (SelectionKey)var4.next();
                Object a = key.attachment();

                try {
                    if (key.isValid() && key.channel().keyFor(newSelectorTuple.unwrappedSelector) == null) {
                        int interestOps = key.interestOps();
                        key.cancel();
                        SelectionKey newKey = key.channel().register(newSelectorTuple.unwrappedSelector, interestOps, a);
                        if (a instanceof AbstractNioChannel) {
                            ((AbstractNioChannel)a).selectionKey = newKey;
                        }

                        ++nChannels;
                    }
                } catch (Exception var11) {
                    logger.warn("Failed to re-register a Channel to the new Selector.", var11);
                    if (a instanceof AbstractNioChannel) {
                        AbstractNioChannel ch = (AbstractNioChannel)a;
                        ch.unsafe().close(ch.unsafe().voidPromise());
                    } else {
                        NioTask task = (NioTask)a;
                        invokeChannelUnregistered(task, key, var11);
                    }
                }
            }

            //将该NioEventLoop关联的selector赋值为新建的selector
            this.selector = newSelectorTuple.selector;
            this.unwrappedSelector = newSelectorTuple.unwrappedSelector;

            try {
                //关闭旧的selector
                oldSelector.close();
            } catch (Throwable var10) {
                if (logger.isWarnEnabled()) {
                    logger.warn("Failed to close the old Selector.", var10);
                }
            }

            if (logger.isInfoEnabled()) {
                logger.info("Migrated " + nChannels + " channel(s) to the new Selector.");
            }
        }
    }
}

参考:
https://www.jianshu.com/p/b1ba37b6563b

https://blog.csdn.net/zhengchao1991/article/details/106534280

https://www.cnblogs.com/devilwind/p/8351732.html

你可能感兴趣的:(Netty——解决Selector 空轮询BUG)