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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM