NIO空轮训出现的原理以及修复方案

O&NIO介绍

IO读取

NIO读取

NIO中epoll空轮询表现

public static void main(String[] args) {
        Selector selector = Selector.open();
        System.out.println(selector.isOpen());
        ServerSocketChannel socketChannel = ServerSocketChannel.open();
        InetSocketAddress inetSocketAddress = new InetSocketAddress("localhost", 8080);
        socketChannel.bind(inetSocketAddress);
        socketChannel.configureBlocking(false);
        int ops = socketChannel.validOps();
        SelectionKey selectionKey = socketChannel.register(selector, ops, null);
        Set selectedKeys = selector.selectedKeys();
        for (;;) {
            System.out.println("等待...");
            /**
             * 通常是阻塞的,但是在epoll空轮询的bug中,
             * 之前处于连接状态突然被断开,select()的
             * 返回值noOfKeys应该等于0,也就是阻塞状态
             * 但是,在此bug中,select()被唤醒,而又
             * 没有数据传入,导致while (itr.hasNext())
             * 根本不会执行,而后就进入for (;;) {的死循环
             * 但是,正常状态下应该阻塞,也就是只输出一个waiting...
             * 而此时进入死循环,不断的输出waiting...,程序死循环
             * cpu自然很快飙升到100%状态。
             */
            int noOfKeys = selector.select();
            System.out.println("selected keys:" + noOfKeys);
            Iterator itr = selectedKeys.iterator();
            while (itr.hasNext()) {
                SelectionKey key = (SelectionKey) itr.next();
                if (key.isAcceptable()) {
                    SocketChannel client = socketChannel.accept();
                    client.configureBlocking(false);
                    client.register(selector, SelectionKey.OP_READ);
                    System.out.println("The new connection is accepted from the client: " + client);
                } else if (key.isReadable()) {
                    SocketChannel client = (SocketChannel) key.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(256);
                    client.read(buffer);
                    String output = new String(buffer.array()).trim();
                    System.out.println("Message read from client: " + output);
                    if (output.equals("Bye Bye")) {
                        client.close();
                        System.out.println("The Client messages are complete; close the session.");
                    }
                }
                itr.remove();
            }
        }
    }

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年才最终修复的原因,最终影响力太广。

解决办法
不完善的解决办法
grizzly的commiteer们最先进行修改的,并且通过众多的测试说明这种修改方式大大降低了JDK NIO的问题。

// the key you registered on the temporary selector
if (SelectionKey != null)  {  
   // cancel the SelectionKey that was registered with the temporary selector
   SelectionKey.cancel();  
   // flush the cancelled key
   temporarySelector.selectNow();
} 

但是,这种修改仍然不是可靠的,一共有两点:

  1. 多个线程中的SelectionKey的key的cancel,很可能和下面的Selector.selectNow同时并发,如果是导致key的cancel后运行很可能没有效果
  2. 与其说第一点使得NIO空转出现的几率大大降低,经过Jetty服务器的测试报告发现,这种重复利用Selector并清空SelectionKey的改法很可能没有任何的效果,

完善的解决办法

最终的终极办法是创建一个新的Selector:

Trash wasted Selector, creates a new one.

各应用具体解决方法
Jetty

Jetty首先定义两了-D参数:

  • JVMBUG_THRESHHOLD

org.mortbay.io.nio.JVMBUG_THRESHHOLD, defaults to 512 and is the number of zero select returns that must be exceeded in a period.

  • threshhold

org.mortbay.io.nio.MONITOR_PERIOD defaults to 1000 and is the period over which the threshhold applies.

第一个参数是select返回值为0的计数,第二个是多长时间,整体意思就是控制在多长时间内,如果Selector.select不断返回0,说明进入了JVM的bug的模式。

做法是:

  • 记录select()返回为0的次数(记做jvmBug次数)
  • 在MONITOR_PERIOD时间范围内,如果jvmBug次数超过JVMBUG_THRESHHOLD,则新创建一个selector
long before = now;
int selected = selector.select(wait);
now = System.currentTimeMillis();
_idleTimeout.setNow(now);
_timeout.setNow(now);

/**
 * 判断等待时间是否大于_JVMBUG_THRESHHOLD
 * selected是否等于0,时间的机制。
 */
