天天看點

揭秘ServerBootstrap神秘面紗(服務端ServerBootstrap)課前準備一、ServerBootstrap驚鴻一瞥二、NioServerSocketChannel的建立三、服務端Channel(NioServerSocketChannel)的初始化四、ChannelPipeline初始化五、服務端注冊到Selector六、bossGroup與workGroup七、handler 的添加過程八、服務端Selector事件輪詢八、Netty解決JDK空輪詢bug

課前準備

我在讀源碼的過程中,有些問題會經常浮現在腦海裡。根本原因是自己對于Netty的元件和運作過程不熟悉,是以我把這些問題貼在這裡:

  1. 服務端openServerSocketChannel()和用戶端openSocketChannel()的差別?
  2. 服務端newSafe()和用戶端newSafe()有什麼不一樣?
  3. Channel的傳入參數是OP_ACCEPT,用戶端是OP_READ。
  4. EventLoopGroup和EventLoop之間的關系是什麼?
  5. 到底什麼是多路複用?
  6. reactor,redis 是reactor單線程模式?
  7. reactor模式有三種,我們現在的demo用的是主從模式,還是多線程
  8. bossGroup和workGroup分别是在哪關聯的
  9. ServerBootstrapAcceptor的作用?
  10. childLoop如何綁定請求過來的channel
  11. 我們知道selector輪詢服務端是輪詢的,用戶端輪詢嗎?
  12. 服務端特有參數:

    childHandler / childOption / childAttr 方法(隻有服務端ServerBootstrap才有child類型的方法)。

    ——對于服務端而言,有兩種通道需要處理, 一種是ServerSocketChannel:用于處理使用者連接配接的accept操作, 另一種是SocketChannel,表示對應用戶端連接配接。而對于用戶端,一般都隻有一種channel,也就是SocketChannel。用戶端和服務端使用的都是NioEventLoop嗎?服務端workGroup中的EventLoop是可以綁定用戶端來的Channel,那用戶端的EventLoop是用來幹啥的?

  13. 三種reactor模型的差別:

    單線程:acceptor和nio共用一個線程

    多線程:acceptor是單個線程,nio是一個線程池

    主從:acceptor是線程池,nio也是線程池

  14. selector的for循環部分,是我們所說的select、epoll中的select模式的代碼實作嗎?

一、ServerBootstrap驚鴻一瞥

在用戶端的代碼中,我們對Bootstrap有了一個基本的了解,接下來我們來分析ServerBootstrap。ServerBootstrap和Bootstrap有很多地方是相同的,但我們要尤其注意兩者不同的地方。首先來看看服務端的啟動代碼:

public class RpcRegistry {
    private int port;
    public RpcRegistry(int port) {
        this.port = port;
    }


    private void start() {
        //1.建立對象
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            //2.配置參數
            ServerBootstrap server = new ServerBootstrap();
            server.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer() {
                        @Override
                        protected void initChannel(Channel ch) throws Exception {
                            //接收課用戶端請求的處理流程
                            ChannelPipeline pipeline = ch.pipeline();

                            int fieldLength = 4;
                            //通用解碼器設定
                            pipeline.addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE,0,fieldLength,0,fieldLength));
                            //通用編碼器
                            pipeline.addLast(new LengthFieldPrepender(fieldLength));
                            //對象編碼器
                            pipeline.addLast("encoder",new ObjectEncoder());
                            //對象解碼器
                            pipeline.addLast("decoder",new ObjectDecoder(Integer.MAX_VALUE, ClassResolvers.cacheDisabled(null)));

                            pipeline.addLast(new RegistryHandler());
                        }
                    })
                    .option(ChannelOption.SO_BACKLOG, 128)
                    .childOption(ChannelOption.SO_KEEPALIVE, true);
            //3.啟動
            ChannelFuture future = server.bind(this.port).sync();
            System.out.println("GP RPC registry is start,listen at " + this.port);
            future.channel().closeFuture().sync();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

    public static void main(String[] args) {
        new RpcRegistry(8080).start();
    }

}
           

