Netty4源码深入学习2-Reactor模式

1 Reactor模式

  Reactor是一种模式,它要求主线程(I/O处理单元,下同)只负责监听文件描述上是否有事件发生,有的话就立即将该事件通知工作线程(逻辑单元,下同)。除此之外,主线程不做任何其他实质性的工作。读写数据,接受新的连接,以及处理客户请求均在工作线程中完成。
使用同步I/O模型(以epoll_wait为例)实现的Reactor模式的工作流程是:

1)主线程往epoll内核时间表中注册socket上的读就绪时间;
2)主线程调用epoll_wait等待socket上有数据可读。
3)当socket上有数据可读时,epoll_wait通知主线程,主线程则将socket可读时间放入请求队列。
4)此时唤醒请求队列上的某个工作线程,此线程从socket读取数据,并处理客户请求,
         然后往epoll内核时间表中注册该socket上的写就绪事件。
5)主线程调用epoll_wait等待socket可写
6)当socket可写时,epoll_wait通知主线程,主线程将socket可写事件放入请求队列。
7)唤醒请求队列上的某个工作线程,往socket上写入服务器处理客户请求的结果。

Reactor模式

简单来说

  Reactor模式是事件驱动的,有一个或多个并发输入源,有一个Service Handler,有多个Request Handlers;这个Service Handler会同步的将输入的请求(Event)多路复用的分发给相应的Request Handler。

Reactor单线程模型

  Reactor单线程模型就是指所有的IO操作都在同一个NIO线程上面完成的,也就是IO处理线程是单线程的。NIO线程的职责是:

(1)作为NIO服务端,接收客户端的TCP连接;
(2)作为NIO客户端,向服务端发起TCP连接;
(3)读取通信对端的请求或则应答消息;
(4)向通信对端发送消息请求或则应答消息。

如图所示:

Reactor单线程模型结构图

  从结构上,这有点类似生产者消费者模式,即有一个或多个生产者将事件放入一个Queue中,而一个或多个消费者主动的从这个Queue中Poll事件来处理;
  而Reactor模式则并没有Queue来做缓冲,每当一个Event输入到Service Handler之后,该Service Handler会立刻的根据不同的Event类型将其分发给对应的Request Handler来处理。
  这个做的好处有很多,首先我们可以将处理event的Request handler实现一个单独的线程,即如图所示:

9954986-f765204b41a02a7f.png

  这样Service Handler 和request Handler实现了异步,加快了service Handler处理event的速度,那么每一个request同样也可以以多线程的形式来处理自己的event,即Thread1 扩展成Thread pool 1,

2 Netty的Reactor线程模型

(1) Reactor单线程模型

  Reactor机制中保证每次读写能非阻塞读写:
   Acceptor类接收客户端的TCP请求消息,当链路建立成功之后,通过Dispatch将对应的ByteBuffer转发到指定的handler上,进行消息的处理。
如图所示:

netty的Reactor单线程模型

    一个线程(单线程)来处理CONNECT事件(Acceptor),一个线程池(多线程)来处理read,一个线程池(多线程)来处理write,那么从Reactor Thread到handler都是异步的,从而IO操作也多线程化。由于Reactor Thread依然为单线程,从性能上考虑依然有所限制。
    对于一些小容量的应用场景下,可以使用单线程模型,但是对于高负载、大并发的应用场景却不适合,主要原因如下:

(1)一个NIO线程处理成千上万的链路,性能无法支撑,即使CPU的负荷达到100%;

(2)当NIO线程负载过重,处理性能就会变慢,导致大量客户端连接超时然后重发请求,
     导致更多堆积未处理的请求,成为性能瓶颈。

(3)可靠性低,只有一个NIO线程,万一线程假死或则进入死循环,
     就完全不可用了,这是不能接受的。

(2) Reactor多线程模型

  Reactor多线程模型与单线程模型最大的区别在于,IO处理线程不再是一个线程,而是一组NIO处理线程。原理如下图所:

netty的Reactor多线程模型

  Reactor多线程模型的特点如下:

(1)有一个专门的NIO线程—-Acceptor线程用于监听服务端,接收客户端的TCP连接请求。

(2)网络IO操作—-读写等操作由一个专门的线程池负责,
     线程池可以使用JDK标准的线程池实现,包含一个任务队列和N个可用的线程,
     这些NIO线程就负责读取、解码、编码、发送。