if (_JVMBUG_THRESHHOLD > 0 && selected == 0
        && wait > _JVMBUG_THRESHHOLD
        && (now - before) < (wait/2)) {
    _jvmBug++;
    // 判断jvmBug计数是否大于设置的标准值
    if (_jvmBug >= (_JVMBUG_THRESHHOLD2)) {
        // 确定发生epoll空轮询bug,开启新的selector
        synchronized (this) {
            _lastJvmBug = now;
            final Selector new_selector = Selector.open();
            // 将之前的事件复制到新的selector
            for (SelectionKey k:selector.selectedKeys()) {
                if (!k.isValid() || k.interestOps() == 0) {
                    continue;
                }
                final SelectableChannel channel = k.channel();
                final Object attachment = k.attachment();
                if (attachment == null) {
                    addChange(channel);
                } else {
                    addChange(channel, attachment);
                }
            }
            // 关闭旧selector
            _selector.close();
            // 开启新的selector
            _selector = new_selector;
            // bug数归0
            _jvmBug = 0;
            return;
        }
    }
}

Netty

思路和Jetty的处理方式几乎是一样的,就是netty讲重建Selector的过程抽取成了一个方法。

Netty解决空轮询的4步骤:

Netty的解决办法总览:

  • 1、对Selector的select操作周期进行统计,每完成一次空的select操作进行一次计数,若在某个周期内连续发生N次空轮询,则触发了epoll死循环bug。
  • 2、重建Selector,判断是否是其他线程发起的重建请求,若不是则将原SocketChannel从旧的Selector上去除注册,重新注册到新的Selector上,并将原来的Selector关闭。

Netty解决空轮询的4步骤

Netty解决空轮询的4步骤,具体如下:

在这里插入图片描述

第一部分:定时阻塞select(timeMillins)

  • 先定义当前时间currentTimeNanos。
  • 接着计算出一个执行最少需要的时间timeoutMillis。
  • 定时阻塞select(timeMillins) 。
  • 每次对selectCnt做++操作。

第二部分:有效IO事件处理逻辑

第三部分:超时处理逻辑

  • 如果查询超时,则seletCnt重置为1。

第四步: 解决空轮询 BUG

  • 一旦到达SELECTOR_AUTO_REBUILD_THRESHOLD这个阀值,就需要重建selector来解决这个问题。
  • 这个阀值默认是512。
  • 重建selector,重新注册channel通道

Netty解决空轮询的4步骤的核心代码


long time = System.nanoTime();

//调用select方法,阻塞时间为上面算出的最近一个将要超时的定时任务时间
int selectedKeys = selector.select(timeoutMillis);

//计数器加1
++selectCnt;

if (selectedKeys != 0 || oldWakenUp || this.wakenUp.get() || this.hasTasks() || this.hasScheduledTasks()) {
   //进入这个分支,表示正常场景     

   //selectedKeys != 0: selectedKeys个数不为0, 有io事件发生
   //oldWakenUp:表示进来时,已经有其他地方对selector进行了唤醒操作
   //wakenUp.get():也表示selector被唤醒
   //hasTasks() || hasScheduledTasks():表示有任务或定时任务要执行
   //发生以上几种情况任一种则直接返回

   break;
}

//此处的逻辑就是: 当前时间 - 循环开始时间 >= 定时select的时间timeoutMillis,说明已经执行过一次阻塞select(), 有效的select
if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) {
   //进入这个分支,表示超时,属于正常的场景
   //说明发生过一次阻塞式轮询, 并且超时
   selectCnt = 1;
} else if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 && selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {
   //进入这个分支,表示没有超时,同时 selectedKeys==0
   //属于异常场景
   //表示启用了select bug修复机制,
   //即配置的io.netty.selectorAutoRebuildThreshold
   //参数大于3,且上面select方法提前返回次数已经大于
   //配置的阈值,则会触发selector重建

   //进行selector重建
   //重建完之后,尝试调用非阻塞版本select一次,并直接返回
   selector = this.selectRebuildSelector(selectCnt);
   selectCnt = 1;
   break;
}
currentTimeNanos = time;

Netty对Selector.select提前返回的检测和处理逻辑主要在NioEventLoop.select方法中,完整的代码如下:

public final class NioEventLoop extends SingleThreadEventLoop {