和用戶端的啟動代碼相比, 差別不大, 基本上也是進行了如下幾個部分的初始化:

  1. EventLoopGroup: 不論是伺服器端還是用戶端, 都必須指定 EventLoopGroup. 在這個例子中, 指定了 NioEventLoopGroup, 表示一個 NIO 的EventLoopGroup, 不過伺服器端需要指定兩個 EventLoopGroup, 一個是 bossGroup, 用于處理用戶端的連接配接請求; 另一個是 workerGroup, 用于處理與各個用戶端連接配接的 IO 操作.
  2. ChannelType: 指定 Channel 的類型. 因為是伺服器端, 是以使用了 NioServerSocketChannel.
  3. Handler: 設定資料的處理器。

二、NioServerSocketChannel的建立

我們在分析用戶端的 Channel 初始化過程時, 已經提到, Channel 是對 Java 底層 Socket 連接配接的抽象, 并且知道了用戶端的 Channel 的具體類型是 NioSocketChannel, 那麼自然的, 伺服器端的 Channel 類型就是 NioServerSocketChannel 了。接下來我們分析一下服務端啟動代碼,順便對比一下伺服器端和用戶端有哪些不一樣的地方。

 我們已經知道了, 在用戶端中, Channel 的類型其實是在初始化時, 通過 Bootstrap.channel() 方法設定的, 伺服器端自然也不例外.在伺服器端, 我們調用了 ServerBootstarap.channel(NioServerSocketChannel.class), 傳遞了一個 NioServerSocketChannel Class 對象. 這樣的話, 按照和分析用戶端代碼一樣的流程, 我們就可以确定, NioServerSocketChannel 的執行個體化是通過 BootstrapChannelFactory 工廠類來完成的, 而 BootstrapChannelFactory 中的 clazz 字段被設定為了 NioServerSocketChannel.class, 是以當調用 BootstrapChannelFactory.newChannel() 時:

@Override
    public T newChannel() {
        try {
            return clazz.newInstance();
        } catch (Throwable t) {
            throw new ChannelException("Unable to create Channel from class " + clazz, t);
        }
    }
           

我們來總結一下:

  1.  ServerBoostrap的ChannelFactory的實作類是ReflectiveChannelFactory類。
  2. 建立的Channel的具體類型是NioServerSocketChannel。

和用戶端的代碼一樣,Channel的執行個體過程其實就是調用ChannelFactory.newChannel()方法,而執行個體化的具體類型就是初始化過程中我們傳給channel()方法的實參。是以上面服務端執行個體化出來的Channel類型是NioServerSocketChannel執行個體。具體過程可以參考前面一篇用戶端代碼分析。

三、服務端Channel(NioServerSocketChannel)的初始化

在分析NioServerSocketChannel初始化之前,先看下NioServerSocketChannel的類圖:

揭秘ServerBootstrap神秘面紗(服務端ServerBootstrap)課前準備一、ServerBootstrap驚鴻一瞥二、NioServerSocketChannel的建立三、服務端Channel(NioServerSocketChannel)的初始化四、ChannelPipeline初始化五、服務端注冊到Selector六、bossGroup與workGroup七、handler 的添加過程八、服務端Selector事件輪詢八、Netty解決JDK空輪詢bug

首先,我們在追蹤NioServerSocketChannel的預設構造,和NioSocketChannel類似,構造器都是構造newSocket()來打開一個Java的NIO Socket。不過不同的是:用戶端的newSocket()調用的是openSocketChannel(),而服務端的newSocket()調用的是openServerSocketChannel()。我們來看看代碼:

