Netty 之 NioEventLoop 源碼分析

每一個 NioEventLoop 開啓一個線程,線程啓動時會調用 NioEventLoop 的 run 方法,執行I/O任務和非I/O任務

I/O任務

I/O 任務就是處理 Nio 中 Selector 中註冊的 4 種事件。

SelectionKey.OP_READ
SelectionKey.OP_WRITE
SelectionKey.OP_CONNECT
SelectionKey.OP_ACCEPT

非IO任務

  • 系統 Task:通過調用 NioEventLoop 的 excute(Runnable task) 方法實現, Netty 有很多系統 Task,創建它們的主要原因:當 I/O 線程和用戶線程同時操作網絡資源時,爲了防止併發操作導致的鎖競爭,將用戶線程操作封裝成 Task 放入消息隊列中,由 NioEventLoop 線程執行,由同一個線程執行,不需要考慮多線程併發問題。
  • 定時任務:通過調用 NioEventLoop 的 schedule(Runnable command,long delay,TimeUnit unit) 方法實現。

NioEventLoop 源碼分析

public final class NioEventLoop extends SingleThreadEventLoop {

    private Selector selector;
    private Selector unwrappedSelector;
    private SelectedSelectionKeySet selectedKeys;

    private final SelectorProvider provider;
    ......

從 NioEventLoop 類中可用看到內部使用了 java.nio.channels.Selector。 由 Selector 處理網絡 I/O 讀寫操作操作。

初始化 Selector

    NioEventLoop(NioEventLoopGroup parent, Executor executor, SelectorProvider selectorProvider,
                 SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler) {
        super(parent, executor, false, DEFAULT_MAX_PENDING_TASKS, rejectedExecutionHandler);
        ......
        final SelectorTuple selectorTuple = openSelector();
        selector = selectorTuple.selector;
    }

獲取 Selector

    private SelectorTuple openSelector() {
        final Selector unwrappedSelector;
        try {
            //1、 創建 Selector
            unwrappedSelector = provider.openSelector();
        } catch (IOException e) {
            throw new ChannelException("failed to open a new selector", e);
        }
        // 2、判斷是否開啓優化開關,默認沒有開啓直接返回 Selector
        if (DISABLE_KEY_SET_OPTIMIZATION) {
            return new SelectorTuple(unwrappedSelector);
        }

        // 3、反射創建 SelectorImpl 對象
        Object maybeSelectorImplClass = AccessController.doPrivileged(new PrivilegedAction<Object>() {
            @Override
            public Object run() {
                try {
                    return Class.forName( "sun.nio.ch.SelectorImpl", false, PlatformDependent.getSystemClassLoader());
                } catch (Throwable cause) {
                    return cause;
                }
            }
        });
        // 省略代碼 ......


        final Class<?> selectorImplClass = (Class<?>) maybeSelectorImplClass;
        // 3、使用優化後的 SelectedSelectionKeySet 對象將 JDK 的 sun.nio.ch.SelectorImpl.selectedKeys 替換掉。
        final SelectedSelectionKeySet selectedKeySet = new SelectedSelectionKeySet();

        Object maybeException = AccessController.doPrivileged(new PrivilegedAction<Object>() {
            @Override
            public Object run() {
                try {
                    Field selectedKeysField = selectorImplClass.getDeclaredField("selectedKeys");
                    Field publicSelectedKeysField = selectorImplClass.getDeclaredField("publicSelectedKeys");

                    // 省略代碼 ......

                    Throwable cause = ReflectionUtil.trySetAccessible(selectedKeysField, true);
                    if (cause != null) {
                        return cause;
                    }
                    cause = ReflectionUtil.trySetAccessible(publicSelectedKeysField, true);
                    if (cause != null) {
                        return cause;
                    }

                    selectedKeysField.set(unwrappedSelector, selectedKeySet);
                    publicSelectedKeysField.set(unwrappedSelector, selectedKeySet);
                    return null;
                } catch (NoSuchFieldException e) {
                    return e;
                } catch (IllegalAccessException e) {
                    return e;
                }
            }
        });

        if (maybeException instanceof Exception) {
            selectedKeys = null;
            Exception e = (Exception) maybeException;
            logger.trace("failed to instrument a special java.util.Set into: {}", unwrappedSelector, e);
            return new SelectorTuple(unwrappedSelector);
        }
        selectedKeys = selectedKeySet;
        logger.trace("instrumented a special java.util.Set into: {}", unwrappedSelector);
        return new SelectorTuple(unwrappedSelector,
                                 new SelectedSelectionKeySetSelector(unwrappedSelector, selectedKeySet));
    }

1、通過 Nio 的 java.nio.channels.spi.SelectorProvider 創建 Selector。
2、判斷是否開啓 Selector 的優化開關,默認是不開啓,則直接返回已經創建的 Selector。
3、如果開啓優化則通過反射加載 sun.nio.ch.SelectorImpl 對象,並通過已經優化過的 SelectedSelectionKeySet 替換 sun.nio.ch.SelectorImpl 中的 selectedKeys 和 publicSelectedKeys 兩個 HashSet 集合。

NioEventLoop 啓動運行

當 NioEventLoop 初始化後,開始運行會調用 run() 方法。

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