    private void select(boolean oldWakenUp) throws IOException {
        Selector selector = this.selector;

        try {
            //计数器置0
            int selectCnt = 0;
            long currentTimeNanos = System.nanoTime();
            
            //根据注册的定时任务,获取本次select的阻塞时间
            long selectDeadLineNanos = currentTimeNanos + this.delayNanos(currentTimeNanos);

            while(true) {
                //每次循环迭代都重新计算一次select的可阻塞时间
                long timeoutMillis = (selectDeadLineNanos - currentTimeNanos + 500000L) / 1000000L;
                
                //如果可阻塞时间为0,表示已经有定时任务快要超时
                //此时如果是第一次循环(selectCnt=0),则调用一次selector.selectNow,然后退出循环返回
                //selectorNow方法的调用主要是为了尽可能检测出准备好的网络事件进行处理
                if (timeoutMillis <= 0L) {
                    if (selectCnt == 0) {
                        selector.selectNow();
                        selectCnt = 1;
                    }
                    break;
                }
                
                //如果没有定时任务超时,但是有以前注册的任务(这里不限定是定时任务),
                //且成功设置wakenUp为true,则调用selectNow并返回
                if (this.hasTasks() && this.wakenUp.compareAndSet(false, true)) {
                    selector.selectNow();
                    selectCnt = 1;
                    break;
                }
                
                //调用select方法,阻塞时间为上面算出的最近一个将要超时的定时任务时间
                int selectedKeys = selector.select(timeoutMillis);
                
                //计数器加1
                ++selectCnt;
                

                if (selectedKeys != 0 || oldWakenUp || this.wakenUp.get() || this.hasTasks() || this.hasScheduledTasks()) {
               //进入这个分支,表示正常场景     
                    
                //selectedKeys != 0: selectedKeys个数不为0, 有io事件发生
                //oldWakenUp:表示进来时,已经有其他地方对selector进行了唤醒操作
                //wakenUp.get():也表示selector被唤醒
                //hasTasks() || hasScheduledTasks():表示有任务或定时任务要执行
                //发生以上几种情况任一种则直接返回
                    
                    break;
                }

                //如果线程被中断,计数器置零,直接返回
                if (Thread.interrupted()) {
                    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;
                }

                //这里判断select返回是否是因为计算的超时时间已过,
                //这种情况下也属于正常返回,计数器置1,进入下次循环
                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) {
                    //进入这个分支,表示没有超时,同时 selectedKeys==0
                    //属于异常场景
                    //表示启用了select bug修复机制,
                    //即配置的io.netty.selectorAutoRebuildThreshold
                    //参数大于3,且上面select方法提前返回次数已经大于
                    //配置的阈值,则会触发selector重建
                    
                    //进行selector重建
                    //重建完之后,尝试调用非阻塞版本select一次,并直接返回
                    selector = this.selectRebuildSelector(selectCnt);
                    selectCnt = 1;
                    break;
                }

                currentTimeNanos = time;
            }

            //这种是对于关闭select bug修复机制的程序的处理,
            //简单记录日志,便于排查问题
            if (selectCnt > 3 && logger.isDebugEnabled()) {
                logger.debug("Selector.select() returned prematurely {} times in a row for Selector {}.", selectCnt - 1, selector);
            }
        } catch (CancelledKeyException var13) {
            if (logger.isDebugEnabled()) {
                logger.debug(CancelledKeyException.class.getSimpleName() + " raised by a Selector {} - JDK bug?", selector, var13);
            }
        }

    }
    
    private Selector selectRebuildSelector(int selectCnt) throws IOException {
        logger.warn("Selector.select() returned prematurely {} times in a row; rebuilding Selector {}.", selectCnt, this.selector);
        //进行selector重建
        this.rebuildSelector();
        Selector selector = this.selector;
        //重建完之后,尝试调用非阻塞版本select一次,并直接返回
        selector.selectNow();
        return selector;
    }   
}

上面调用的this.rebuildSelector()源码如下:

 
public final class NioEventLoop extends SingleThreadEventLoop {

    public void rebuildSelector() {
        //如果不在该线程中,则放到任务队列中
        if (!this.inEventLoop()) {
            this.execute(new Runnable() {
                public void run() {
                    NioEventLoop.this.rebuildSelector0();
                }
            });
        } else {
            //否则表示在该线程中,直接调用实际重建方法
            this.rebuildSelector0();
        }
    }
    
