当前位置: 首页 > article >正文

Netty——NIO 空轮询 bug

文章目录

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


1. NIO 服务端的常见代码

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. 一种简单的解决方案

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 出现的概率降低了许多。


http://www.kler.cn/a/598066.html

相关文章:

  • Redis + 布隆过滤器解决缓存穿透问题
  • Kafka-1
  • Redis、Memcached应用场景对比
  • 字节DAPO算法:改进DeepSeek的GRPO算法-解锁大规模LLM强化学习的新篇章(代码实现)
  • 数据结构 -- 线索二叉树
  • 针对永磁电机(PMM)的d轴和q轴电流,考虑交叉耦合补偿,设计P1控制器并推导出相应的传递函数
  • 2025.3.17-2025.3.23学习周报
  • 银河麒麟桌面版包管理器(一)
  • vue3 UnwrapRef 与 unref的区别
  • 【从零开始学习计算机科学】软件工程(一)软件工程中的过程模型
  • 安装PrettyZoo操作指南
  • 计算机二级:函数基础题
  • 相控阵雷达的EIRP和G/T
  • 路由工程师大纲-1:路由+AI研究的知识体系与低成本论文方向
  • WPF-实现按钮的动态变化
  • 深度剖析HTTP协议—GET/PUT请求方法的使用-构造请求的方法
  • sv线程基础
  • React 开发环境搭建
  • python学习笔记--实现简单的爬虫(二)
  • JS数组扁平化(多维转一维)