                case SelectStrategy.BUSY_WAIT:
                    // fall-through to SELECT since the busy-wait is not supported with NIO

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

// io.netty.channel.DefaultSelectStrategy#calculateStrategy
public int calculateStrategy(IntSupplier selectSupplier, boolean hasTasks) throws Exception {
    //如果 hasTask 沒有任務則調用則返回  SelectStrategy.SELECT,否則調用 selectNow
    return hasTasks ? selectSupplier.get() : SelectStrategy.SELECT;
}

// io.netty.channel.nio.NioEventLoop#selectNowSupplier
private final IntSupplier selectNowSupplier = new IntSupplier() {
    @Override
    public int get() throws Exception {
        // selectNow 是否非阻塞的,返回可操作的 Channel 的個數,如果沒有返回 0 。
        return selectNow();
    }
};

1、調用selectStrategy.calculateStrategy 判斷是否有 Task任務,如果沒有則調用 SelectorImpl.selectNow() 方法,該方法是非阻塞的,判斷是否有需要處理的 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 的閾值。

NioEventLoop.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 上
                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。

selectRebuildSelector() 解決空輪詢bug

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

    selector.selectNow();
    return selector;
}

重新創建 Selector,並把原 Selector 上註冊的 Channel 遷移到新的 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;
    ......
}

1、創建新的 Selector
2、循環把原 Selector 上所有的 SelectorKey 註冊到 新的 Selector 上
3、將新的 Selector 替換掉原來的 Selector

處理 IO 任務

NioEventLoop 調用 processSelectedKeys 處理 IO 任務

private void processSelectedKeys() {
    if (selectedKeys != null) {
        processSelectedKeysOptimized();
    } else {
        processSelectedKeysPlain(selector.selectedKeys());
    }
}

默認沒有使用優化的 Set,所有調用 processSelectedKeysPlain() 方法進行處理 IO 任務

private void processSelectedKeysPlain(Set<SelectionKey> selectedKeys) {

    if (selectedKeys.isEmpty()) {
        return;
    }

    Iterator<SelectionKey> i = selectedKeys.iterator();
    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();
            }
        }
    }
}

循環處理每個 selectionKey,每個selectionKey的處理首先根據attachment的類型來進行分發處理髮,這裏我們只分析 attachment 爲 AbstractNioChannel 的處理過程。

private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
    final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe();

    // 省略代碼 ......

    try {
        int readyOps = k.readyOps();
        // We first need to call finishConnect() before try to trigger a read(...) or write(...) as otherwise
        // the NIO JDK channel implementation may throw a NotYetConnectedException.
        if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
            // remove OP_CONNECT as otherwise Selector.select(..) will always return without blocking
            // See https://github.com/netty/netty/issues/924
            int ops = k.interestOps();
            ops &= ~SelectionKey.OP_CONNECT;
            k.interestOps(ops);

            unsafe.finishConnect();
        }


        if ((readyOps & SelectionKey.OP_WRITE) != 0) {
            // Call forceFlush which will also take care of clear the OP_WRITE once there is nothing left to write
            ch.unsafe().forceFlush();
        }

        if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
            unsafe.read();
        }
    } catch (CancelledKeyException ignored) {
        unsafe.close(unsafe.voidPromise());
    }
}

1、首先獲取 Channel 的 NioUnsafe,所有的讀寫等操作都在 Channel 的 unsafe 類中操作。
2、獲取 SelectionKey 就緒事件,如果是 OP_CONNECT,則說明已經連接成功,並把註冊的 OP_CONNECT 事件取消。
3、如果是 OP_WRITE 事件,說明可以繼續向 Channel 中寫入數據,當寫完數據後用戶自己吧 OP_WRITE 事件取消掉。
4、如果是 OP_READ 或 OP_ACCEPT 事件,則調用 unsafe.read() 進行讀取數據。unsafe.read() 中會調用到 ChannelPipeline 進行讀取數據。

private final class NioMessageUnsafe extends AbstractNioUnsafe {

        @Override
        public void read() {
            // 省略代碼 ......
            // 獲取 Channel 對應的 ChannelPipeline
            final ChannelPipeline pipeline = pipeline();

            boolean closed = false;
            Throwable exception = null;
            try {
                // 省略代碼 ......
                int size = readBuf.size();
                for (int i = 0; i < size; i ++) {
                    readPending = false;
                    // 委託給 pipeline 中的 Handler 進行讀取數據
                    pipeline.fireChannelRead(readBuf.get(i));
                }

當 NioEventLoop 讀取數據的時候會委託給 Channel 中的 unsafe 對象進行讀取數據。
Unsafe中真正讀取數據是交由 ChannelPipeline 來處理。
ChannelPipeline 中是註冊的我們自定義的 Handler,然後由 ChannelPipeline中的 Handler 一個接一個的處理請求的數據。
下一篇我們來分析 ChannelPipeline 原理。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章