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

相关推荐
angushine38 分钟前
Gateway获取下游最终响应码
java·开发语言·gateway
爱的叹息44 分钟前
关于 JDK 中的 jce.jar 的详解,以及与之功能类似的主流加解密工具的详细对比分析
java·python·jar
一一Null1 小时前
Token安全存储的几种方式
android·java·安全·android studio
AUGENSTERN_dc1 小时前
RaabitMQ 快速入门
java·后端·rabbitmq
晓纪同学1 小时前
C++ Primer (第五版)-第十三章 拷贝控制
java·开发语言·c++
小样vvv1 小时前
【源码】SpringMvc源码分析
java
nzwen6661 小时前
Redis学习笔记及总结
java·redis·学习笔记
燃星cro2 小时前
参照Spring Boot后端框架实现序列化工具类
java·spring boot·后端
匹马夕阳2 小时前
java开发中的设计模式之单例模式
java·单例模式·设计模式