• Netty解决Selector空轮询BUG的策略(图解+秒懂+史上最全)


    文章很长,建议收藏起来,慢慢读! Java 高并发 发烧友社群:疯狂创客圈 奉上以下珍贵的学习资源:


    推荐:入大厂 、做架构、大力提升Java 内功 的 精彩博文

    入大厂 、做架构、大力提升Java 内功 必备的精彩博文 2021 秋招涨薪1W + 必备的精彩博文
    1:Redis 分布式锁 (图解-秒懂-史上最全) 2:Zookeeper 分布式锁 (图解-秒懂-史上最全)
    3: Redis与MySQL双写一致性如何保证? (面试必备) 4: 面试必备:秒杀超卖 解决方案 (史上最全)
    5:面试必备之:Reactor模式 6: 10分钟看懂, Java NIO 底层原理
    7:TCP/IP(图解+秒懂+史上最全) 8:Feign原理 (图解)
    9:DNS图解(秒懂 + 史上最全 + 高薪必备) 10:CDN图解(秒懂 + 史上最全 + 高薪必备)
    11: 分布式事务( 图解 + 史上最全 + 吐血推荐 ) 12:seata AT模式实战(图解+秒懂+史上最全)
    13:seata 源码解读(图解+秒懂+史上最全) 14:seata TCC模式实战(图解+秒懂+史上最全)

    Java 面试题 30个专题 , 史上最全 , 面试必刷 阿里、京东、美团... 随意挑、横着走!!!
    1: JVM面试题(史上最强、持续更新、吐血推荐) 2:Java基础面试题(史上最全、持续更新、吐血推荐
    3:架构设计面试题 (史上最全、持续更新、吐血推荐) 4:设计模式面试题 (史上最全、持续更新、吐血推荐)
    17、分布式事务面试题 (史上最全、持续更新、吐血推荐) 一致性协议 (史上最全)
    29、多线程面试题(史上最全) 30、HR面经,过五关斩六将后,小心阴沟翻船!
    9.网络协议面试题(史上最全、持续更新、吐血推荐) 更多专题, 请参见【 疯狂创客圈 高并发 总目录

    SpringCloud 精彩博文
    nacos 实战(史上最全) sentinel (史上最全+入门教程)
    SpringCloud gateway (史上最全) 更多专题, 请参见【 疯狂创客圈 高并发 总目录

    Netty解决Selector空轮询BUG的策略(图解+秒懂+史上最全)

    Selector 的空轮询BUG

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

    注意:是CPU 100%,非常严重的bug。

    这个臭名昭著的epoll bug,是 JDK NIO的BUG,官方声称在JDK1.6版本的update18修复了该问题,但是直到JDK1.7、JDK1.8版本该问题仍旧存在,只不过该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

    Netty解决空轮询的4步骤:

    Netty的解决办法总览:

    • 1、对Selector的select操作周期进行统计,每完成一次空的select操作进行一次计数,若在某个周期内连续发生N次空轮询,则触发了epoll死循环bug。
    • 2、重建Selector,判断是否是其他线程发起的重建请求,若不是则将原SocketChannel从旧的Selector上去除注册,重新注册到新的Selector上,并将原来的Selector关闭。

    Netty解决空轮询的4步骤

    Netty解决空轮询的4步骤,具体如下:

    在这里插入图片描述

    第一部分:定时阻塞select(timeMillins)

    • 先定义当前时间currentTimeNanos。
    • 接着计算出一个执行最少需要的时间timeoutMillis。
    • 定时阻塞select(timeMillins) 。
    • 每次对selectCnt做++操作。

    第二部分:有效IO事件处理逻辑

    第三部分:超时处理逻辑

    • 如果查询超时,则seletCnt重置为1。

    第四步: 解决空轮询 BUG

    • 一旦到达SELECTOR_AUTO_REBUILD_THRESHOLD这个阀值,就需要重建selector来解决这个问题。
    • 这个阀值默认是512。
    • 重建selector,重新注册channel通道

    Netty解决空轮询的4步骤的核心代码

    
    long time = System.nanoTime();
    
    //调用select方法,阻塞时间为上面算出的最近一个将要超时的定时任务时间
    int selectedKeys = selector.select(timeoutMillis);
    
    //计数器加1
    ++selectCnt;
    
    if (selectedKeys != 0 || oldWakenUp || this.wakenUp.get() || this.hasTasks() || this.hasScheduledTasks()) {
       //进入这个分支,表示正常场景     
    
       //selectedKeys != 0: selectedKeys个数不为0, 有io事件发生
       //oldWakenUp:表示进来时,已经有其他地方对selector进行了唤醒操作
       //wakenUp.get():也表示selector被唤醒
       //hasTasks() || hasScheduledTasks():表示有任务或定时任务要执行
       //发生以上几种情况任一种则直接返回
    
       break;
    }
    
    //此处的逻辑就是: 当前时间 - 循环开始时间 >= 定时select的时间timeoutMillis,说明已经执行过一次阻塞select(), 有效的select
    if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) {
       //进入这个分支,表示超时,属于正常的场景
       //说明发生过一次阻塞式轮询, 并且超时
       selectCnt = 1;
    } else if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 && selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {
       //进入这个分支,表示没有超时,同时 selectedKeys==0
       //属于异常场景
       //表示启用了select bug修复机制,
       //即配置的io.netty.selectorAutoRebuildThreshold
       //参数大于3,且上面select方法提前返回次数已经大于
       //配置的阈值,则会触发selector重建
    
       //进行selector重建
       //重建完之后,尝试调用非阻塞版本select一次,并直接返回
       selector = this.selectRebuildSelector(selectCnt);
       selectCnt = 1;
       break;
    }
    currentTimeNanos = time;
    

    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;
                    
    
                    if (selectedKeys != 0 || oldWakenUp || this.wakenUp.get() || this.hasTasks() || this.hasScheduledTasks()) {
                   //进入这个分支,表示正常场景     
                        
                    //selectedKeys != 0: selectedKeys个数不为0, 有io事件发生
                    //oldWakenUp:表示进来时,已经有其他地方对selector进行了唤醒操作
                    //wakenUp.get():也表示selector被唤醒
                    //hasTasks() || 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) {
                        //进入这个分支,表示没有超时,同时 selectedKeys==0
                        //属于异常场景
                        //表示启用了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<SelectableChannel> 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.");
                }
            }
        }
    }
    

    Netty空轮询的阈值配置

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

    参考文献:

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

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

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

  • 相关阅读:
    vb.net FTP上传下载,目录操作
    vb.net导出CSV文件
    服务器内存总量
    定义数组
    监控键盘健代码
    C# FTp 上传,下载
    使用EasyUI中Tree
    微信web开发自定义分享
    mysql将时间戳格式化
    查询表时给字段赋默认值 sql
  • 原文地址:https://www.cnblogs.com/crazymakercircle/p/15370299.html
Copyright © 2020-2023  润新知