Netty組件EventLoopGroup和EventLoop源碼分析

目錄

一、UnSafe相關介紹

1、JAVA中Unsafe簡介

2、Netty中Unsafe介紹

二、EventLoopGroup和EventLoop源碼分析

1、初始化EventLoopGroup

2、NioEventLoop 的運行

3、Select方法

4、processSelectedKeys IO事件處理

5、Netty解決JAVA NiO空輪詢BUG

6、EventLoop構造過程一圖總結


一、UnSafe相關介紹

1、JAVA中Unsafe簡介

爲什麼先介紹Unsafe這個東西呢?我們知道JDK中也有UnSafe,Java中的Unsafe類爲我們提供了類似C++手動管理內存的能力。封裝這一系列的native方法。並且是禁止我們開發者自己使用的。當然你可以通過反射進行獲取。

JAVA中的UnSafe提供以下的功能

可以看到,java中的unsafe提供的都是至關重要的一些功能。

2、Netty中Unsafe介紹

Netty中的unsafe同樣也是非常重要的。因爲在Netty源碼中很多地方都是用到了這個相關工具,Unsafe接口中定義了socket相關操作,包括SocketAddress獲取、selector註冊、網卡端口綁定、socket建連與斷連、socket寫數據。這些操作都和jdk底層socket相關。他的繼承關係如下。

Unsafe是Channel的內部類,一個Channel對應一個Unsafe。

Unsafe用於處理Channel對應網絡IO的底層操作。ChannelHandler處理回調事件時產生的相關網絡IO操作最終也會委託給Unsafe執行。

NioUnsafe在Unsafe基礎上增加了幾個操作,包括訪問jdk的SelectableChannel、socket讀數據等。

NioByteUnsafe實現了與socket連接的字節數據讀取相關的操作。

NioMessageUnsafe實現了與新連接建立相關的操作。

二、EventLoopGroup和EventLoop源碼分析

我們就從最開始的Demo開始瞭解Netty的源碼吧。大家都知道Netty是一個網絡Io框架,他繼承NIO,BIO,AIO,並能夠按照模板化的方式去實現相關功能。我們以前也單獨講過JAVA原生的NIO實現。那麼Netty到底是怎麼將他們產生聯繫的呢?接下來一步一步爲大家解讀Netty源碼。瞭解Netty的技術內幕。

PS:以下源碼使用的是Netty4.1.28版本

1、初始化EventLoopGroup

在服務器啓動的常規代碼裏,首先是實例化NioEventLoopGroup和ServerBootstrap。

執行這行代碼時會發生什麼?由NioEventLoopGroup開始,一路調用,到達MultithreadEventLoopGroup,如果沒有指定創建的線程數量,則默認創建的線程個數爲DEFAULT_EVENT_LOOP_THREADS,該數值爲:處理器數量x2。

 protected MultithreadEventLoopGroup(int nThreads, ThreadFactory threadFactory, Object... args) {
        super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, threadFactory, args);
    }