(3)一个NIO线程可以同时处理N个链路,但是一个链路只对应一个NIO线程。

  通过Reactor Thread Pool来提高event的分发能力。
  Reactor多线程模型可以满足绝大多数的场景,除了一些个别的特殊场景:比如一个NIO线程负责处理客户所有的连接请求,但是如果连接请求中包含认证的需求(安全认证),在百万级别的场景下,就存在性能问题了,因为认证本身就要消耗CPU,为了解决这种情景下的性能问题,产生了第三种线程模型:Reactor主从线程模型。

(3) Reactor主从模型

  主从Reactor线程模型的特点是:
  服务端用于接收客户端连接的不再是一个单独的NIO线程,而是一个独立的NIO的线程池。Acceptor接收到客户端TCP连接请求并处理完成后(可能包含接入认证),再将新创建的SocketChannel注册到IO线程池(sub reactor)的某个IO处理线程上并处理编解码和读写工作。Acceptor线程池仅负责客户端的连接与认证,一旦链路连接成功,就将链路注册到后端的sub Reactor的IO线程池中。利用主从Reactor模型可以解决服务端监听线程无法有效处理所有客户连接的性能不足问题,这也是netty推荐使用的线程模型。

image.png

  Reactor就是一个执行while (true) { selector.select(); ...}循环的线程,会源源不断的产生新的事件,称作反应堆很贴切。
  事件又分为连接事件、IO读和IO写事件,一般把连接事件单独放一线程里处理,即主Reactor(MainReactor),IO读和IO写事件放到另外的一组线程里处理,即从Reactor(SubReactor),从Reactor线程数量一般为2*(CPUs - 1)。
所以在运行时,MainReactor只处理Accept事件,连接到来,马上按照策略转发给从Reactor之  一,只处理连接,故开销非常小;每个SubReactor管理多个连接,负责这些连接的读和写,属于IO密集型线程,读到完整的消息就丢给业务线程池处理业务,处理完比后,响应消息一般放到队列里,SubReactor会去处理队列,然后将消息写回。

(4) netty的线程模型

netty的线程模型是可以通过设置启动类的参数来配置的,设置不同的启动参数,netty支持Reactor单线程模型、多线程模型和主从Reactor多线程模型。Netty中的Reactor模型如下图:

Netty中的Reactor模型

  服务端启动时创建了两个NioEventLoopGroup,一个是boss(NioEventLoop Acceptor Pool),一个是worker(NioEventLoop IO Thread Pool)。实际上他们是两个独立的Reactor线程池,一个用于接收客户端的TCP连接,另一个用于处理Io相关的读写操作,或则执行系统的Task,定时Task。

--Boss线程池职责如下: 

(1)接收客户端的连接,初始化Channel参数 

(2)将链路状态变更时间通知给ChannelPipeline

--worker线程池作用是: 

(1)异步读取通信对端的数据报,发送读事件到ChannelPipeline 

(2)异步发送消息到通信对端,调用ChannelPipeline的消息发送接口 

(3)执行系统调用Task; 

(4)执行定时任务Task;

通过配置boss和worker线程池的线程个数以及是否共享线程池等方式,netty的线程模型可以在单线程、多线程、主从线程之间切换。

NioEventLoop

NioEventLoop是Netty的Reactor线程,它在Netty Reactor线程模型中的职责如下:

1. 作为服务端Acceptor线程,负责处理客户端的请求接入
2. 作为客户端Connector线程,负责注册监听连接操作位,用于判断异步连接结果
3. 作为IO线程,监听网络读操作位,负责从SocketChannel中读取报文
4. 作为IO线程,负责向SocketChannel写入报文发送给对方,
   如果发生写半包,会自动注册监听写事件,用于后续继续发送半包数据,
   直到数据全部发送完成

如下图,是一个NioEventLoop的处理链:

NioEventLoop的处理链

处理链中的处理方法是串行化执行的
一个客户端连接只注册到一个NioEventLoop上,避免了多个IO线程并发操作

Task

Netty Reactor线程模型中有两种Task:
  系统Task和定时Task

系统Task:创建它们的主要原因是,当IO线程和用户线程都在操作同一个资源时,
   为了防止并发操作时锁的竞争问题,将用户线程封装为一个Task,
   在IO线程负责执行,实现局部无锁化

定时Task:主要用于监控和检查等定时动作