    private void rebuildSelector0() {
        Selector oldSelector = this.selector;
        
        //如果旧的selector为空,则直接返回
        if (oldSelector != null) {
            NioEventLoop.SelectorTuple newSelectorTuple;
            try {
                //新建一个新的selector
                newSelectorTuple = this.openSelector();
            } catch (Exception var9) {
                logger.warn("Failed to create a new Selector.", var9);
                return;
            }

            int nChannels = 0;
            Iterator var4 = oldSelector.keys().iterator();
            
            //对于注册在旧selector上的所有key,依次重新在新建的selecor上重新注册一遍
            while(var4.hasNext()) {
                SelectionKey key = (SelectionKey)var4.next();
                Object a = key.attachment();

                try {
                    if (key.isValid() && key.channel().keyFor(newSelectorTuple.unwrappedSelector) == null) {
                        int interestOps = key.interestOps();
                        key.cancel();
                        SelectionKey newKey = key.channel().register(newSelectorTuple.unwrappedSelector, interestOps, a);
                        if (a instanceof AbstractNioChannel) {
                            ((AbstractNioChannel)a).selectionKey = newKey;
                        }

                        ++nChannels;
                    }
                } catch (Exception var11) {
                    logger.warn("Failed to re-register a Channel to the new Selector.", var11);
                    if (a instanceof AbstractNioChannel) {
                        AbstractNioChannel ch = (AbstractNioChannel)a;
                        ch.unsafe().close(ch.unsafe().voidPromise());
                    } else {
                        NioTask<SelectableChannel> task = (NioTask)a;
                        invokeChannelUnregistered(task, key, var11);
                    }
                }
            }

            //将该NioEventLoop关联的selector赋值为新建的selector
            this.selector = newSelectorTuple.selector;
            this.unwrappedSelector = newSelectorTuple.unwrappedSelector;

            try {
                //关闭旧的selector
                oldSelector.close();
            } catch (Throwable var10) {
                if (logger.isWarnEnabled()) {
                    logger.warn("Failed to close the old Selector.", var10);
                }
            }

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

Netty空轮询的阈值配置

Netty在NioEventLoop中考虑了这个问题,并通过在select方法不正常返回(Netty源码注释称其为prematurely,即提前返回)超过一定次数时重新创建新的Selector来修复此bug。

Netty提供了配置参数io.netty.selectorAutoRebuildThreshold供用户定义select创建新Selector提前返回的次数阈值,超过该次数则会触发Selector自动重建,默认为512。

但是如果指定的io.netty.selectorAutoRebuildThreshold小于3在Netty中被视为关闭了该功能。

public final class NioEventLoop extends SingleThreadEventLoop {

    private static final int SELECTOR_AUTO_REBUILD_THRESHOLD;

    static {
        //......省略部分代码

        int selectorAutoRebuildThreshold = SystemPropertyUtil.getInt("io.netty.selectorAutoRebuildThreshold", 512);
        if (selectorAutoRebuildThreshold < 3) {
            selectorAutoRebuildThreshold = 0;
        }

        SELECTOR_AUTO_REBUILD_THRESHOLD = selectorAutoRebuildThreshold;
        if (logger.isDebugEnabled()) {
            logger.debug("-Dio.netty.noKeySetOptimization: {}", DISABLE_KEY_SET_OPTIMIZATION);
            logger.debug("-Dio.netty.selectorAutoRebuildThreshold: {}", SELECTOR_AUTO_REBUILD_THRESHOLD);
        }

    }
}

参考文献:

Netty源码-Selector.select bug修复实现 - 简书

https://blog.csdn.net/zhengchao1991/article/details/106534280

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Java NIO是一种用于高效处理I/O操作的Java API。其原理是在Java中使用了一些新的I/O概念和类,包括缓冲区(Buffer)、通道(Channel)、选择器(Selector)等。 缓冲区是Java NIO中重要的概念之一,它是一个可以被读写的内存块,可以作为I/O操作的输入输出缓存。在Java NIO中,所有的数据读取和写入都必须通过缓冲区来完成。缓冲区分为直接缓冲区和非直接缓冲区两种。直接缓冲区是通过调用操作系统的API直接分配的内存块,可以显著提高I/O操作的性能;非直接缓冲区则是通过Java堆内存分配的,性能略逊于直接缓冲区。 通道是Java NIO中另一个重要的概念,它是一个用于传输数据的实体,可以读取和写入数据。通道提供了比Java传统的I/O流更高效的数据传输方式,因为它可以在缓冲区和底层操作系统之间建立直接的连接,避免了I/O流中的中间层。Java NIO中提供了多种类型的通道,包括文件通道、套接字通道等。 选择器是Java NIO中另一个重要的概念,它可以检测一个或多个通道的状态,并且可以在通道准备好进行读写操作时被通知。选择器提供了一种高效的方式来处理多个通道的I/O操作,可以避免线程阻塞和不必要的轮询。 综上所述,Java NIO通过使用缓冲区、通道和选择器等新的概念和类,可以提供更高效的I/O操作方式,可以在处理高并发和大数据量的情况下发挥出更好的性能。 ### 回答2: Java NIO(Non-blocking I/O) 是Java提供的一种新的I/O处理方式。相比于传统的I/O操作,Java NIO使用了非阻塞的方式来处理输入和输出。以下是Java NIO的主要原理Java NIO主要包含以下几个核心组件: 1. Channel(通道):是用于读写数据的对象,可以理解为指向实际数据源的双向管道。不同类型的数据可以通过不同类型的通道进行传输。 2. Buffer(缓冲区):是一个用于存储数据的对象,实际上就是一个数组。数据通过缓冲区在通道和应用程序之间传输。 3. Selector(选择器):是一个可以监听多个通道事件的对象。可以通过Selector注册通道,并监听通道上的不同事件,如连接、接收和发送数据等。 Java NIO的工作流程如下: 1. 创建通道:使用Channel类创建需要的通道,如FileChannel、SocketChannel、ServerSocketChannel等。 2. 创建缓冲区:使用Buffer类创建需要的缓冲区,如ByteBuffer、CharBuffer等。 3. 将数据写入缓冲区:通过调用缓冲区的put()方法将数据写入缓冲区。 4. 切换缓冲区为读模式:通过调用缓冲区的flip()方法切换缓冲区为读模式。 5. 从缓冲区读取数据:通过调用缓冲区的get()方法从缓冲区读取数据。 6. 注册通道,并监听感兴趣的事件:通过Selector对象的register()方法注册通道,并指定感兴趣的事件,如连接就绪、接收就绪、写入就绪等。 7. 轮询选择器:通过Selector对象的select()方法进行轮询,查看是否有感兴趣的事件准备就绪。 8. 处理选择器就绪的事件:通过遍历SelectionKey集合,处理选择器返回的已就绪的事件。 9. 执行对应的操作:根据不同的事件类型,执行相应的操作,如连接、接收和发送数据等。 总结来说,Java NIO通过非阻塞的方式处理I/O操作,通过通道、缓冲区和选择器来实现高效的IO处理。它具有更高的处理能力、更低的内存消耗和更少的线程占用,适用于高并发、大数据量的应用场景。 ### 回答3: Java NIO(New I/O)是Java编程语言的一种扩展,提供了可以进行非阻塞I/O操作的功能。它是为了解决传统的阻塞I/O模型在高并发和大规模数据处理场景下性能不佳的问题而引入的。 Java NIO的核心概念是通道(Channel)和缓冲区(Buffer)。通道代表了一个可以进行读写操作的实体,提供了非阻塞的I/O操作。缓冲区是一个内存块,用于临时存储数据,供通道读写数据。 在Java NIO中,通过Selector可以同时监控多个通道的输入/输出状态。Selector会不断地轮询注册在其上的通道,如果某个通道发生了读或写事件,Selector就会将该通道加入到就绪集合中。这样,我们就可以通过Selector轮询检查哪些通道已经准备好进行读写,从而避免了传统阻塞I/O中每个连接需要一个线程等待数据的情况。 在Java NIO中,数据通过缓冲区进行传输。读取数据时,可以从通道读取数据到缓冲区中,然后从缓冲区中读取数据;写入数据时,可以将数据写入缓冲区,然后从缓冲区写入通道。通过使用缓冲区,可以减少实际的I/O操作次数,提高效率。 Java NIO还提供了FileChannel用于对文件进行I/O操作,以及SocketChannel和ServerSocketChannel用于对网络Socket进行I/O操作。此外,Java NIO还支持内存映射文件(Memory-mapped files),可以将文件直接映射到内存中,避免了传统文件I/O的开销。 总之,Java NIO通过通道、缓冲区和Selector等实现了高效的非阻塞I/O操作。相对于传统的阻塞I/O模型,可有效提高系统的并发处理能力和性能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值