揭秘ServerBootstrap神秘面紗(服務端ServerBootstrap)課前準備一、ServerBootstrap驚鴻一瞥二、NioServerSocketChannel的建立三、服務端Channel(NioServerSocketChannel)的初始化四、ChannelPipeline初始化五、服務端注冊到Selector六、bossGroup與workGroup七、handler 的添加過程八、服務端Selector事件輪詢八、Netty解決JDK空輪詢bug
揭秘ServerBootstrap神秘面紗(服務端ServerBootstrap)課前準備一、ServerBootstrap驚鴻一瞥二、NioServerSocketChannel的建立三、服務端Channel(NioServerSocketChannel)的初始化四、ChannelPipeline初始化五、服務端注冊到Selector六、bossGroup與workGroup七、handler 的添加過程八、服務端Selector事件輪詢八、Netty解決JDK空輪詢bug

接着調用調用構造方法的重載方法:

揭秘ServerBootstrap神秘面紗(服務端ServerBootstrap)課前準備一、ServerBootstrap驚鴻一瞥二、NioServerSocketChannel的建立三、服務端Channel(NioServerSocketChannel)的初始化四、ChannelPipeline初始化五、服務端注冊到Selector六、bossGroup與workGroup七、handler 的添加過程八、服務端Selector事件輪詢八、Netty解決JDK空輪詢bug

我們可以看到在上述方法中,傳入的參數是SelectionKey.OP_ACCEPT。還記得用戶端傳的是什麼嗎?——是SelectionKey.OP_READ。在服務啟動後需要監聽用戶端連接配接請求,是以在這裡我們設定SelectionKey.OP_ACCEPT,也就是通知selector我們對用戶端的連接配接請求感興趣。

接着和用戶端的分析一下, 會逐級地調用父類的構造器 NioServerSocketChannel <- AbstractNioMessageChannel <- AbstractNioChannel <- AbstractChannel.同樣的, 在 AbstractChannel 中會執行個體化一個 unsafe 和 pipeline:

protected AbstractChannel(Channel parent) {
        this.parent = parent;
        id = newId();
        unsafe = newUnsafe();
        pipeline = newChannelPipeline();
    }
           

不過, 這裡有一點需要注意的是, 用戶端的 unsafe 是一個 AbstractNioByteChannel#NioByteUnsafe 的執行個體, 而在伺服器端時, 因為 AbstractNioMessageChannel 重寫了newUnsafe 方法:

@Override
    protected AbstractNioUnsafe newUnsafe() {
        return new NioMessageUnsafe();
    }
           

 是以在伺服器端, unsafe 字段其實是一個 AbstractNioMessageChannel#AbstractNioUnsafe 的執行個體。

最後總結一下,在NioServerSocketChannel執行個體化過程中的執行邏輯:

  1. 調用 NioServerSocketChannel.newSocket(DEFAULT_SELECTOR_PROVIDER) 打開一個新的 Java NIO ServerSocketChannel
  2. AbstractChannel(Channel parent) 中初始化 AbstractChannel 的屬性:

    parent:屬性置為 null

    unsafe:通過newUnsafe() 執行個體化一個 unsafe 對象, 它的類型是 AbstractNioMessageChannel#AbstractNioUnsafe 内部類

  3. AbstractNioChannel 中的屬性:

    ch:指派為Java NIO的ServerSocketChannel,調用NioServerSocketChannel的newSocket()方法擷取。

    readInterestOp:預設指派為SelectionKey.OP_ACCEPT。

    ch設定為非阻塞,調用ch.configureBlocking(false)方法。

  4. NioSeverSocketChannel中被指派的屬性:

    ServerSocketChannelConfig config = new NioServerSocketChannelConfig(this, javaChannel().socket())

四、ChannelPipeline初始化

伺服器端和用戶端的 ChannelPipeline 的初始化一緻, 此處不作單獨分析了。

五、服務端注冊到Selector

伺服器端和用戶端的 Channel 的注冊過程一緻, 此處不作單獨分析了。

六、bossGroup與workGroup

在用戶端的時候,我們隻初始化了一個EventLoopGroup 對象,但是在服務端,我們設定了兩個EventLoopGroup對象,一個bossGroup,一個workrGroup。他們兩個的作用分别是什麼呢?我們接下來分析一下。

