netty③ 源码- NioEventloop执行流程

306 阅读11分钟

上文说到基于NioEventloop注册channel时,会创建一线程并持有其引用,线程内部调用的就是NioEventLoop#run方法。 我们看下这个方法都干了些啥。

NioEventLoop#run

protected void run() {
    for (;;) {
        try {
            //hasTask就是判断taskQuene 或者 tailTasks是否为空 
            //calculateStrategy方法就是根据有无任务 无任务就返回SelectStrategy.SELECT
               //有任务就执行selector.selectNow(); 这是非阻塞的 会直接返回已准备好的通道数
            switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) {
                case SelectStrategy.CONTINUE:
                    continue;
                case SelectStrategy.SELECT:
                   //阻塞直到一个通道就绪 或者有其他线程唤醒
                   //select之前会将wakenUp状态修改为false,表示可能线程进入阻塞状态
                    select(wakenUp.getAndSet(false));
                    if (wakenUp.get()) {
                        selector.wakeup();
                    }
                default:
                    // fallthrough
            }

            cancelledKeys = 0;
            //select执行完毕后 将needsToSelectAgain 为false  为何 ?
            //我们知道 当channel注册到selector上时会返回一个selectionKey,
           //当selectionKey取消的时候,也就是channel从selector上移除的时候,cancelledKeys计数会+1,
           //当cancelledKeys >= 256时,会将 needsToSelectAgain置为true,同时 cancelledKeys重置为0
           //这么做的寓意在下方 if (needsToSelectAgain) { 可以看到
            needsToSelectAgain = false;
            final int ioRatio = this.ioRatio;
            if (ioRatio == 100) {
                try {
                    processSelectedKeys();
                } finally {
                    //Ensure we always run tasks.
                    runAllTasks();
                }
            } else {
                //默认ioRatio设置为50
                final long ioStartTime = System.nanoTime();
                try {
                   //2.处理产生io事件的channel
                    processSelectedKeys();
                } finally {
                    //3.处理任务队列  执行时间由处理io事件的时间动态算得,默认是1:1
                    final long ioTime = System.nanoTime() - ioStartTime;
                    runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
                }
            }
        } catch (Throwable t) {
            handleLoopException(t);
        }
        ....
    }
}

从run方法中可以看到3块

  1. select调用
  2. 处理select产生的io事件
  3. 处理任务队列

1.NioEventLoop#select