private static final int DEFAULT_EVENT_LOOP_THREADS;

    static {
        DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt(
                "io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2));

        if (logger.isDebugEnabled()) {
            logger.debug("-Dio.netty.eventLoopThreads: {}", DEFAULT_EVENT_LOOP_THREADS);
        }
    }

最終由MultithreadEventExecutorGroup實例化

 /**
     * Create a new instance.
     *
     * @param nThreads          the number of threads that will be used by this instance.
     * @param executor          the Executor to use, or {@code null} if the default should be used.
     * @param chooserFactory    the {@link EventExecutorChooserFactory} to use.
     * @param args              arguments which will passed to each {@link #newChild(Executor, Object...)} call
     */
    protected MultithreadEventExecutorGroup(int nThreads, Executor executor,
                                            EventExecutorChooserFactory chooserFactory, Object... args)

在這個構造方法中,實例化了每個EventLoop所需要的執行器Executor


       if (executor == null) {
            executor = new ThreadPerTaskExecutor(newDefaultThreadFactory());
        }

由此可見,每個NioEventLoop的執行器爲ThreadPerTaskExecutor,ThreadPerTaskExecutor實現了Executor接口,並會在execute方法中啓動真正的線程,但是要和NioEventLoop的線程掛鉤則在SingleThreadEventExecutor的doStartThread方法裏。

public final class ThreadPerTaskExecutor implements Executor {
    private final ThreadFactory threadFactory;

    public ThreadPerTaskExecutor(ThreadFactory threadFactory) {
        if (threadFactory == null) {
            throw new NullPointerException("threadFactory");
        }
        this.threadFactory = threadFactory;
    }

    @Override
    public void execute(Runnable command) {
        //使用真正的線程執行方法
        threadFactory.newThread(command).start();
    }
}

接下來,new出EventExecutor(實際是NioEventLoop)的實例數組,並在循環裏new每個具體的EventLoop實例

那麼在NioEventLoop實例的構造方法裏又做了什麼事情呢?

    @Override
    protected EventLoop newChild(Executor executor, Object... args) throws Exception {
        return new NioEventLoop(this, executor, (SelectorProvider) args[0],
            ((SelectStrategyFactory) args[1]).newSelectStrategy(), (RejectedExecutionHandler) args[2]);
    }

作爲IO事件處理的主要組件,內部持有了Selector、SelectionKey的集合,所以構造方法中執行了關鍵方法openSelector(),最終通過JDK的api拿到selector的實例,作用和我們通過原生JDK的NIO編程中創建選擇器是一樣的。

另外,我們觀察下NioEventLoop的類圖如下

發現最終實現Exector,我們可以知道,EventLoop本質上是一個線程池,EventLoop內部維護着一個線程Thread和幾個阻塞隊列,所以EventLoop可以看成只有一個線程的線程池(SingleThreadPool)

每個EventLoop包含的線程Thread定義在父類SingleThreadEventExecutor中,每個EventLoop包含兩個隊列,taskQueue來自父類SingleThreadEventExecutor,保存各種任務,比如處理事件等等,tailTask來自父類SingleThreadEventLoop,用於每次事件循環後置任務處理

作爲IO事件處理的主要組件,必然離不開對事件的處理機制,在NioEventLoop的run方法,就有selector上進行select和調用processSelectedKeys()處理各種事件集。

2、NioEventLoop 的運行

@Override
protected void run() {
    for (;;) {
        try {
            try {
                // 1、通過 hasTasks() 判斷當前消息隊列中是否還有未處理的消息
                switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) {
                case SelectStrategy.CONTINUE:
                    continue;

                //hasTasks() 沒有任務則執行 select() 處理網絡IO
                case SelectStrategy.SELECT:
                //輪詢事件,見第三小節
                    select(wakenUp.getAndSet(false));

                    if (wakenUp.get()) {
                        selector.wakeup();
                    }
                    // fall through
                default:
                }
            } catch (IOException e) {
                // If we receive an IOException here its because the Selector is messed up. Let's rebuild
                // the selector and retry. https://github.com/netty/netty/issues/8566
                rebuildSelector0();
                handleLoopException(e);
                continue;
            }

            cancelledKeys = 0;
            needsToSelectAgain = false;
            // 處理IO事件所需的時間和花費在處理 task 時間的比例,默認爲 50%
            final int ioRatio = this.ioRatio;
            if (ioRatio == 100) {
                try {
                    // 如果 IO 的比例是100,表示每次都處理完IO事件後,才執行所有的task
                    processSelectedKeys();
                } finally {
                    // 執行 task 任務
                    runAllTasks();
                }
            } else {
                // 記錄處理 IO 開始的執行時間
                final long ioStartTime = System.nanoTime();
                try {
                //IO任務處理,見第四小節
                    processSelectedKeys();
                } finally {
                    // 計算處理 IO 所花費的時間
                    final long ioTime = System.nanoTime() - ioStartTime;
                    // 執行 task 任務,判斷執行 task 任務時間是否超過配置的比例,如果超過則停止執行 task 任務
                    runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
                }
            }
        } catch (Throwable t) {
            handleLoopException(t);
        }
        // Always handle shutdown even if the loop processing threw an exception.
        try {
            if (isShuttingDown()) {
                closeAll();
                if (confirmShutdown()) {
                    return;
                }
            }
        } catch (Throwable t) {
            handleLoopException(t);
        }
    }
}

1、調用selectStrategy.calculateStrategy 判斷是否有 Task任務,如果沒有則調用 selectSupplier.get() 方法,該方法是非阻塞的,判斷是否有需要處理的 Channel。如果沒有則返回 SelectStrategy.SELECT,然後執行 select(wakenUp.getAndSet(false)) 方法,阻塞等待可處理的 IO 就緒事件。

2、如果有 Task 任務,則判斷 ioRatio 的比率值,該值爲 EventLoop 處理 IO 和 處理 Task 任務的時間的比率。默認比率爲 50%。

  • 如果 ioRatio == 100,則說明優先處理所有的 IO 任務,處理完所有的IO事件後纔會處理所有的 Task 任務。
  • 如果 ioRatio <> 100, 則優先處理所有的IO任務,處理完所有的IO事件後,纔會處理所有的Task 任務,但處理所有的Task 任務的時候會判斷執行 Task 任務的時間比率,如果超過配置的比率則中斷處理 Task 隊列中的任務。