基于以上原因,NioEventLoop不是一个纯粹的IO线程,它还会负责用户线程的调度.

IO线程的分配细节

  线程池对IO线程进行资源管理,是通过EventLoopGroup实现的。线程池平均分配channel到所有的线程(循环方式实现,不是100%准确),一个线程在同一时间只会处理一个通道的IO操作,这种方式可以确保我们不需要关心同步问题。
为了提升性能,netty在很多地方都进行了无锁设计。比如在IO线程内部进行串行操作,避免多  线程竞争造成的性能问题。表面上似乎串行化设计似乎CPU利用率不高,但是通过调整NIO线程池的线程参数,可以同时启动多个串行化的线程并行运行,这种局部无锁串行线程设计性能更优。

NioEventLoop是Reactor的核心线程,那么它就就必须实现多路复用。
EventLoopGroup的实现中使用next().register(channel)来完成channel的注册,
       即将channel注册时就绑定了一个EventLoop,
       然后EvetLoop将channel注册到EventLoop的Selector上。

NioEventLoopGroup还有几点需要注意:

NioEventLoopGroup下默认的NioEventLoop个数为cpu核数 * 2,因为有很多的io处理

NioEventLoop和java的single线程池在5里差异变大了,它本身不负责线程的创建销毁,
       而是由外部传入的线程池管理

channel和EventLoop是绑定的,即一旦连接被分配到EventLoop,
       其相关的I/O、编解码、超时处理都在同一个EventLoop中,
       这样可以确保这些操作都是线程安全的

服务端线程模型

下图为Netty的服务端线程模型:

服务端线程模型

下面结合Netty的源码,对服务端创建线程工作流程进行介绍。
第一步,从用户线程发起创建服务端操作,代码如下:

D0DF22AA-8B04-424B-852C-BDD3874C9922.jpg

  在创建服务端的时候实例化了2个EventLoopGroup,1个EventLoopGroup实际就是一个EventLoop线程组,负责管理EventLoop的申请和释放。

EventLoopGroup管理的线程数可以通过构造函数设置,
         如果没有设置,默认取-Dio.netty.eventLoopThreads,
         如果该系统参数也没有指定,则为可用的CPU内核数 × 2。
bossGroup线程组实际就是Acceptor线程池,负责处理客户端的TCP连接请求,
         如果系统只有一个服务端端口需要监听,则建议bossGroup线程组线程数设置为1。
workerGroup是真正负责I/O读写操作的线程组,
         通过ServerBootstrap的group方法进行设置,用于后续的Channel绑定。

NioEventLoopGroup的构造函数实现如图所示:

构造函数

由代码可知,初始化时未传入nThreads的数量,此时默认为0,继续debug跳转到MultithreadEventLoopGroup实现类,此时调用构造方法生成初始化的thread数量

BEF48402-E5A3-4A83-AEA6-1D02C91EEF56.jpg


  通过debug可以看到,当不传入参数时,nThreads默认为0,在此类中,初始化DEFAULT_EVENT_LOOP_THREADS,类中的static代码块中,判断生成多少个threads,

DEFAULT_EVENT_LOOP_THREADS = Math.max(1, 
               SystemPropertyUtil.getInt(
                         "io.netty.eventLoopThreads", 
                         Runtime.getRuntime().availableProcessors() * 2));
   获取计算机核数,并乘以2,作为变量数,

  在super构造函数中,如果nThreads==0时,使用DEFAULT_EVENT_LOOP_THREADS变量的值作为线程池中的线程数量。
  继续debug跳转到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) {
        if (nThreads <= 0) {
            throw new IllegalArgumentException(String.format("nThreads: %d (expected: > 0)", nThreads));
        }

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

        children = new EventExecutor[nThreads];

        for (int i = 0; i < nThreads; i ++) {
            boolean success = false;
            try {
                children[i] = newChild(executor, args);
                success = true;
            } catch (Exception e) {
                // TODO: Think about if this is a good exception type
                throw new IllegalStateException("failed to create a child event loop", e);
            } finally {
                if (!success) {
                    for (int j = 0; j < i; j ++) {
                        children[j].shutdownGracefully();
                    }

                    for (int j = 0; j < i; j ++) {
                        EventExecutor e = children[j];
                        try {
                            while (!e.isTerminated()) {
                                e.awaitTermination(Integer.MAX_VALUE, TimeUnit.SECONDS);
                            }
                        } catch (InterruptedException interrupted) {
                            // Let the caller handle the interruption.
                            Thread.currentThread().interrupt();
                            break;
                        }
                    }
                }
            }
        }

        chooser = chooserFactory.newChooser(children);

        final FutureListener<Object> terminationListener = new FutureListener<Object>() {
            @Override
            public void operationComplete(Future<Object> future) throws Exception {
                if (terminatedChildren.incrementAndGet() == children.length) {
                    terminationFuture.setSuccess(null);
                }
            }
        };

        for (EventExecutor e: children) {
            e.terminationFuture().addListener(terminationListener);
        }

        Set<EventExecutor> childrenSet = new LinkedHashSet<EventExecutor>(children.length);
        Collections.addAll(childrenSet, children);
        readonlyChildren = Collections.unmodifiableSet(childrenSet);
    }

  首先根据传入的nThreads线程数量创建初始化children:

 children = new EventExecutor[nThreads];

  然后循环创建每个children,