private void select(boolean oldWakenUp) throws IOException {
    Selector selector = this.selector;
    try {
        int selectCnt = 0;
        long currentTimeNanos = System.nanoTime();
        //delayNanos 判断 scheduledTaskQueue有无定时任务 没有的话默认返回1s 有则返回最近的定时任务时间 
        long selectDeadLineNanos = currentTimeNanos + delayNanos(currentTimeNanos);
        for (;;) {
       //只有500000/1000000 = 0.5取整是为0的,只有当最近的定时任务延迟时间大于0.5ms,timeoutMillis才会>=1 
            long timeoutMillis = (selectDeadLineNanos - currentTimeNanos + 500000L) / 1000000L;
            //定时任务截止时间快到了 低于0.5ms 中断本次轮询
            if (timeoutMillis <= 0) {
                //如果没有进行过select操作就就调用一次非阻塞的selectNow
                if (selectCnt == 0) {
                    selector.selectNow();
                    selectCnt = 1;
                }
                break;
            }
            //有任务加入  中断本次轮询
            if (hasTasks() && wakenUp.compareAndSet(false, true)) {
                selector.selectNow();
                selectCnt = 1;
                break;
            }
            //阻塞 select,假设 timeoutMillis时间非常宽裕,则有可能长时间阻塞,但只有NioEventloop中有任务加入,就有可能进行唤醒
            int selectedKeys = selector.select(timeoutMillis);
            //每“阻塞”式执行完一次就+1
            selectCnt ++;
            //唤醒之后 判断是否满足,如满足就中断轮询
              a. 有io事件
              b. 被唤醒
              c. 有任务
              d. 第一个定时任务将要被执行
            if (selectedKeys != 0 || oldWakenUp || wakenUp.get() || hasTasks() || hasScheduledTasks()) {
                break;
            }
            if (Thread.interrupted()) {
                ..
                selectCnt = 1;
                break;
            }
            //解决jdk selector 空轮询bug
            long time = System.nanoTime();
            //如果阻塞的时间大于等于timeoutMillis 那么就是一次有效的轮询
              //否则相当于没有阻塞  一旦有io事件 在上面的selectedKeys != 0就已经break 
              //这里只有可能是触发了空轮询bug 或者被其他线程添加任务唤醒了
            if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) {
                selectCnt = 1;
            } else if 
            //默认值是512 如果空轮询的次数超过512次 那么需要重建selector
             (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 &&
                    selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {
                1.1 重建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.1 重建selector rebuildSelector

NioEventLoop#rebuildSelector

public void rebuildSelector() {
    ...
    final Selector oldSelector = selector;
    final Selector newSelector;

    if (oldSelector == null) {
        return;
    }

    try {
        newSelector = 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 (;;) {
        try {
            for (SelectionKey key: oldSelector.keys()) {
              //前文有说过 注册的时候netty的 socketChannel会作为attchment绑定起来
                Object a = key.attachment();
                try {
                    //已经转移到新的selector
                    if (!key.isValid() || key.channel().keyFor(newSelector) != null) {
                        continue;
                    }
                    int interestOps = key.interestOps();
                    //取消关系
                    key.cancel();
                    //注册到新的selector
                    SelectionKey newKey = key.channel().register(newSelector, interestOps, a);
                    if (a instanceof AbstractNioChannel) {
                        //引用更新
                        ((AbstractNioChannel) a).selectionKey = newKey;
                    }
                    nChannels ++;
                } catch (Exception e) {
                    ...
                }
            }
        } catch (ConcurrentModificationException e) {
            //故障重试
            continue;
        }
        break;
    }
    selector = newSelector;
    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);
        }
    }
}

1.2 jdk selector空轮询bug

以我在 netty前序 -io模式 中代码举例

 public class SelectServerSockect {
    public static void main(String[] args) throws IOException {
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.bind(new InetSocketAddress(8080));
        ssc.configureBlocking(false);
        Selector selector = Selector.open();
        ssc.register(selector, SelectionKey.OP_ACCEPT);
        while (true){
           //阻塞直到一个通道就绪,SelectionKey中的ready集合将会更新,返回值为就绪的通道数 
           //当出现轮询bug时  num返回就是0 ,常规情况下它是会阻塞直至有io事件发生的
           //空轮询bug就是快速响应,然后一直调用select,导致cpu被打满,没时间处理其他的任务了
            int num = selector.select();
            if(num == 0){
                continue;
            }
            System.out.println(num);
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()){
                SelectionKey next = iterator.next();
                if(next.isAcceptable()){
                    ServerSocketChannel channel = (ServerSocketChannel) next.channel();
                    SocketChannel accept = channel.accept();
                    accept.configureBlocking(false);
                    //注册到selector上 感兴趣的操作是读
                    accept.register(selector,SelectionKey.OP_READ);
                }
                //ServerSocketChannel SocketChannel都注册到了一个Selector上 所有会有多种Ops
                if(next.isReadable()){
                    SocketChannel channel = (SocketChannel) next.channel();
                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                    while(channel.read(byteBuffer) > 0){
                        byteBuffer.flip();
                        while (byteBuffer.hasRemaining()){
                            channel.write(byteBuffer);
                        }
                        byteBuffer.clear();
                    }
                    channel.close();
                }
                //selectedKeys 需要手动移除
                iterator.remove();
            }
        }
    }
}

此bug可能存在的场景

1.服务端等待连接
2.客户端发起连接,发送消息
3.服务端接受连接,并注册监听通道的OP_READ
4.服务端读取消息,从感兴趣事件集合中移除OP_READ
5.客户端关闭连接
6.服务端给客户端发送消息
7.服务端select方法不再阻塞,无限被唤醒并且返回值为0.

可能的解释

问题产生于linux的epoll。如果一个socket文件描述符,注册的事件集合码为0,然后连接突然被对端中断,那么epoll会被POLLHUP或者有可能是POLLERR事件给唤醒,并返回到事件集中去。这意味着,Selector会被唤醒,即使对应的channel兴趣事件集是0,并且返回的events事件集合也是0

可解决的方案

1.取消对应的key,立即调用selector.selectNow刷新一次selector。

2.如果注册到selector兴趣事件集为0,则直接取消注册。 如果注册到selector兴趣事件集不为0,则需要将linux epoll事件POLLHUP/POLLERR转化为OP_READ 或者OP_WRITE。。

(netty采用的方案)
3.重新new一个selector 。
重新构造selector,需要重新注册channnel到其上,并注册感兴趣事件,重新注册的过程中有机会检测channel的可用性

总结: 可以看到select操作根据taskQueue普通任务队列是否有任务进行非阻塞/阻塞调用。同时还会根据定时任务队列最近到期的任务时间来决定阻塞时间。阻塞期间,外部线程添加任务时可唤醒阻塞状态。netty还根据计数空轮询次数,如果超过512次就重建selector来修复空轮询bug。

2.处理io事件 processSelectedKeys

NioEventLoop#processSelectedKeys

private void processSelectedKeys() {
    if (selectedKeys != null) {
        //前文讲创建NioEventloop时 openSelector netty有调整selectedKeys的数据结构,
        //并将SelectedSelectionKeySet引用指向到了NioEventloop,
        //这里只要selectedKeys不为空,那就是进入这个分支了,获取所有就绪的通道进行处理
        processSelectedKeysOptimized(selectedKeys.flip());
    } else {
        processSelectedKeysPlain(selector.selectedKeys());
    }
}

NioEventLoop#processSelectedKeysOptimized

private void processSelectedKeysOptimized(SelectionKey[] selectedKeys) {
    for (int i = 0;; i ++) {
        final SelectionKey k = selectedKeys[i];
        if (k == null) {
            break;
        }
        //清除引用 防止内存泄露  SelectionKey的attachement关联channel,
          //channel消亡时所有的引用都应清除,不然无法回收
          //jdk selectedKeys是set结构,netty改造成了数组,添加/遍历更快,remove相当于空方法,这里需要手动移除
        selectedKeys[i] = null;
        //获取io事件对应的channel
        final Object a = k.attachment(); 
        if (a instanceof AbstractNioChannel) {
            //处理通道
            processSelectedKey(k, (AbstractNioChannel) a);
        } else {
           //attachment除了可能为channel外 还有可能为NioTask  基本没有使用
            NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
            processSelectedKey(k, task);
        }
        //是否需要再次轮询
        if (needsToSelectAgain) { 
           //每256个连接断开 清理下selectedKeys。 因为SelectedSelectionKeySet重写了remove方法,
           //remove方法并不会对数组进行改变,因为需要依赖null进行边界判定,且数组频繁重排也会影响影响性能,
         //而当连接取消,调用selectionKey.cancel 加入到cancelledKeys时。
         //jdk的selector在select前后都会校验cancelledKeys中是否有值,
      //如果有,调用selectedKeys的remove方法进行移除。 这里没有移除,用的是上方    selectedKeys[i] = null; 
           //下方2.1中   if (!k.isValid())  判断,以及此处的批量移除来防止内存泄漏和性能优化。
            for (;;) {
                i++;
                //到达边界
                if (selectedKeys[i] == null) {
                    break;
                }
                selectedKeys[i] = null;
            }
            //将needsToSelectAgain 置为false,调用selectNow
            selectAgain();
            selectedKeys = this.selectedKeys.flip();
            //重置下标 重新开始遍历
            i = -1;
        }
    }
}

2.1 NioEventLoop#processSelectedKey

private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
    final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe();
   final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe();
    if (!k.isValid()) {
            try {
                    eventLoop = ch.eventLoop();
            } catch (Throwable ignored) {
                    return;
            }
            if (eventLoop != this || eventLoop == null) {
                    return;
            }
           //关闭通道 这个key已经无效了
            unsafe.close(unsafe.voidPromise());
            return;
    }
    try {
        int readyOps = k.readyOps();
        if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
            int ops = k.interestOps();
            ops &= ~SelectionKey.OP_CONNECT;
            k.interestOps(ops);

            unsafe.finishConnect();
        }
        if ((readyOps & SelectionKey.OP_WRITE) != 0) {
            ch.unsafe().forceFlush();
        }
        if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
            //有连接进入 2.1.1 处理连接请求  主从reactor线程其实都是NioEventloop,
            //所以他们的区别实质是接下来的维持的unsafe实例的区别
            unsafe.read();
            if (!ch.isOpen()) {
                return;
            }
        }
    } catch (CancelledKeyException ignored) {
        unsafe.close(unsafe.voidPromise());
    }
}