bossGroup隻用于服務端的accept,也就是用于處理用戶端的連接配接請求。下面我們看一下bossGroup和workerGroup之間的關系,如下圖:

揭秘ServerBootstrap神秘面紗(服務端ServerBootstrap)課前準備一、ServerBootstrap驚鴻一瞥二、NioServerSocketChannel的建立三、服務端Channel(NioServerSocketChannel)的初始化四、ChannelPipeline初始化五、服務端注冊到Selector六、bossGroup與workGroup七、handler 的添加過程八、服務端Selector事件輪詢八、Netty解決JDK空輪詢bug

首先, 伺服器端 bossGroup 不斷地監聽是否有用戶端的連接配接, 當發現有一個新的用戶端連接配接到來時, bossGroup 就會為此連接配接初始化各項資源, 然後從 workerGroup 中選出一個 EventLoop 綁定到此用戶端連接配接中. 那麼接下來的伺服器與用戶端的互動過程就全部在此配置設定的 EventLoop 中了。接下來看源碼。

首先在ServerBootstrap 初始化時, 調用了 b.group(bossGroup, workerGroup) 設定了兩個 EventLoopGroup, 我們跟蹤進去看一下:

揭秘ServerBootstrap神秘面紗(服務端ServerBootstrap)課前準備一、ServerBootstrap驚鴻一瞥二、NioServerSocketChannel的建立三、服務端Channel(NioServerSocketChannel)的初始化四、ChannelPipeline初始化五、服務端注冊到Selector六、bossGroup與workGroup七、handler 的添加過程八、服務端Selector事件輪詢八、Netty解決JDK空輪詢bug
揭秘ServerBootstrap神秘面紗(服務端ServerBootstrap)課前準備一、ServerBootstrap驚鴻一瞥二、NioServerSocketChannel的建立三、服務端Channel(NioServerSocketChannel)的初始化四、ChannelPipeline初始化五、服務端注冊到Selector六、bossGroup與workGroup七、handler 的添加過程八、服務端Selector事件輪詢八、Netty解決JDK空輪詢bug

 顯然, 這個方法初始化了兩個字段, 一個是 group = parentGroup, 它是在 super.group(parentGroup) 也就是上圖中AbstractBootstrap中初始化的, 另一個是childGroup = childGroup。接着從應用程式的啟動代碼來看,調用了 b.bind()方法來監聽一個本地端口。bind()方法會觸發如下的調用鍊:

AbstractBootstrap.bind -> AbstractBootstrap.doBind -> AbstractBootstrap.initAndRegister

 AbstractBootstrap#initAndRegister方法我們已經很熟悉了,在分析用戶端代碼的時候就跟他打過交道,現在再回顧下吧:

final ChannelFuture initAndRegister() {
    final Channel channel = channelFactory().newChannel();
    ... 省略異常判斷
    init(channel);
    ChannelFuture regFuture = group().register(channel);
    return regFuture;
}
           

這裡 group() 方法傳回的是上面我們提到的 bossGroup, 而這裡的 channel 我們也已經分析過了, 它是一個NioServerSocketChannel 執行個體, 是以我們可以知道,group().register(channel) 将 bossGroup 和 NioServerSocketChannsl 關聯起來了.

那麼 workerGroup 是在哪裡與 NioSocketChannel 關聯的呢?

我們繼續看 init(channel) 方法: 