從中可以發現,什麼情況下都會優先處理 IO任務,但處理非 IO 任務時,會判斷非 IO 任務執行的時間不能超過 ioRatio 的閾值。

3、Select方法

private void select(boolean oldWakenUp) throws IOException {
    Selector selector = this.selector;
    try {
        int selectCnt = 0;
        long currentTimeNanos = System.nanoTime();
        // 計算出 NioEventLoop 定時任務最近執行的時間(還有多少 ns 執行),單位 ns
        long selectDeadLineNanos = currentTimeNanos + delayNanos(currentTimeNanos);

        for (;;) {
            // 爲定時任務中的時間加上0.5毫秒,將時間換算成毫秒
            long timeoutMillis = (selectDeadLineNanos - currentTimeNanos + 500000L) / 1000000L;
            // 對定時任務的超時時間判斷,如果到時間或超時,則需要立即執行 selector.selectNow()
            if (timeoutMillis <= 0) {
                if (selectCnt == 0) {
                    selector.selectNow();
                    selectCnt = 1;
                }
                break;
            }
            // 輪詢過程中發現有任務加入,中斷本次輪詢
            if (hasTasks() && wakenUp.compareAndSet(false, true)) {
                selector.selectNow();
                selectCnt = 1;
                break;
            }
            // Nio 的 阻塞式 select 操作
            int selectedKeys = selector.select(timeoutMillis);
            // select 次數 ++ , 通過該次數可以判斷是否出發了 JDK Nio中的 Selector 空輪循 bug
            selectCnt ++;

             // 如果selectedKeys不爲空、或者被用戶喚醒、或者隊列中有待處理任務、或者調度器中有任務,則break
            if (selectedKeys != 0 || oldWakenUp || wakenUp.get() || hasTasks() || hasScheduledTasks()) {
                break;
            }
            //如果線程被中斷則重置selectedKeys,同時break出本次循環,所以不會陷入一個繁忙的循環。
            if (Thread.interrupted()) {
                selectCnt = 1;
                break;
            }

            long time = System.nanoTime();
            // 如果超時,把 selectCnt 置爲 1,開始下一次的循環
            if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) {
                // timeoutMillis elapsed without anything selected.
                selectCnt = 1;
            }
            //  如果 selectCnt++ 超過 默認的 512 次,說明觸發了 Nio Selector 的空輪訓 bug,則需要重新創建一個新的 Selector,並把註冊的 Channel 遷移到新的 Selector 上
            else if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 &&
                    selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {
                // 重新創建一個新的 Selector,並把註冊的 Channel 遷移到新的 Selector 上,
                //解決NIO Selector空輪詢bug,見第五小節
                selector = selectRebuildSelector(selectCnt);
                selectCnt = 1;
                break;
            }

            currentTimeNanos = time;
        }

        if (selectCnt > MIN_PREMATURE_SELECTOR_RETURNS) {
            if (logger.isDebugEnabled()) {
                logger.debug("Selector.select() returned prematurely {} times in a row for Selector {}.",
                        selectCnt - 1, selector);
            }
        }
    } catch (CancelledKeyException e) {
        if (logger.isDebugEnabled()) {
            logger.debug(CancelledKeyException.class.getSimpleName() + " raised by a Selector {} - JDK bug?",
                    selector, e);
        }
    }
}

1、通過 delayNanos(currentTimeNanos) 計算出 定時任務隊列中第一個任務的執行時間。
2、判斷是否到期,如果到期則執行 selector.selectNow(),退出循環
3、如果定時任務未到執行時間,則通過 hasTasks() 判斷是否有可執行的任務,如果有則中斷本次循環。
4、既沒有到期的定時任務、也沒有可執行的Task,則調用 selector.select(timeoutMillis) 方法阻塞,等待註冊到 Selector 上感興趣的事件。
5、每次 select() 後都會 selectCnt++。通過該次數可以判斷是否出發了 JDK Nio中的 Selector 空輪詢 bug
6、如果selectedKeys不爲空、或者被用戶喚醒、或者隊列中有待處理任務、或者調度器中有任務,則break。
7、通過 selectCnt 判斷是否觸發了 JDK Selector 的空輪詢 bug,SELECTOR_AUTO_REBUILD_THRESHOLD 默認爲 512, 可修改。
8、通過 selectRebuildSelector() 方法解決 Selector 空輪詢 bug。