2.1.1 处理连接请求 boss read

这里称之为boss,因为是AbstractNioMessageChannel(NioServerSocketChannel),专门负责请求分发 AbstractNioMessageChannel.NioMessageUnsafe#read

public void read() {
        assert eventLoop().inEventLoop();
        final ChannelConfig config = config();
        final ChannelPipeline pipeline = pipeline();
        final RecvByteBufAllocator.Handle allocHandle = unsafe().recvBufAllocHandle();
        allocHandle.reset(config);

        boolean closed = false;
        Throwable exception = null;
        try {
           ...
            //readBuf 是一个 List<Object> 结构 保留下面创建的NioSocketChannel的引用
            //2.1.1.1 接收连接信息 创建NioSocketChannel
            doReadMessages(readBuf);
           ...
            int size = readBuf.size();
            //可能多个连接同时接入
            for (int i = 0; i < size; i ++) {
                readPending = false;
              //将NioSocketChannel作为msg传入,从head节点开始 挨个调用所有ChannelInboundHandler
              //的channelRead方法 初始化 NioSocketChannel并注册其相关的SocketChannel(jdk)
              //其中关键性的一个节点ServerBootstrapAcceptor下方有介绍
                pipeline.fireChannelRead(readBuf.get(i));
            }
            readBuf.clear();
            allocHandle.readComplete();
            pipeline.fireChannelReadComplete();
            if (exception != null) {
                closed = closeOnReadError(exception);
                pipeline.fireExceptionCaught(exception);
            }
            if (closed) {
                inputShutdown = true;
                if (isOpen()) {
                    close(voidPromise());
                }
            }
        } finally {
            if (!readPending && !config.isAutoRead()) {
                removeReadOp();
            }
        }
    }
}
2.1.1.1 接收连接信息 创建NioSocketChannel
protected int doReadMessages(List<Object> buf) throws Exception {
   //调用jdk方法 创建 SocketChannel
    SocketChannel ch = javaChannel().accept();
    ...
    //创建NioSocketChannel
    buf.add(new NioSocketChannel(this, ch));
    ...
}


 public NioSocketChannel(Channel parent, SocketChannel socket) {
    //类图中有看过NioSocketChannel 和 NioServerSocketChannel的关系,他们都继承自AbstractNioChannel
    //除了下面 传入的父类 channel(jdk) 和关心的操作不一样 后面的逻辑都是一样的
    //  super(parent, ch, SelectionKey.OP_READ);
    //也就是获取唯一编号id,实例化unsafe,实例化pipeline ,保存socketChannel(jdk)引用,同时将socketChannel(jdk)设置为非阻塞
    super(parent, socket);
    config = new NioSocketChannelConfig(this, socket.socket());
}