children[i] = newChild(executor, args);

  debug后跳入到类NioEventLoopGroup中进行实现


    @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]);
    }

  然后进入NioEventLoop的构造函数中。

    NioEventLoop(NioEventLoopGroup parent, Executor executor, SelectorProvider selectorProvider,
                 SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler) {
        super(parent, executor, false, DEFAULT_MAX_PENDING_TASKS, rejectedExecutionHandler);
        if (selectorProvider == null) {
            throw new NullPointerException("selectorProvider");
        }
        if (strategy == null) {
            throw new NullPointerException("selectStrategy");
        }
        provider = selectorProvider;
        selector = openSelector();
        selectStrategy = strategy;
    }

  逐步跳入super中,最终进入类SingleThreadEventExecutor

38366259-C4C7-4EB4-8B34-B29B84DDBDD4.jpg

  线程启动后,创建任务队列taskQueue,,是一个LinkedBlockingQueue结构。

selector的创建

如上图中 selector = openSelector();
进入方法openSelector中,
首先创建selector的存储空间:

final SelectedSelectionKeySet selectedKeySet = new SelectedSelectionKeySet();

跳入类SelectedSelectionKeySet中查看初始化:‘

SelectedSelectionKeySet类实现.jpg


  初始分配数组大小置为1024避免频繁扩容,当大小超过1024时,对数组进行双倍扩容;
  如上图添加数据,初始分配数组大小置为1024避免频繁扩容,当大小超过1024时,对数组进行双倍扩容。
创建过程:

      当数据量等于当前容量时,进行数据扩容,调用increaseCapacity方法进行扩容,
   扩容方法是新建一个数组newKeys,此数组大小为原数组keys大小的两倍,
   然后调用方法System.arraycopy将数组keys中的数据复制到newKeys中.
   此处与老版本不同。

总结一

总结:

      EventLoopGroup bossGroup = new NioEventLoopGroup()发生了以下事情:

      1、 为NioEventLoopGroup创建数量为:处理器个数 x 2的,类型为NioEventLoop的实例。
          每个NioEventLoop实例 都持有一个线程,以及一个类型为LinkedBlockingQueue的任务队列

      2、线程的执行逻辑由NioEventLoop实现

      3、每个NioEventLoop实例都持有一个selector,并对selector进行优化。


reactor 线程的执行

  NioEventLoop中维护了一个线程,线程启动时会调用NioEventLoop的run方法,执行I/O任务和非I/O任务。
首先我们debug次线程启动过程,从NettyServer开始debug:

 ChannelFuture f = b.bind(port).sync();  // 通过调用sync同步方法阻塞直到绑定成功

  进入bind中,进而进入initAndRegister方法,其中有一句代码:

ChannelFuture regFuture = config().group().register(channel);

EventLoop 与 Channel 的关联过程

  Netty 中, 每个 Channel 都有且仅有一个 EventLoop 与之关联, 它们的关联过程如下:

EventLoop 启动过程的时序图.png

  从上图中我们可以看到, 当调用了 AbstractChannel#AbstractUnsafe.register 后, 就完成了 Channel 和 EventLoop 的关联

