对 NioEventLoop的些许解读

681 阅读11分钟

解读 NioEventLoop

在创建出 NioEventLoop对应 thread后,其 thread是如何来进行工作的?

由前述,我们知道,其线程创建时机便是第一次往 NioEventLoop中提交任务,即

    private void doStartThread() {
        assert thread == null;
        // executor这个是关键:在 Group构造时分析过了, 这里的 executor类型是 ThreadPerTaskExecutor,
        // 其内部包含着线程工厂(DefaultThreadFactory类型), 以此来进行线程的创建.
        executor.execute(new Runnable() {
            // 这里设计得挺好的, 通过往 executor中提交任务, 提交的任务会去创建出一个线程来执行
            @Override
            public void run() {
                // 将当前创建出来的线程赋予到 thread上, 依赖完成对 EventLoop中线程的绑定与创建
                thread = Thread.currentThread();
                if (interrupted) {
                    thread.interrupt();
                }

                boolean success = false;
                updateLastExecutionTime();
                try {
                    // 关键方法, 切换到 NioEventLoop.run() - 诠释了 EventLoop中线程是如何来工作的
                    SingleThreadEventExecutor.this.run();
                    success = true;
                } catch (Throwable t) {
                    logger.warn("Unexpected exception from an event executor: ", t);
                } finally {
                    for (;;) {
                        int oldState = state;
                        if (oldState >= ST_SHUTTING_DOWN || STATE_UPDATER.compareAndSet(
                                SingleThreadEventExecutor.this, oldState, ST_SHUTTING_DOWN)) {
                            break;
                        }
                    }

                    // Check if confirmShutdown() was called at the end of the loop.
                    if (success && gracefulShutdownStartTime == 0) {
                        if (logger.isErrorEnabled()) {
                            logger.error("Buggy " + EventExecutor.class.getSimpleName() + " implementation; " +
                                    SingleThreadEventExecutor.class.getSimpleName() + ".confirmShutdown() must " +
                                    "be called before run() implementation terminates.");
                        }
                    }

                    try {
                        // Run all remaining tasks and shutdown hooks. At this point the event loop
                        // is in ST_SHUTTING_DOWN state still accepting tasks which is needed for
                        // graceful shutdown with quietPeriod.
                        for (;;) {
                            if (confirmShutdown()) {
                                break;
                            }
                        }

                        // Now we want to make sure no more tasks can be added from this point. This is
                        // achieved by switching the state. Any new tasks beyond this point will be rejected.
                        for (;;) {
                            int oldState = state;
                            if (oldState >= ST_SHUTDOWN || STATE_UPDATER.compareAndSet(
                                    SingleThreadEventExecutor.this, oldState, ST_SHUTDOWN)) {
                                break;
                            }
                        }

                        // We have the final set of tasks in the queue now, no more can be added, run all remaining.
                        // No need to loop here, this is the final pass.
                        confirmShutdown();
                    } finally {
                        try {
                            cleanup();
                        } finally {
                            // Lets remove all FastThreadLocals for the Thread as we are about to terminate and notify
                            // the future. The user may block on the future and once it unblocks the JVM may terminate
                            // and start unloading classes.
                            // See https://github.com/netty/netty/issues/6596.
                            FastThreadLocal.removeAll();

                            STATE_UPDATER.set(SingleThreadEventExecutor.this, ST_TERMINATED);
                            threadLock.countDown();
                            int numUserTasks = drainTasks();
                            if (numUserTasks > 0 && logger.isWarnEnabled()) {
                                logger.warn("An event executor terminated with " +
                                        "non-empty task queue (" + numUserTasks + ')');
                            }
                            terminationFuture.setSuccess(null);
                        }
                    }
                }
            }
        });
    }

可以看到,SingleThreadEventExecutor.this.run(),这一步便诠释了 EventLoop中线程的工作原理