@Override
    void init(Channel channel) throws Exception {
        final Map<ChannelOption<?>, Object> options = options0();
        synchronized (options) {
            channel.config().setOptions(options);
        }

        final Map<AttributeKey<?>, Object> attrs = attrs0();
        synchronized (attrs) {
            for (Entry<AttributeKey<?>, Object> e: attrs.entrySet()) {
                @SuppressWarnings("unchecked")
                AttributeKey<Object> key = (AttributeKey<Object>) e.getKey();
                channel.attr(key).set(e.getValue());
            }
        }

        ChannelPipeline p = channel.pipeline();

        final EventLoopGroup currentChildGroup = childGroup;
        final ChannelHandler currentChildHandler = childHandler;
        final Entry<ChannelOption<?>, Object>[] currentChildOptions;
        final Entry<AttributeKey<?>, Object>[] currentChildAttrs;
        synchronized (childOptions) {
            currentChildOptions = childOptions.entrySet().toArray(newOptionArray(childOptions.size()));
        }
        synchronized (childAttrs) {
            currentChildAttrs = childAttrs.entrySet().toArray(newAttrArray(childAttrs.size()));
        }

        p.addLast(new ChannelInitializer<Channel>() {
            @Override
            public void initChannel(Channel ch) throws Exception {
                final ChannelPipeline pipeline = ch.pipeline();
                ChannelHandler handler = config.handler();
                if (handler != null) {
                    pipeline.addLast(handler);
                }
                ch.eventLoop().execute(new Runnable() {
                    @Override
                    public void run() {
                        pipeline.addLast(new ServerBootstrapAcceptor(
                                currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
                    }
                });
            }
        });
    }
           

init()方法被ServerBootstrap重寫了,在上面代碼中,我們需要關注一個點——ServerBootstrapAcceptor。

上面代碼為pipeline中添加了一個ChannelInitializer,而這個ChannelInitializer中添加了一個非常關鍵ServerBootstrapAcceptor的handler。現在我們關注一下ServerBootstrapAcceptor類。在ServerBootstrapAcceptor中重寫了channelRead()方法,其主要代碼如下:

揭秘ServerBootstrap神秘面紗(服務端ServerBootstrap)課前準備一、ServerBootstrap驚鴻一瞥二、NioServerSocketChannel的建立三、服務端Channel(NioServerSocketChannel)的初始化四、ChannelPipeline初始化五、服務端注冊到Selector六、bossGroup與workGroup七、handler 的添加過程八、服務端Selector事件輪詢八、Netty解決JDK空輪詢bug

ServerBootstrapAcceptor 中的 childGroup就是我們的workerGroup, 而 Channel 是一個 NioSocketChannel 的執行個體, 是以這裡的 childGroup.register 就是将 workerGroup 中的摸個 EventLoop 和 NioSocketChannel 關聯了。既然這樣, 那麼現在的問題是,ServerBootstrapAcceptor.channelRead()方法是怎麼被調用的呢? 其實當一個 client 連接配接到 server 時, Java 底層的 NIO ServerSocketChannel 會有一個 SelectionKey.OP_ACCEPT 就緒事件, 接着就會調用到 NioServerSocketChannel.doReadMessages()方法:

揭秘ServerBootstrap神秘面紗(服務端ServerBootstrap)課前準備一、ServerBootstrap驚鴻一瞥二、NioServerSocketChannel的建立三、服務端Channel(NioServerSocketChannel)的初始化四、ChannelPipeline初始化五、服務端注冊到Selector六、bossGroup與workGroup七、handler 的添加過程八、服務端Selector事件輪詢八、Netty解決JDK空輪詢bug

在 doReadMessages 中, 通過 javaChannel().accept() 擷取到用戶端新連接配接的 SocketChannel, 接着就執行個體化一個NioSocketChannel, 并且傳入 NioServerSocketChannel 對象(即 this), 由此可知, 我們建立的這個 NioSocketChannel 的父 Channel 就是 NioServerSocketChannel 執行個體 。

接下來就經由 Netty 的 ChannelPipeline 機制,将讀取事件逐級發送到各個 handler 中, 于是就會觸發前面我們提到的 ServerBootstrapAcceptor.channelRead()方法。

七、handler 的添加過程

用戶端handler我們知道是在初始化的時候通過啟動代碼的.handler()添加的。但是服務端有兩個group,那麼我們就要弄清楚.handler()是設定bossGroup的還是workerGroup的。

一個是通過 handler() 方法設定 handler 字段, 另一個是通過 childHandler() 設定 childHandler 字段. 通過前面的 bossGroup 和 workerGroup 的分析, 其實我們在這裡可以大膽地猜測: handler 字段與 accept 過程有關, 即這個 handler 負責處理用戶端的連接配接請求; 而 childHandler 就是負責和用戶端的連接配接的 IO 互動。實際就是這樣的,我們後續的篇章中會講解到。在我們的demo示例中:隻有childHandler() 方法,如果要對bossGroup添加handler,我們可以使用.handler()方式在啟動代碼處添加。

八、服務端Selector事件輪詢

再回到ServerBootstrap的啟動代碼,是從bind()方法開始的。ServerBootstrap的bind()方法實際上就是其父類AbstractBootstrap的bind()方法,來看代碼:

private static void doBind0(
            final ChannelFuture regFuture, final Channel channel,
            final SocketAddress localAddress, final ChannelPromise promise) {

        // This method is invoked before channelRegistered() is triggered.  Give user handlers a chance to set up
        // the pipeline in its channelRegistered() implementation.
        channel.eventLoop().execute(new Runnable() {
            @Override
            public void run() {
                if (regFuture.isSuccess()) {
                    channel.bind(localAddress, promise).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
                } else {
                    promise.setFailure(regFuture.cause());
                }
            }
        });
    }
           

 在doBind0()方法中,調用的是EventLoop的execute()方法,繼續跟進:

揭秘ServerBootstrap神秘面紗(服務端ServerBootstrap)課前準備一、ServerBootstrap驚鴻一瞥二、NioServerSocketChannel的建立三、服務端Channel(NioServerSocketChannel)的初始化四、ChannelPipeline初始化五、服務端注冊到Selector六、bossGroup與workGroup七、handler 的添加過程八、服務端Selector事件輪詢八、Netty解決JDK空輪詢bug

在execute()主要是建立線程,将線程添加到EventLoop的無鎖化串行任務隊列。我們重點關注startThread()方法,繼續看源碼:

揭秘ServerBootstrap神秘面紗(服務端ServerBootstrap)課前準備一、ServerBootstrap驚鴻一瞥二、NioServerSocketChannel的建立三、服務端Channel(NioServerSocketChannel)的初始化四、ChannelPipeline初始化五、服務端注冊到Selector六、bossGroup與workGroup七、handler 的添加過程八、服務端Selector事件輪詢八、Netty解決JDK空輪詢bug

我們發現startThread()最終調用的是SingleThreadEventExecutor.this.run()方法,這個this就是NioEventLoop對象:

@Override
    protected void run() {
        for (;;) {
            try {
                switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) {
                    case SelectStrategy.CONTINUE:
                        continue;
                    case SelectStrategy.SELECT:
                        select(wakenUp.getAndSet(false));
                        //select的喚醒邏輯
                        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);
            }
        }
    }
           

終于看到了似曾相識的代碼, 上面的代碼主要是用一個死循環不斷的輪詢SelectionKey。select()方法主要用來解決JDK空輪詢bug,而processSelectedKeys()就是針對不同的輪詢事件進行處理。如果用戶端有資料寫入,最終也會調用AbstractNioMessageChannel的doReadMessage()方法。總結一下:

  1. Netty的select事件輪詢是從EventLoop的execute()方法開始的。
  2. 在EventLoop的execute()方法中,會為每一個事件建立一個獨立的線程,并儲存到無鎖化串行任務隊列。
  3. 線程任務隊列的每個任務實際調用的是NioEventLoop的run()方法。
  4. 在run()方法中調用processSelectKeys()處理輪詢事件。

八、Netty解決JDK空輪詢bug

8.1 jdk空輪詢bug表現及原因:

bug表現:

揭秘ServerBootstrap神秘面紗(服務端ServerBootstrap)課前準備一、ServerBootstrap驚鴻一瞥二、NioServerSocketChannel的建立三、服務端Channel(NioServerSocketChannel)的初始化四、ChannelPipeline初始化五、服務端注冊到Selector六、bossGroup與workGroup七、handler 的添加過程八、服務端Selector事件輪詢八、Netty解決JDK空輪詢bug

epoll bug

  • 正常情況下,

    selector.select()

    操作是阻塞的,隻有被監聽的fd有讀寫操作時,才被喚醒
  • 但是,在這個bug中,沒有任何fd有讀寫請求,但是

    select()

    操作依舊被喚醒
  • 很顯然,這種情況下,

    selectedKeys()

    傳回的是個空數組
  • 然後按照邏輯執行到

    while(true)

    處,循環執行,導緻死循環。

bug原因:

JDK bug清單中有兩個相關的bug報告:

  1. JDK-6670302 : (se) NIO selector wakes up with 0 selected keys infinitely
  2. JDK-6403933 : (se) Selector doesn't block on Selector.select(timeout) (lnx)

JDK-6403933的bug說出了實質的原因:

This is an issue with poll (and epoll) on Linux. If a file descriptor for a connected socket is polled with a request event mask of 0, and if the connection is abruptly terminated (RST) then the poll wakes up with the POLLHUP (and maybe POLLERR) bit set in the returned event set. The implication of this behaviour is that Selector will wakeup and as the interest set for the SocketChannel is 0 it means there aren't any selected events and the select method returns 0.

具體解釋為:在部分Linux的2.6的kernel中,poll和epoll對于突然中斷的連接配接socket會對傳回的eventSet事件集合置為POLLHUP,也可能是POLLERR,eventSet事件集合發生了變化,這就可能導緻Selector會被喚醒。

這是與作業系統機制有關系的,JDK雖然僅僅是一個相容各個作業系統平台的軟體,但很遺憾在JDK5和JDK6最初的版本中(嚴格意義上來将,JDK部分版本都是),這個問題并沒有解決,而将這個帽子抛給了作業系統方,這也就是這個bug最終一直到2013年才最終修複的原因,最終影響力太廣。

8.2 Netty解決方法

在Netty最終解決方法是:建立一個新的Selector,将可用事件重新注冊到新的Selector中來終止空輪詢。回顧事件輪詢的關鍵代碼:

protected void run() {
        for (;;) {
            try {
                switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) {
                    case SelectStrategy.CONTINUE:
                        continue;
                    case SelectStrategy.SELECT:
                        select(wakenUp.getAndSet(false));
                        //省略select喚醒邏輯
                    default:
                     
                }

               //省略事件輪詢處理邏輯
        }
    }
           

前面我們提到select()方法解決了JDK空輪詢bug,它到底是如何解決的呢?下面我們來一探究竟,進入select()方法的源碼:

long currentTimeNanos = System.nanoTime();
for (;;) {
    // 1.定時任務截止事時間快到了,中斷本次輪詢
    ...
    // 2.輪詢過程中發現有任務加入,中斷本次輪詢
    ...
    // 3.阻塞式select操作
    selector.select(timeoutMillis);
    // 4.解決jdk的nio bug
    long time = System.nanoTime();
    if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) {
        selectCnt = 1;
    } else if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 &&
            selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {

        rebuildSelector();
        selector = this.selector;
        selector.selectNow();
        selectCnt = 1;
        break;
    }
    currentTimeNanos = time; 
    ...
 }
           

netty 會在每次進行 selector.select(timeoutMillis) 之前記錄一下開始時間currentTimeNanos,在select之後記錄一下結束時間,判斷select操作是否至少持續了timeoutMillis秒(這裡将time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos改成time - currentTimeNanos >= TimeUnit.MILLISECONDS.toNanos(timeoutMillis)或許更好了解一些),如果持續的時間大于等于timeoutMillis,說明就是一次有效的輪詢,重置selectCnt标志,否則,表明該阻塞方法并沒有阻塞這麼長時間,可能觸發了jdk的空輪詢bug,當空輪詢的次數超過一個閥值的時候,預設是512,就開始重建selector。

本節參考自:https://www.jianshu.com/p/3ec120ca46b2