@Override
        public final void register(EventLoop eventLoop, final ChannelPromise promise) {
            if (eventLoop == null) {
                throw new NullPointerException("eventLoop");
            }
            if (isRegistered()) {
                promise.setFailure(new IllegalStateException("registered to an event loop already"));
                return;
            }
            if (!isCompatible(eventLoop)) {
                promise.setFailure(
                        new IllegalStateException("incompatible event loop type: " + eventLoop.getClass().getName()));
                return;
            }

            AbstractChannel.this.eventLoop = eventLoop;

            if (eventLoop.inEventLoop()) {
                register0(promise);
            } else {
                try {
                    eventLoop.execute(new Runnable() {
                        @Override
                        public void run() {
                            register0(promise);
                        }
                    });
                } catch (Throwable t) {
                    logger.warn(
                            "Force-closing a channel whose registration task was not accepted by an event loop: {}",
                            AbstractChannel.this, t);
                    closeForcibly();
                    closeFuture.setClosed();
                    safeSetFailure(promise, t);
                }
            }
        }

  在 AbstractChannel#AbstractUnsafe.register 中, 会将一个 EventLoop 赋值给 AbstractChannel 内部的 eventLoop 字段, 到这里就完成了 EventLoop 与 Channel 的关联过程.
&emsp;&emsp;AbstractChannel.this.eventLoop = eventLoop;

EventLoop 的启动

  在前面我们已经知道了, NioEventLoop 本身就是一个 SingleThreadEventExecutor, 因此 NioEventLoop 的启动, 其实就是 NioEventLoop 所绑定的本地 Java 线程的启动.
  只要找到在哪里调用了 SingleThreadEventExecutor 的 thread 字段的 start() 方法就可以知道是在哪里启动的这个线程了.
  执行AbstractUnsafe.register 中的线程启动方法:

eventLoop.execute(new Runnable() {
                        @Override
                        public void run() {
                            register0(promise);
                        }
                    });

  进一步,进入register,一直debug最终进入SingleThreadEventExecutor类中的execute方法:


    @Override
    public void execute(Runnable task) {
        if (task == null) {
            throw new NullPointerException("task");
        }

        boolean inEventLoop = inEventLoop();
        if (inEventLoop) {
            addTask(task);
        } else {
            startThread();
            addTask(task);
            if (isShutdown() && removeTask(task)) {
                reject();
            }
        }

        if (!addTaskWakesUp && wakesUpForTask(task)) {
            wakeup(inEventLoop);
        }
    }

  外部线程在往任务队列里面添加任务的时候执行 startThread() ,netty会判断reactor线程有没有被启动,如果没有被启动,那就启动线程再往任务队列里面添加任务

    private void startThread() {
        if (STATE_UPDATER.get(this) == ST_NOT_STARTED) {
            if (STATE_UPDATER.compareAndSet(this, ST_NOT_STARTED, ST_STARTED)) {
                doStartThread();
            }
        }
    }

  STATE_UPDATER 是 SingleThreadEventExecutor内部维护的一个属性, 它的作用是标识当前的 thread 的状态. 在初始的时候,STATE_UPDATER == ST_NOT_STARTED, 因此第一次调用 startThread() 方法时, 就会进入到 if 语句内,然后执行 doStartThread方法。
  当 EventLoop.execute 第一次被调用时, 就会触发 startThread() 的调用, 进而导致了 EventLoop 所对应的 Java 线程的启动.SingleThreadEventExecutor 在执行doStartThread的时候,会调用内部执行器executor的execute方法,将调用NioEventLoop的run方法的过程封装成一个runnable塞到一个线程中去执行

NioEventLoop

  NioEventLoop中维护了一个线程,线程启动时会调用NioEventLoop的run方法,执行I/O任务和非I/O任务:
  I/O任务:
  即selectionKey中ready的事件,如accept、connect、read、write等,由processSelectedKeys方法触发。
  非IO任务:
  添加到taskQueue中的任务,如register0、bind0等任务,由runAllTasks方法触发。两种任务的执行时间比由变量ioRatio控制,默认为50,则表示允许非IO任务执行的时间与IO任务的执行时间相等。
  初始化时 ioRatio为50:

private volatile int ioRatio = 50;