4、processSelectedKeys IO事件處理
 

 private void processSelectedKeys() {
        if (selectedKeys != null) {
            processSelectedKeysOptimized();
        } else {   
        //默認沒有使用優化的 Set,所有調用 processSelectedKeysPlain() 方法進行處理 IO 任務
            processSelectedKeysPlain(selector.selectedKeys());
        }
    }
 private void processSelectedKeysPlain(Set<SelectionKey> selectedKeys) {
        // check if the set is empty and if so just return to not create garbage by
        // creating a new Iterator every time even if there is nothing to process.
        // See https://github.com/netty/netty/issues/597
        if (selectedKeys.isEmpty()) {
            return;
        }

        Iterator<SelectionKey> i = selectedKeys.iterator();
//循環處理每個 selectionKey,每個selectionKey的處理首先根據attachment的類型來進行分發處理;
        for (;;) {
            final SelectionKey k = i.next();
            final Object a = k.attachment();
            i.remove();

            if (a instanceof AbstractNioChannel) {
                processSelectedKey(k, (AbstractNioChannel) a);
            } else {
                @SuppressWarnings("unchecked")
                NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
                processSelectedKey(k, task);
            }

            if (!i.hasNext()) {
                break;
            }

            if (needsToSelectAgain) {
                selectAgain();
                selectedKeys = selector.selectedKeys();

                // Create the iterator again to avoid ConcurrentModificationException
                if (selectedKeys.isEmpty()) {
                    break;
                } else {
                    i = selectedKeys.iterator();
                }
            }
        }
    }

 

 private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
//首先獲取 Channel 的 NioUnsafe,所有的讀寫等操作都在 Channel 的 unsafe 類中操作。
        final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe();
        if (!k.isValid()) {
            final EventLoop eventLoop;
            try {
                eventLoop = ch.eventLoop();
            } catch (Throwable ignored) {
           
                return;
            }
         
            if (eventLoop != this || eventLoop == null) {
                return;
            }
         
            unsafe.close(unsafe.voidPromise());
            return;
        }

        try {
            int readyOps = k.readyOps();
          //熟悉的獲取 SelectionKey 就緒事件,如果是 OP_CONNECT,則說明已經連接成功,並把註冊的 OP_CONNECT 事件取消
            if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
              
                int ops = k.interestOps();
                ops &= ~SelectionKey.OP_CONNECT;
                k.interestOps(ops);

                unsafe.finishConnect();
            }

          //如果是 OP_WRITE 事件,說明可以繼續向 Channel 中寫入數據,當寫完數據後用戶自己吧 OP_WRITE 事件取消掉。
            if ((readyOps & SelectionKey.OP_WRITE) != 0) {
             
                ch.unsafe().forceFlush();
            }

          //如果是 OP_READ 或 OP_ACCEPT 事件,則調用 unsafe.read() 進行讀取數據。unsafe.read() 中會調用到 ChannelPipeline 進行讀取數據。
            if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
                unsafe.read();
            }
        } catch (CancelledKeyException ignored) {
            unsafe.close(unsafe.voidPromise());
        }
    }

5、Netty解決JAVA NiO空輪詢BUG

第三節select方法介紹中,已經描述了Netty解決JAVA原生NIO空輪詢bug的方法,主要思路就是重新創建 Selector,並把原 Selector 上註冊的 Channel 遷移到新的 Selector 上

private Selector selectRebuildSelector(int selectCnt) throws IOException {
    // 重新創建 Selector,並把原 Selector 上註冊的 Channel 遷移到新的 Selector 上
    rebuildSelector();
    Selector selector = this.selector;

    selector.selectNow();
    return selector;
}

 

private void rebuildSelector0() {
    final Selector oldSelector = selector;
    final SelectorTuple newSelectorTuple;
    ......
    try {
        // 創建新的 Selector
        newSelectorTuple = openSelector();
    } catch (Exception e) {
        logger.warn("Failed to create a new Selector.", e);
        return;
    }


    int nChannels = 0;
    // 循環原 Selector 上註冊的所有的 SelectionKey
    for (SelectionKey key: oldSelector.keys()) {
        Object a = key.attachment();
        try {

            int interestOps = key.interestOps();
            key.cancel();
            SelectionKey newKey = key.channel().register(newSelectorTuple.unwrappedSelector, interestOps, a);
            ......
            nChannels ++;
        } catch (Exception e) {
            ......
        }
    }
    // 將新的 Selector 替換 原 Selector
    selector = newSelectorTuple.selector;
    unwrappedSelector = newSelectorTuple.unwrappedSelector;
    ......
}

6、EventLoop構造過程一圖總結

發佈了42 篇原創文章 · 獲贊 102 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章