Netty——NIO 空轮询 bug

文章目录

  • [1. NIO 服务端的常见代码](#1. NIO 服务端的常见代码)
  • [2. 成因](#2. 成因)
  • [3. 危害](#3. 危害)
  • [4. 一种简单的解决方案](#4. 一种简单的解决方案)
  • [5. Netty 的解决方案](#5. Netty 的解决方案)
    • [5.1 涉及的变量、常量和方法](#5.1 涉及的变量、常量和方法)
    • [5.2 判断流程](#5.2 判断流程)
    • [5.3 优点](#5.3 优点)
  • [6. JDK 对 NIO 的优化](#6. JDK 对 NIO 的优化)
  • [7. 总结](#7. 总结)

1. NIO 服务端的常见代码

java 复制代码
public static void main(String[] args) throws IOException {
    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    serverSocketChannel.socket().bind(new InetSocketAddress(8080));
    serverSocketChannel.configureBlocking(false);

    Selector selector = Selector.open();
    serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    while (true) {
    	// 如果没有连接的通道,则跳过本轮循环
        int readyChannels = selector.select();
        if (readyChannels == 0) {
            continue;
        }

        Set<SelectionKey> selectionKeys = selector.selectedKeys();
        Iterator<SelectionKey> iterator = selectionKeys.iterator();
        while (iterator.hasNext()) {
            SelectionKey selectionKey = iterator.next();
            if (selectionKey.isAcceptable()) {
                SocketChannel socketChannel = serverSocketChannel.accept();
                socketChannel.configureBlocking(false);
                socketChannel.register(selector, SelectionKey.OP_READ);
            } else if (selectionKey.isReadable()) {
                // 处理读取数据的逻辑
            }
            iterator.remove();
        }
    }
}

说明:select() 是阻塞的,直到有返回值。但当它频繁返回 0 时,会导致 CPU 不断跳过本轮循环,在循环内空转,这就出现了空轮询 bug。

2. 成因

Selector 借助操作系统的多路复用机制(例如 Linux 系统的 epoll)来监控多个通道的 I/O 事件 。不过在某些特定情形下,Selector 对象的 select()select(long timeout) 方法可能会提前返回 0。此时没有任何通道准备好进行 I/O 操作,这就引发了空轮询现象。

这种 bug 通常是由 底层操作系统的多路复用机制 、Java 虚拟机(JVM)以及网络环境等多方面因素共同导致的。例如,在 Linux 系统中使用 epoll 时,当 epoll_ctl 操作失败或者 epoll_wait 返回错误时,就可能致使 Selector 出现空轮询问题。

3. 危害

空轮询 bug 会 Selector 不断地进行无意义的轮询 ,从而 使 CPU 使用率急剧上升,系统性能大幅下降。

4. 一种简单的解决方案

java 复制代码
public static void main(String[] args) throws IOException {
    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    serverSocketChannel.socket().bind(new InetSocketAddress(8080));
    serverSocketChannel.configureBlocking(false);

    int emptySelections = 0; // 统计空轮询的次数
    Selector selector = Selector.open();
    serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    while (true) {
        // 如果没有连接的通道,则空轮询的次数加一,并跳过本轮循环
        int readyChannels = selector.select();
        if (readyChannels == 0) {
            emptySelections++;
            // 如果空轮询的次数大于等于 100 次,则重建 selector,并将 空轮询的次数 置为 0
            if (emptySelections >= 100) {
                selector = rebuildSelector(selector);
                emptySelections = 0;
            }
            continue;
        }

        Set<SelectionKey> selectionKeys = selector.selectedKeys();
        Iterator<SelectionKey> iterator = selectionKeys.iterator();
        while (iterator.hasNext()) {
            SelectionKey selectionKey = iterator.next();
            if (selectionKey.isAcceptable()) {
                SocketChannel socketChannel = serverSocketChannel.accept();
                socketChannel.configureBlocking(false);
                socketChannel.register(selector, SelectionKey.OP_READ);
            } else if (selectionKey.isReadable()) {
                // 处理读取数据的逻辑
            }
            iterator.remove();
        }
    }
}

private static Selector rebuildSelector(Selector oldSelector) throws IOException {
    Selector newSelector = Selector.open();
    // 将旧 selector 的所有 有效 的通道 注册到新 selector 里
    for (SelectionKey key : oldSelector.keys()) {
        if (key.isValid() && key.channel().isOpen()) {
            int interestOps = key.interestOps();
            key.cancel(); // 从旧 selector 中取消注册
            key.channel().register(newSelector, interestOps);
        }
    }
    oldSelector.close();
    return newSelector;
}

说明:本解决方案认为 只要一个 selector 有至少 100 次的空轮询,就对其进行重建,重建时排除无效的通道。

缺点:检测机制不精确 。如果空轮询的次数在很长一段时间才累积到 100 次,那么这时候大概率不需要重建 selector。因为在这个场景中,空轮询是一个偶发事件 ,这样就会由于重建 selector 浪费一定的时间。如果将判断条件改为 在某个时间段内的空轮询次数超过某个值,那就会避免无效的 selector 重建。

5. Netty 的解决方案

5.1 涉及的变量、常量和方法

  • private static final int SELECTOR_AUTO_REBUILD_THRESHOLD 常量:空轮询次数的阈值(默认为 512),如果超过,则重建 selector。
  • int selectCnt 变量:run 方法循环外的局部变量,用于统计空轮询的次数。每次轮询都加一,无论是否是空轮询,如果不是空轮询,则会将其重新置为 0
  • protected void run() 方法:相当于 NIO 代码中的 main 方法,包含一个无限循环,在循环中监听并处理。
  • private int select(long deadlineNanos) 方法:监测已注册的通道中有没有发生事件,如果发生,则返回发生事件的通道数。该方法实际上使用的是 NIO 的 Selectorselect 方法,由于多种因素,该方法可能返回 0
  • private boolean unexpectedSelectorWakeup(int selectCnt) 方法:判断空轮询次数有没有大于等于阈值,如果超过,则触发重建 selector 的机制,并在 run 方法中将空轮询次数置为 0
  • public void rebuildSelector() 方法:触发重建 selector 的流程。
  • private void rebuildSelector0() 方法:重建 selector。

5.2 判断流程

  1. run 方法中,通过 select 方法监测有没有通道发生事件。如果由于某些因素,该方法返回 0,则表示发生了空轮询,之后调用 unexpectedSelectorWakeup 方法,如果该方法返回 true,则将 selectCnt 置为 0
  2. unexpectedSelectorWakeup 方法中判断 selectCnt 是否大于等于 SELECTOR_AUTO_REBUILD_THRESHOLD。如果是,则会调用 rebuildSelector 方法。
  3. rebuildSelector 方法中调用 rebuildSelector0 方法。
  4. rebuildSelector0 方法中,将通道绑定到新的 selector,最后关闭旧的 selector。

5.3 优点

这种方案比简单方案优秀的就是它的监测机制,简单方案监测的是 自服务端启动开始的空轮询次数 ,这种方案监测的是 连续的空轮询次数因为一旦轮询不是空轮询,selectCnt 就会清零)。

6. JDK 对 NIO 的优化

在 JDK 1.5、JDK 1.6 等早期版本中,Selector 的空轮询 bug 比较突出。Oracle 对 JDK 不断进行优化和修复,JDK 1.8 及后续版本在处理 Selector 相关问题上有了更好的表现。虽然不能绝对保证不会出现空轮询,但出现的概率相比早期版本大大降低。

7. 总结

NIO 的 Selector 借助底层操作系统的多路复用机制来监听多个通道的事件,由于某些因素,可能导致这个方法频繁返回 0,从而服务端不停地在循环中空转,这就是 NIO 的空轮询问题。

解决这个问题的方法就是统计空轮询的次数,当超过一个数值后,就重建 selector。其中的统计空轮询的机制很重要,建议设计为 统计连续空轮询的次数

这个问题在 JDK 的早期版本中比较退出,但随着 JDK 的升级,这个 bug 出现的概率降低了许多。

相关推荐
低头不见21 分钟前
Spring Boot 的启动流程
java·spring boot·后端
小码农<^_^>23 分钟前
linux环境变量
java·linux·运维
计算机-秋大田43 分钟前
基于Spring Boot的消防物资存储系统的设计与实现(LW+源码+讲解)
java·vue.js·spring boot·后端·课程设计
计算机-秋大田1 小时前
基于Spring Boot的乡村养老服务管理系统的设计与实现(LW+源码+讲解)
java·spring boot·后端
&有梦想的咸鱼&1 小时前
Android Compose 框架物理动画之弹簧动画(Spring、SpringSpec)深入剖析(二十七)
android·java·spring
天草二十六_简村人2 小时前
Rabbitmq消息被消费时抛异常,进入Unacked 状态,进而导致消费者不断尝试消费(上)
java·spring boot·分布式·后端·rabbitmq
多多*2 小时前
使用事件监听器来处理并发环境中RabbitMQ的同步响应问题
java·开发语言·spring boot·分布式·docker·mybatis
农夫阿才2 小时前
排序算法总结
java·算法·排序算法
失业写写八股文3 小时前
如何选择栈与堆?堆跟栈的区别
java·后端