线程执行主体为NioEventLoop的run方法。


 @Override
    protected void run() {
        for (;;) {
            try {
                switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) {
                    case SelectStrategy.CONTINUE:
                        continue;
                    case SelectStrategy.SELECT:
                        select(wakenUp.getAndSet(false)); //wakenUp 表示是否应该唤醒正在阻塞的select操作,
                            // 可以看到netty在进行一次新的loop之前,都会将wakeUp 被设置成false,
                            //标志新的一轮loop的开始,

                        // 'wakenUp.compareAndSet(false, true)' is always evaluated
                        // before calling 'selector.wakeup()' to reduce the wake-up
                        // overhead. (Selector.wakeup() is an expensive operation.)
                        //
                        // However, there is a race condition in this approach.
                        // The race condition is triggered when 'wakenUp' is set to
                        // true too early.
                        //
                        // 'wakenUp' is set to true too early if:
                        // 1) Selector is waken up between 'wakenUp.set(false)' and
                        //    'selector.select(...)'. (BAD)
                        // 2) Selector is waken up between 'selector.select(...)' and
                        //    'if (wakenUp.get()) { ... }'. (OK)
                        //
                        // In the first case, 'wakenUp' is set to true and the
                        // following 'selector.select(...)' will wake up immediately.
                        // Until 'wakenUp' is set to false again in the next round,
                        // 'wakenUp.compareAndSet(false, true)' will fail, and therefore
                        // any attempt to wake up the Selector will fail, too, causing
                        // the following 'selector.select(...)' call to block
                        // unnecessarily.
                        //
                        // To fix this problem, we wake up the selector again if wakenUp
                        // is true immediately after selector.select(...).
                        // It is inefficient in that it wakes up the selector for both
                        // the first case (BAD - wake-up required) and the second case
                        // (OK - no wake-up required).

                        if (wakenUp.get()) {
                            selector.wakeup();
                        }
                    default:
                        // fallthrough
                }

                cancelledKeys = 0;
                needsToSelectAgain = false;
                final int ioRatio = this.ioRatio;
                if (ioRatio == 100) {
                    try {
                        processSelectedKeys();
                    } finally {
                        // Ensure we always run tasks.
                        runAllTasks();
                    }
                } else {
                    final long ioStartTime = System.nanoTime();
                    try {
                        processSelectedKeys();
                    } finally {
                        // Ensure we always run tasks.
                        final long ioTime = System.nanoTime() - ioStartTime;
                        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);
            }
        }
    }

reactor action

  reactor线程大概做的事情分为对三个步骤不断循环

  1.首先轮询注册到reactor线程对用的selector上的所有的channel的IO事件

select(wakenUp.getAndSet(false));
if (wakenUp.get()) {
    selector.wakeup();
}

  2.处理产生网络IO事件的channel
  processSelectedKeys();
  3.处理任务队列
  runAllTasks(...);

  下面对每个步骤详细说明

1. select操作

select(wakenUp.getAndSet(false));
if (wakenUp.get()) {
      selector.wakeup();
}

wakenUp 表示是否应该唤醒正在阻塞的select操作,可以看到netty在进行一次新的loop之前,都会将wakeUp 被设置成false,标志新的一轮loop的开始,具体的select操作我们也拆分开来看:


    private void select(boolean oldWakenUp) throws IOException {
        Selector selector = this.selector;
        try {
            int selectCnt = 0;
            long currentTimeNanos = System.nanoTime(); //系统计时器的当前值,以毫微秒为单位。
            long selectDeadLineNanos = currentTimeNanos + delayNanos(currentTimeNanos);
            for (;;) {
                long timeoutMillis = (selectDeadLineNanos - currentTimeNanos + 500000L) / 1000000L;
                // 1.定时任务截至事时间快到了,中断本次轮询
                if (timeoutMillis <= 0) {
                    if (selectCnt == 0) {
                        selector.selectNow();
                        selectCnt = 1;
                    }
                    break;
                }

                // If a task was submitted when wakenUp value was true, the task didn't get a chance to call
                // Selector#wakeup. So we need to check task queue again before executing select operation.
                // If we don't, the task might be pended until select operation was timed out.
                // It might be pended until idle timeout if IdleStateHandler existed in pipeline.

                // 2.轮询过程中发现有任务加入,中断本次轮询
                if (hasTasks() && wakenUp.compareAndSet(false, true)) {
                    selector.selectNow();
                    selectCnt = 1;
                    break;
                }

                int selectedKeys = selector.select(timeoutMillis);
                selectCnt ++;

                if (selectedKeys != 0 || oldWakenUp || wakenUp.get() || hasTasks() || hasScheduledTasks()) {
                    // - Selected something,
                    // - waken up by user, or
                    // - the task queue has a pending task.
                    // - a scheduled task is ready for processing
                    break;
                }
                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.");
                    }
                    selectCnt = 1;
                    break;
                }

                long time = System.nanoTime();
                if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) {
                    // timeoutMillis elapsed without anything selected.
                    selectCnt = 1;
                } else 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();
                    selector = this.selector;

                    // Select again to populate selectedKeys.
                    selector.selectNow();
                    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);
            }
            // Harmless exception - log anyway
        }
    }

