面试难题:netty如何解决selector空轮询bug?(图解+秒懂+史上最全)(代码片段)

架构师-尼恩 架构师-尼恩     2023-01-11     742

关键词:

文章很长,建议收藏起来,慢慢读! 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

管你jdk还是linux,我netty稳坐钓鱼台

 JDKNIO在Linux系统下空轮询的bug,就是调用Selector.select(timeout),即使没事件发生,也不会阻塞timeout时间,而是立马return,这样的空轮询导致CPU100%。产生这个bug大致的原因我讲下:连接突然中断,poll和epo... 查看详情

netty什么是netty

...C-rpc框架Dubbo-rpc框架Zookeeper-分布式协调框架三.Netty的优势解决TCP传输问题,如粘包、半包epoll空轮询 查看详情

netty框架之概述及基本组件介绍(代码片段)

...缺点:JDK的NIO底层由epoll实现,该实现饱受诟病的Selector空轮询bug会导致cpu飙升100%NIO的API繁杂,使用麻烦&#x 查看详情

day470.netty概述&netty高性能架构设计-netty(代码片段)

...题NIO的类库和API繁杂,使用麻烦:需要熟练掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等。需要具备其他的额外技能:要熟悉Java多线程编程,因为NIO编程涉及到Reactor模式,你必须对多线程和网络编程非... 查看详情

面试官:说说netty断开连接的原理

参考技术A多路复用器(Selector)接收到OP_READ事件:处理OP_READ事件:NioSocketChannel.NioSocketChannelUnsafe.read()关闭连接,会触发OP_READ事件:到了最后,关闭selection上的selectionkey,这样selector上就不会再发生该channel上的各种事件了。如果发送... 查看详情

day470&471.netty概述&netty高性能架构设计-netty(代码片段)

...题NIO的类库和API繁杂,使用麻烦:需要熟练掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等。需要具备其他的额外技能:要熟悉Java多线程编程,因为NIO编程涉及到Reactor模式,你必须对多线程和网络编程非... 查看详情

netty源码_nioeventloop详解

...,你将了解:netty的事件轮询器也是通过的javanio的选择器Selector来管理多个嵌套字Socket的通道channel。那么选择器Selector是如何与通道channel,并管理它们的呢。在SelectableChannel类用register方法将通道注册到选择器Selector中。如果当前... 查看详情

netty源码之接收连接(代码片段)

...orker线程4、workerGroup将socketChannel注册到选择的NioEventLoop的selector5、workerGroup注册读事件接收链接NIO的读事件while(!stop)//循环遍历selector,休眠时间为1S,当又处于就绪状态的CHannel时,selector将返回该channel的集合。通过对Channel集合... 查看详情

netty源码之接收连接(代码片段)

...orker线程4、workerGroup将socketChannel注册到选择的NioEventLoop的selector5、workerGroup注册读事件接收链接NIO的读事件while(!stop)//循环遍历selector,休眠时间为1S,当又处于就绪状态的CHannel时,selector将返回该channel的集合。通过对Channel集合... 查看详情

netty服务端基本的工作原理

...annel接受客户端的连接请求channel是双向的,而流是单向的Selector会不断的轮训注册在其上的Channel,如果某个Channel上有新的TCP连接接入、读和写事件,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取... 查看详情

三.netty入门到超神系列-javanio三大核心(selector,channel,buffer)(代码片段)

...用场景,其中也用到了channel。这一章我们来理解一下selector,结合channel来做一个c/s通信。理解Selector和ChannelSelector选择器,也叫多路复用器,可以同时处理多个客户端连接,多路复用器采用轮询机制来选择 查看详情

netty学习(代码片段)

...源的浪费NIO怎么实现的同步非阻塞关键就是轮询器(Selector)的使用。轮询器(Selector)负责监视全部通道IO的状态,当其中任意一个或者多个通道具有可用的IO操作时,该轮询器会通过一个方法返回一个大... 查看详情

分享即时通讯开发之netty高性能原理

...9;NIO的类库和API繁杂,使用麻烦:你需要熟练掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等。   2)需要具备其他的额外技能做铺垫:例如熟悉Java多线程编程,因为NIO编程涉及到Reactor模式,你必须... 查看详情

面试难题:分布式session实现难点,这篇就够!(代码片段)

来源:blog.csdn.net/Gaowumao/article/details/124309548Hollis的新书限时折扣中,一本深入讲解Java基础的干货笔记!那如何来解决分布式Session问题呢?一、解决方案列举二、Java代码实现解决分布式Session平常做的项目都是在一... 查看详情

面试难题:分布式session实现难点,这篇就够!(代码片段)

来源:blog.csdn.net/Gaowumao/article/details/124309548Hollis的新书限时折扣中,一本深入讲解Java基础的干货笔记!那如何来解决分布式Session问题呢?一、解决方案列举二、Java代码实现解决分布式Session平常做的项目都是在一... 查看详情

面试难题:分布式session实现难点,这篇就够!(代码片段)

点击上方关注“终端研发部”设为“星标”,和你一起掌握更多数据库知识来源:blog.csdn.net/Gaowumao/article/details/124309548那如何来解决分布式Session问题呢?一、解决方案列举二、Java代码实现解决分布式Session平常做的项... 查看详情

netty源码之接收连接(代码片段)

...orker线程4、workerGroup将socketChannel注册到选择的NioEventLoop的selector5、workerGroup注册读事件接收链接NIO的读事件while(!stop)//循环遍历 查看详情

面试官:什么是netty粘包拆包?怎么解决netty粘包拆包问题

哈喽!大家好,我是小奇,一位热爱分享的程序员小奇打算以轻松幽默的对话方式来分享一些技术,如果你觉得通过小奇的文章学到了东西,那就给小奇一个赞吧文章持续更新一、前言书接上回,昨天肯定是狗蛋通风报信,导致... 查看详情