2.1.2 ServerBootstrapAcceptor

在前文 服务端启动一章中 讲到 ServerBootstrap#init 有一段这样的代码 将我们自定义的handler 以及 ServerBootstrapAcceptor加入到 pipeline中

 p.addLast(new ChannelInitializer<Channel>() {
    @Override
    public void initChannel(Channel ch) throws Exception {
        final ChannelPipeline pipeline = ch.pipeline();
        //config.handler() 其实就是我们传给ServerBootStrap的自定义handler
        ChannelHandler handler = config.handler();
        if (handler != null) {
//加入我们自定义的childHandler,异步任务,会在注册完成后 pipeline.invokeHandlerAddedIfNeeded();完成回调加入
            pipeline.addLast(handler);
        }
        ch.eventLoop().execute(new Runnable() {
            @Override
            public void run() {
                //添加一个接入器 用于接收新请求
                pipeline.addLast(new ServerBootstrapAcceptor(
                        currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
            }
        });
    }
});
private static class ServerBootstrapAcceptor extends ChannelInboundHandlerAdapter {
   //这些都是构造方法直接传入 我们声明的配置信息
    private final EventLoopGroup childGroup;
    private final ChannelHandler childHandler;
    private final Entry<ChannelOption<?>, Object>[] childOptions;
    private final Entry<AttributeKey<?>, Object>[] childAttrs;

    @Override
    @SuppressWarnings("unchecked")
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
       //这里直接将msg 强转为 channel(netty)  这里是NioSocketChannel
        final Channel child = (Channel) msg;
        //NioSocketChannel的pipeline加入我们自定义的handler
        child.pipeline().addLast(childHandler);
       ...设置 option attr 配置...
       //下面的操作和服务端启动时注册时一样的逻辑 这里回顾下
       //调用线程选择器获取获取EventExecutor[]数组中的一个也就是NioEventLoop 来执行register操作
       //register是一个异步任务,NioEventLoop的首次任务会先激活线程,调用DefaultThreadFactory创建一个线程,
       //并指向给NioEventLoop.thread,所以说NioEventLoop就是一个工作线程(但只有有任务进来才会激活线程),
       //线程开始执行NioEventLoop#run方法
       //register任务会调用jdk方法 将SocketChannel(jdk) 注册到selector上获取selectionKey并保留,
       //invokeHandlerAddedIfNeeded回调 往pipeline中加入我们自定义的childHandler,当然最后还是会移除,因为它是ChannelInitializer,
       //再之后就是回调各个handler的channelRegistered方法了。与服务端注册不同的点是,
       //服务端注册需要在bind操作完成后才会回调channelActive事件,
       //这里的channelActive判断依据是SocketChannel(jdk)保持连接就通过,所以会立即回调,
       //其中会完成将刚兴趣的操作SelectionKey.OP_READ绑定。
       //至此NioSocketChannel就绪,可以监听read事件了 和客户端进行交互了。
        childGroup.register(child).addListener(....)
    }
}

可以看到主reactor线程监听到accept事件后,调用 AbstractNioMessageChannel.NioMessageUnsafe#read方法,其中会调用jdk方法 创建 SocketChannel,并创建NioSocketChannel。之后pipeline传递channelRead事件,ServerBootstrapAcceptor为channel设置options和attr属性,同时从NioEventloopGroup(subreactor线程池)中分配一个NioEventloop与channel绑定。NioEventloop将channel注册到自己维护的selector上,同时监听读事件。后续此连接的读写都由NioEventloop负责。一个NioEventloop可以同时管理多个channel。这也就是io多路复用,充分利用cpu效能。

2.1.3处理请求 worker read

这里是AbstractNioByteChannel,其子类NioSocketChannel,负责处理请求,所以叫worker AbstractNioByteChannel.NioByteUnsafe#read

public final void read() {
        final ChannelConfig config = config();
        final ChannelPipeline pipeline = pipeline();
        final ByteBufAllocator allocator = config.getAllocator();
        final RecvByteBufAllocator.Handle allocHandle = recvBufAllocHandle();
        allocHandle.reset(config);

        ByteBuf byteBuf = null;
        boolean close = false;
        try {
            do {
                //分配ByteBuf
                byteBuf = allocHandle.allocate(allocator);
                //基于SocketChannel(jdk)读取数据到ByteBuf
                allocHandle.lastBytesRead(doReadBytes(byteBuf));
                ...
                allocHandle.incMessagesRead(1);
                readPending = false;
                //传递channelRead事件 从head节点开始,挨个ChannelInboundHandler调用
                pipeline.fireChannelRead(byteBuf);
                byteBuf = null;
            } while (allocHandle.continueReading());

            allocHandle.readComplete();
            //传递 channelReadComplete事件 从head节点开始,挨个ChannelInboundHandler调用
            pipeline.fireChannelReadComplete();

            if (close) {
                closeOnRead(pipeline);
            }
        } catch (Throwable t) {
        ...
    }
}

subReactor线程监听到读事件后,基于SocketChannel(jdk)读取数据到ByteBuf,调用pipeline传递各类事件。

3.处理任务队列

SingleThreadEventExecutor#runAllTasks(long)

 //timeoutNanos 最多执行多长时间 默认和处理io事件时间相等。
protected boolean runAllTasks(long timeoutNanos) {
//从scheduledTaskQueue队列中取出时间到了的任务 添加到taskQueue中,如果加不进去,再回到scheduledTaskQueue中
    fetchFromScheduledTaskQueue();
    //从taskQueue取出任务
    Runnable task = pollTask();
    if (task == null) {
        afterRunningAllTasks();
        return false;
    }
    //截止时间
    final long deadline = ScheduledFutureTask.nanoTime() + timeoutNanos;
    long runTasks = 0;
    long lastExecutionTime;
    for (;;) {
        //try catch  task.run() 防止影响其他任务执行
        safeExecute(task);

        runTasks ++;
        //每64次任务检查一次 看看是不是 当前时间已经超过截止时间了
        if ((runTasks & 0x3F) == 0) {
            lastExecutionTime = ScheduledFutureTask.nanoTime();
            if (lastExecutionTime >= deadline) {
                break;
            }
        }
        task = pollTask();
        if (task == null) {
            lastExecutionTime = ScheduledFutureTask.nanoTime();
            break;
        }
    }
    //处理尾部队列 tailTasks中的任务,hasTasks()方法有提到这个队列。
    // tailTasks 相比于普通任务队列优先级较低,可以理解为是收尾任务
    // 尾部队列并不常用,如果想对 Netty 的运行状态做一些统计数据,例如任务循环的耗时、占用物理内存的大小等等,都可以向尾部队列添加一个收尾任务完成统计数据的实时更新
    afterRunningAllTasks();
    this.lastExecutionTime = lastExecutionTime;
    return true;
}