(1).定时任务截止事时间快到了,中断本次轮询

  NioEventLoop中reactor线程的select操作也是一个for循环,在for循环第一步中,如果发现当前的定时任务队列中有任务的截止事件快到了(<=0.5ms),就跳出循环。此外,跳出之前如果发现目前为止还没有进行过select操作(if (selectCnt == 0)),那么就调用一次selectNow(),该方法会立即返回,不会阻塞 。netty里面定时任务队列是按照延迟时间从小到大进行排序, delayNanos(currentTimeNanos)方法即取出第一个定时任务的延迟时间

/**
     * Returns the amount of time left until the scheduled task with the closest dead line is executed.
     */
    protected long delayNanos(long currentTimeNanos) {
        ScheduledFutureTask<?> scheduledTask = peekScheduledTask();
        if (scheduledTask == null) {
            return SCHEDULE_PURGE_INTERVAL;
        }

        return scheduledTask.delayNanos(currentTimeNanos);
    }

(2). 轮询过程中发现有任务加入,中断本次轮询

netty为了保证任务队列能够及时执行,在进行阻塞select操作的时候会判断任务队列是否为空,如果不为空,就执行一次非阻塞select操作,跳出循环

123.jpg

    @Override
    protected boolean hasTasks() {
        return super.hasTasks() || !tailTasks.isEmpty();
    }

  netty为了保证任务队列能够及时执行,在进行阻塞select操作的时候会判断任务队列是否为空,如果不为空,就执行一次非阻塞select操作,跳出循环

(3).阻塞式select操作

  执行到这一步,说明netty任务队列里面队列为空,并且所有定时任务延迟时间还未到(大于0.5ms),于是,在这里进行一次阻塞select操作,截止到第一个定时任务的截止时间。

for (;;) {
    // 1.定时任务截至事时间快到了,中断本次轮询
    ...
    // 2.轮询过程中发现有任务加入,中断本次轮询
    ...
    // 3.阻塞式select操作
    int selectedKeys = selector.select(timeoutMillis);
    selectCnt ++;
    if (selectedKeys != 0 || oldWakenUp || wakenUp.get() || hasTasks() || hasScheduledTasks()) {
        break;
    }
    ....
}

  如果第一个定时任务的延迟非常长,比如一个小时,那么有没有可能线程一直阻塞在select操作,当然有可能!But,只要在这段时间内,有新任务加入,该阻塞就会被释放。

外部线程调用execute方法添加任务

56.jpg

  在外部线程添加任务的时候,会调用wakeup方法来唤醒 selector.select(timeoutMillis)

    protected void wakeup(boolean inEventLoop) {
        if (!inEventLoop || STATE_UPDATER.get(this) == ST_SHUTTING_DOWN) {
            // Use offer as we actually only need this to unblock the thread and if offer fails we do not care as there
            // is already something in the queue.
            taskQueue.offer(WAKEUP_TASK);
        }
    }

  阻塞select操作结束之后,netty又做了一系列的状态判断来决定是否中断本次轮询,中断本次轮询的条件有

轮询到IO事件 (selectedKeys != 0)
oldWakenUp 参数为true
任务队列里面有任务(hasTasks)
第一个定时任务即将要被执行 (hasScheduledTasks())
用户主动唤醒(wakenUp.get())