run()

    // NioEventLoop的工作逻辑
    @Override
    protected void run() {
        // epoll bug的特征计数变量
        int selectCnt = 0;
        // 死循环
        for (;;) {
            try {
                // 1. >= 0:表示 selector的返回值 - 注册到 selector上的已就绪的 Channel数量
                // 2. < 0:常量状态:CONTINUE、BUSY_WAIT、SELECT
                int strategy;
                try {
                    // selectStrategy -> DefaultSelectStrategy
                    // 根据 NioEventLoop是否有本地任务, 来决定怎么处理
                    // 1.有本地任务, 调用多路复用器的 selectNow()方法, 返回多路复用器上就绪的 Channel数量
                    // 2.没有本地任务, 返回 -1, 下面会根据常量再进行一些相应的处理
                    strategy = selectStrategy.calculateStrategy(selectNowSupplier, hasTasks());

                    // 因此, 这里我们只需要来考虑 >=0, -1这两种情况即可

                    switch (strategy) {
                    case SelectStrategy.CONTINUE:
                        continue;

                    case SelectStrategy.BUSY_WAIT:
                        // fall-through to SELECT since the busy-wait is not supported with NIO//
                    case SelectStrategy.SELECT: // NioEventLoop中没有任务, 返回 -1的情况
                        // 获取 可调度任务的执行时间
                        long curDeadlineNanos = nextScheduledTaskDeadlineNanos();
                        // true - 说明 eventLoop中并没有需要周期执行的任务
                        if (curDeadlineNanos == -1L) {
                            // 设置成 long最大值
                            curDeadlineNanos = NONE; // nothing on the calendar
                        }
                        nextWakeupNanos.set(curDeadlineNanos);
                        try {
                            // true - 没有本地普通任务执行
                            if (!hasTasks()) {
                                // curDeadlineNanos: long最大值, 没有需要执行的周期性任务
                                // curDeadlineNanos: 周期性任务需要被执行的截止时间
                                // 最终返回的便是, selector上就绪的 Channel事件个数
                                // 注:由于 Linux下 epoll的 bug, 对于突然中断的 Socket会导致 selector唤醒, 此时并没有对于的 Channel事件就绪, 而 selector将一直处于非阻塞状态
                                // 对应的便是此时的 strategy == 0, selctor.select不再阻塞, Netty又是如何来解决的 ?
                                strategy = select(curDeadlineNanos); // 这一步来决定调用哪种 select方法
                            }
                        } finally {
                            // This update is just to help block unnecessary selector wakeups
                            // so use of lazySet is ok (no race condition)
                            nextWakeupNanos.lazySet(AWAKE);
                        }
                        // 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();
                    selectCnt = 0;
                    handleLoopException(e);
                    continue;
                }

                // 来到这里, strategy表示的便是 selector上已就绪的 channel事件个数


                selectCnt++; // 这是个关键的变量, 表示的便是当前循环迭代次数 - 对应的便是 selector无效 select的次数
                cancelledKeys = 0;
                needsToSelectAgain = false;

                // 线程处理 IO事件的实际占比, 默认是 50%
                final int ioRatio = this.ioRatio;
                // 表示本轮线程有没有去处理过本地任务
                boolean ranTasks;

                // true - IO优先, 优 先去处理 IO任务, 然后再去处理本地任务
                if (ioRatio == 100) {
                    try {
                        // 判断有没有要去处理的 IO事件
                        if (strategy > 0) {
                            // 执行 IO事件入口
                            processSelectedKeys();
                        }
                    } finally {
                        // Ensure we always run tasks.
                        // 执行本地任务队列内的任务
                        ranTasks = runAllTasks();
                    }
                }
                // 条件成立, 说明当前 eventLoop中的 selector上有就绪的 Channel事件
                else if (strategy > 0) {
                    // 表示 IO事件处理开始时间
                    final long ioStartTime = System.nanoTime();
                    try {
                        // 开始处理 IO事件
                        processSelectedKeys();
                    } finally {
                        // Ensure we always run tasks.
                        // 计算出 IO事件的处理时长
                        final long ioTime = System.nanoTime() - ioStartTime;
                        // ioTime * (100 - ioRatio) / ioRatio -> 根据 IO占比以及 IO事件处理时长计算出本地任务处理的最大时长
                        // 这一步可以看出, 本地任务的执行是有限制的
                        // 这一步也可以看出, IO占比是来干什么的, 以及 IO事件的执行时长是没有限制的
                        ranTasks = runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
                    }
                }
                // 条件成立, 说明 eventLoop中的 selector上没有就绪的 Channel事件, 那么只来处理本地任务即可
                else {
                    // 执行最少数目的本地任务
                    // timeoutNanos == 0 - 这里最多只会去执行 64个任务
                    ranTasks = runAllTasks(0); // This will run the minimum number of tasks
                }

                if (ranTasks || strategy > 0) {
                    if (selectCnt > MIN_PREMATURE_SELECTOR_RETURNS && logger.isDebugEnabled()) {
                        logger.debug("Selector.select() returned prematurely {} times in a row for Selector {}.",
                                selectCnt - 1, selector);
                    }
                    // 正常 NioEventLoop线程从 selector上唤醒后工作 是因为有 IO事件
                    // 这里会把 selectCnt置为 0
                    selectCnt = 0;
                }
                // unexpectedSelectorWakeup - 其实就是对 Selector出 bug的一个解决方案 - Netty为我们兜底了
                // 来到这, 说明正是 seletcor发生了 bug, 对应的便是 ranTask == false && strategy == 0
                // selectCnt - 表示的便是当前循环迭代次数 - 对应的便是 selector无效 select的次数
                else if (unexpectedSelectorWakeup(selectCnt)) { // Unexpected wakeup (unusual case)
                    // 将 selectCnt置为 0, 并且 selector已经修复完毕
                    selectCnt = 0;
                }
            } catch (CancelledKeyException e) {
                // Harmless exception - log anyway
                if (logger.isDebugEnabled()) {
                    logger.debug(CancelledKeyException.class.getSimpleName() + " raised by a Selector {} - JDK bug?",
                            selector, e);
                }
            } catch (Error e) {
                throw (Error) e;
            } catch (Throwable t) {
                handleLoopException(t);
            } finally {
                // Always handle shutdown even if the loop processing threw an exception.
                try {
                    if (isShuttingDown()) {
                        closeAll();
                        if (confirmShutdown()) {
                            return;
                        }
                    }
                } catch (Error e) {
                    throw (Error) e;
                } catch (Throwable t) {
                    handleLoopException(t);
                }
            }
        }
    }

由上述,可以看出,对于 NioEventLoop而言,其主要去干两件事,一个是 IO任务,一个便是任务队列中的任务,由 NioEventLoop的继承体系我们也知,其本身去继承了 JDK中的 ScheduledExecutorService,因此其任务队列也有两种:普通任务、以及需要被调度的任务 (定时任务、周期性任务)

NioEventLoop实际上就是通过一个实现其工作任务的处理的,其最关心的也是 IO事件对应的任务,当其没有本地任务需要去执行时,那么便会去调用 Selector.select()去监听其管理的 Channel上有没有事件发生,当然因为可能有需要被调度的任务存在,因此其会去通过获取需要被调度的任务队列去获取优先级最高的任务对应的截止时间来计算出一个最长可 select的时间,进行支持超时机制的 select()

如果一开始便有本地任务的话,其会去调用 selectNow()非阻塞地获取 Selector上就绪的 Channel事件个数,其值 >= 0

这一步可从这看出来:

strategy = selectStrategy.calculateStrategy(selectNowSupplier, hasTasks());
    /**
     *     private final IntSupplier selectNowSupplier = new IntSupplier() {
     *         @Override
     *         public int get() throws Exception {
     *             return selectNow(); // 调用的是多路复用器的 selectNow()方法, 该方法不会去阻塞线程, 直接返回
     *         }
     *     };
     * @param selectSupplier The supplier with the result of a select result.
     * @param hasTasks true if tasks are waiting to be processed.
     * @return
     * @throws Exception
     */
    @Override
    public int calculateStrategy(IntSupplier selectSupplier, boolean hasTasks) throws Exception {
        // NioEventLoop中有本地任务 ? 调用 Selector.selectNow() : -1
        return hasTasks ? selectSupplier.get() : SelectStrategy.SELECT;
    }

再往下看,我们发现当这一步计算出来的 strategy == 0时,即没有 IO事件要去处理的话,NioEventLoop又是怎么来进行工作的呢?

即回来到这个分支:

ranTasks = runAllTasks(0); // This will run the minimum number of tasks

对应:SingleThreadEventExecutor

    // timeoutNanos 执行任务最长可用耗时
    protected boolean runAllTasks(long timeoutNanos) {
        // 转移需要被调度的任务到普通任务队列中去
        fetchFromScheduledTaskQueue();
        Runnable task = pollTask(); // 获取普通队列对头任务
        if (task == null) {
            afterRunningAllTasks();
            return false;
        }

        // 计算出执行任务的截止时间, 这是一个准确的时间点
        final long deadline = timeoutNanos > 0 ? ScheduledFutureTask.nanoTime() + timeoutNanos : 0;
        // 已经执行的任务
        long runTasks = 0;
        // 最后一个任务的执行时间戳
        long lastExecutionTime;
        for (;;) {
            // 执行当前任务
            safeExecute(task);

            runTasks ++; // 累计执行的任务

            // Check timeout every 64 tasks because nanoTime() is relatively expensive. 每隔 64个任务可来检查一下 deadline
            // XXX: Hard-coded value - will make it configurable if it is really a problem.
            // runTasks & 111111
            // 64 & 0111111 == 0
            if ((runTasks & 0x3F) == 0) {
                lastExecutionTime = ScheduledFutureTask.nanoTime();
                // 判断此次这批任务的最后一个任务的执行时间是否已超过 deadline
                if (lastExecutionTime >= deadline) {
                    break;
                }
            }
            // 获取下一个任务
            task = pollTask();
            if (task == null) {
                lastExecutionTime = ScheduledFutureTask.nanoTime();
                break; // 没有任务了, 退出自旋
            }
        }

        afterRunningAllTasks();
        this.lastExecutionTime = lastExecutionTime;
        return true;
    }

可以看到,这一步计算出的 deadline == 0,这也就决定了本次只会去执行 64个任务,正如注释所示:This will run the minimum number of tasks

其默认是以 64个任务为一个工作单元,每执行完 64个任务便会去检查是否还有时间可以去执行别的时间,有的话再去执行 64个任务,没有的话之心 break,返回即可

epoll bug?

我们知道,在传统的 NIO中实际上存在着一个 bug,即 epoll轮询导致 cpu飙升,而 Netty为我们提供了解决方案,在了解具体解决之前,我们先来了解这 bug发生的原因以及后果

官方文档:[Bug ID: JDK-6670302 (se) NIO selector wakes up with 0 selected keys infinitely lnx 2.4] (java.com)

文档中对 bug的描述:bug发生主要源于 epoll轮询的方式存在问题,在 Linux 2.4的 kernal中,poll和 epoll中突然中断 socket会导致 Selector被意外唤醒,此时返回的 Channel事件个数为 0,并且 NIO会照样不断从 Selector.select()本该阻塞的情况下 wake up出来,这将导致 select方法调用的不再阻塞,并且没有事件可进行处理,处于不断自旋中,这将导致 cpu的飙升

很遗憾,JDK NIO中并没有为我们提供解决方案,因此我们在实现 NIO框架时此问题的考虑通常也是必须的,不过好在 Netty为 epoll bug进行兜底!

Netty解决

由 run()方法中可以看到,每次进行一次循环迭代,就会去将 selectCnt此变量的值进行自增,我们知道,当 epoll bug发生时,对应的 bug特征便是此次循环没有去任何的事情,对应的便是 strategy == 0以及 ranTask == true

基于此,回来到这分支:

                if (ranTasks || strategy > 0) {
                    if (selectCnt > MIN_PREMATURE_SELECTOR_RETURNS && logger.isDebugEnabled()) {
                        logger.debug("Selector.select() returned prematurely {} times in a row for Selector {}.",
                                selectCnt - 1, selector);
                    }
                    // 正常 NioEventLoop线程从 selector上唤醒后工作 是因为有 IO事件
                    // 这里会把 selectCnt置为 0
                    selectCnt = 0;
                }
                // unexpectedSelectorWakeup - 其实就是对 Selector出 bug的一个解决方案 - Netty为我们兜底了
                // 来到这, 说明正是 seletcor发生了 bug, 对应的便是 ranTask == false && strategy == 0
                // selectCnt - 表示的便是当前循环迭代次数 - 对应的便是 selector无效 select的次数
                else if (unexpectedSelectorWakeup(selectCnt)) { // Unexpected wakeup (unusual case)
                    // 将 selectCnt置为 0, 并且 selector已经修复完毕
                    selectCnt = 0;
                }

可以看到,如果本轮有去执行任务或者说是 strategy大于等于 0,这与说明此时的 Selector还是处于正常工作情况

因为我们考虑的便是方法 unexpectedSelectorWakeup()

    // returns true if selectCnt should be reset
    private boolean unexpectedSelectorWakeup(int selectCnt) {
        if (Thread.interrupted()) {
            // Thread was interrupted so reset selected keys and break so we not run into a busy loop.
            // As this is most likely a bug in the handler of the user or it's client library we will
            // also log it.
            //
            // See https://github.com/netty/netty/issues/2426
            if (logger.isDebugEnabled()) {
                logger.debug("Selector.select() returned prematurely because " +
                        "Thread.currentThread().interrupt() was called. Use " +
                        "NioEventLoop.shutdownGracefully() to shutdown the NioEventLoop.");
            }
            return true;
        }
        // true - 说明 selector无效 select已达到 512次, 此时对 selector进行修复
        if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 &&
                selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {
            // The selector returned prematurely many times in a row.
            // Rebuild the selector to work around the problem.
            logger.warn("Selector.select() returned prematurely {} times in a row; rebuilding Selector {}.",
                    selectCnt, selector);
            // 这便是修复的方案
            rebuildSelector();
            return true;
        }
        return false;
    }

在方法中会去判断 Selector是不是被意外唤醒的,判断的条件的便是迭代的次数是否达到了 512,对应 selectCnt >= 512?

当满足了此条件,便会去对 Selector进行修复

rebuildSelector()

    public void rebuildSelector() {
        if (!inEventLoop()) {
            execute(new Runnable() {
                @Override
                public void run() {
                    rebuildSelector0();
                }
            });
            return;
        }
        rebuildSelector0();
    }

    @Override
    public int registeredChannels() {
        return selector.keys().size() - cancelledKeys;
    }

    private void rebuildSelector0() {
        final Selector oldSelector = selector;
        final SelectorTuple newSelectorTuple;

        if (oldSelector == null) {
            return;
        }

        try {
            // 可以看到, 修复 seletcor的方案 - 重新创建 seletcor并对其进行初始化
            newSelectorTuple = openSelector();
        } catch (Exception e) {
            logger.warn("Failed to create a new Selector.", e);
            return;
        }

        // Register all channels to the new Selector.
        int nChannels = 0;
        for (SelectionKey key: oldSelector.keys()) {
            Object a = key.attachment();
            try {
                if (!key.isValid() || key.channel().keyFor(newSelectorTuple.unwrappedSelector) != null) {
                    continue;
                }

                int interestOps = key.interestOps();
                key.cancel();
                SelectionKey newKey = key.channel().register(newSelectorTuple.unwrappedSelector, interestOps, a);
                if (a instanceof AbstractNioChannel) {
                    // Update SelectionKey
                    ((AbstractNioChannel) a).selectionKey = newKey;
                }
                nChannels ++;
            } catch (Exception e) {
                logger.warn("Failed to re-register a Channel to the new Selector.", e);
                if (a instanceof AbstractNioChannel) {
                    AbstractNioChannel ch = (AbstractNioChannel) a;
                    ch.unsafe().close(ch.unsafe().voidPromise());
                } else {
                    @SuppressWarnings("unchecked")
                    NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
                    invokeChannelUnregistered(task, key, e);
                }
            }
        }

        selector = newSelectorTuple.selector;
        unwrappedSelector = newSelectorTuple.unwrappedSelector;

        try {
            // time to close the old selector as everything else is registered to the new one
            oldSelector.close();
        } catch (Throwable t) {
            if (logger.isWarnEnabled()) {
                logger.warn("Failed to close the old Selector.", t);
            }
        }

        if (logger.isInfoEnabled()) {
            logger.info("Migrated " + nChannels + " channel(s) to the new Selector.");
        }
    }

可以看到,所谓的对 Selector进行修复,其实就是来重新创建一个 Selector,并且对 Selector进行初始化,其实就是对其状态的还原,对应的便是 Channel的注册,以及最终去关闭存在 bug的 OldSelector

之后,便是将 selectCtn设置为 0,重新开始正常的工作

当然,如果没有满足判断 bug的条件,那么对应的便是,继续自旋,不断对 selectCtn自增,直到 512次

这不就解决了 NIO中由于 epoll bug导致 cpu飙升的痛点吗?即,Netty为 epoll的使用进行了兜底

其实代码中注释得却是很清楚了:The selector returned prematurely many times in a row. Rebuild the selector to work around the problem

由于选择器连续过早返回多次;重建选择器来解决问题!

至此,对 NioEventLoop的解读完毕!