(4)解决jdk的nio bug

  nio bug ,问题是围绕一个最初注册Selector的通道,因为I/O在服务器端关闭(由于早期客户端退出)。但是服务器端只有当它执行I/O(读/写),从而进入IO异常时才能知道这种通道。这种情况下,服务器端(选择器)不知道通道已经关闭(对等复位),从而出现错误操作,继续对 key(selector和channel的配对)进行空轮训,但是其相关的通道已关闭或无效。选择器会一直空轮训,从而导致cpu使用率100%。
  在NIO中通过Selector的轮询当前是否有IO事件,根据JDK NIO api描述,Selector的select方法会一直阻塞,直到IO事件达到或超时,但是在Linux平台上这里有时会出现问题,在某些场景下select方法会直接返回,即使没有超时并且也没有IO事件到达,这就是著名的epoll bug,这是一个比较严重的bug,它会导致线程陷入死循环,会让CPU飙到100%,极大地影响系统的可靠性,到目前为止,JDK都没有完全解决这个问题。
  JAVA NIO 中的SelectorImpl.run()方法:

    public void run()
    {
        setName("SelectorThread");
        while (!closed) {    <1>
            try {
                int n = 0;
                if (timeout == 0 && orb.transportDebugFlag) {
                    dprint(".run: Beginning of selection cycle");
                }
                handleDeferredRegistrations();
                enableInterestOps();
                try {
                    n = selector.select(timeout);  //n ==0 例如远端服务器断掉,导致select并不知道服务器断掉,每次调用都返回0.此时不断轮询,最终挂掉
                } catch (IOException  e) {
                    if (orb.transportDebugFlag) {
                        dprint(".run: selector.select: ", e);
                    }
                } catch (ClosedSelectorException csEx) {
                    if (orb.transportDebugFlag) {
                        dprint(".run: selector.select: ", csEx);
                    }
                    break;
                }
                if (closed) {
                    break;
                }
                /*
                  if (timeout == 0 && orb.transportDebugFlag) {
                  dprint(".run: selector.select() returned: " + n);
                  }
                  if (n == 0) {
                  continue;
                  }
                */
                Iterator iterator = selector.selectedKeys().iterator();
                if (orb.transportDebugFlag) {
                    if (iterator.hasNext()) {
                        dprint(".run: n = " + n);
                    }
                }
                while (iterator.hasNext()) {  //如果n==0一直存在,此时不再执行下面代码,跳转到<1>继续执行,
              //形成死循环,不断的轮询,直到linux系统出现100%的CPU情况
                    SelectionKey selectionKey = (SelectionKey) iterator.next();
                    iterator.remove();
                    EventHandler eventHandler = (EventHandler)
                        selectionKey.attachment();
                    try {
                        eventHandler.handleEvent();
                    } catch (Throwable t) {
                        if (orb.transportDebugFlag) {
                            dprint(".run: eventHandler.handleEvent", t);
                        }
                    }
                }
                if (timeout == 0 && orb.transportDebugFlag) {
                    dprint(".run: End of selection cycle");
                }
            } catch (Throwable t) {
                // IMPORTANT: ignore all errors so the select thread keeps running.
                // Otherwise a guaranteed hang.
                if (orb.transportDebugFlag) {
                    dprint(".run: ignoring", t);
                }
            }
        }
        try {
            if (selector != null) {
                if (orb.transportDebugFlag) {
                    dprint(".run: selector.close ");
                }
                selector.close();
            }
        } catch (Throwable t) {
            if (orb.transportDebugFlag) {
                dprint(".run: selector.close: ", t);
            }
        }
    }

Netty解决方式:

1. 对Selector的select操作周期进行统计,每完成一次空的select操作进行一次计数,

   netty 会在每次进行 selector.select(timeoutMillis) 之前记录一下开始时间currentTimeNanos,在select之后记录一下结束时间,判断select操作是否至少持续了timeoutMillis秒。如果持续的时间大于等于timeoutMillis,说明就是一次有效的轮询,重置selectCnt标志。

轮训计数.png

selectCnt计数重置为1.png

2. 若在某个周期内连续发生N次空轮询,则触发了epoll死循环bug。

   当空轮询的次数超过一个阀值的时候,默认是512,就开始重建selector。

Selector重建.png

3. 重建Selector,判断是否是其他线程发起的重建请求,若不是则将原SocketChannel从旧的Selector上去除注册,重新注册到新的Selector上,并将原来的Selector关闭。

   下面我们简单描述一下netty 通过rebuildSelector来fix空轮询bug的过程,rebuildSelector的操作其实很简单:new一个新的selector,将之前注册到老的selector上的的channel重新转移到新的selector上。

selector重建.jpg

重建过程
1. 拿到有效的key
2. 取消该key在旧的selector上的事件注册
3. 将该key对应的channel注册到新的selector上
4. 重新绑定channel和新的key的关系: selector = newSelector;
5. 将原有的selector废弃:oldSelector.close();



作者:未名枯草
链接:https://www.jianshu.com/p/4e836460512c
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

猜你喜欢

转载自blog.csdn.net/zhousenshan/article/details/82818283