Java NIO epoll bug 以及 Netty 的解决之道

最近学习 Netty 时,看到 Netty 宣称解决了很多 Java 原生 NIO 的很多 bug,其中之一是 epoll 空轮询导致 CPU 利用率 100%。

什么是 epoll 空轮询
如果使用 Java 原生 NIO 来编写服务器应用,代码一般类似:

// 创建、配置 ServerSocketChannel
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.socket().bind(new InetSocketAddress(9998));
serverChannel.configureBlocking(false);

// 创建 Selector
Selector selector = Selector.open();

// 注册
serverChannel.register(selector, SelectionKey.OP_ACCEPT);

while (true) {
selector.select(); // select 可能在无就绪事件时异常返回!

Set<SelectionKey> readyKeys = selector.selectedKeys();
Iterator<SelectionKey> it = readyKeys.iterator();

while (it.hasNext()) {
    SelectionKey key = it.next();
    ...  // 处理事件
    it.remove();
}

}
selector.select() 应该 一直阻塞,直到有就绪事件到达,但很遗憾,由于 Java NIO 实现上存在 bug,select() 可能在 没有 任何就绪事件的情况下返回,从而导致 while(true) 被不断执行,最后导致某个 CPU 核心的利用率飙升到 100%,这就是臭名昭著的 Java NIO 的 epoll bug。

实际上,这是 Linux 系统下 poll/epoll 实现导致的 bug,但 Java NIO 并未完善处理它,所以也可以说是 Java NIO 的 bug。

该问题最早在 Java 6 发现,随后很多版本声称解决了该问题,但实际上只是降低了该 bug 的出现频率,起码从网上搜索看,Java 8 还是存在该问题(当 Thrift 遇到 JDK Epoll Bug)。

Netty 的解决之道
很多 NIO 框架都在 Java 原生 NIO 基础上增加了解决 epoll 空轮询的增强,本文介绍 Netty 的做法。

Netty 的解决方式分为两步:

检测 epoll bug;
通过重建 Selector 解决 epoll bug;
其实大部分框架的解决方式都类似,差别仅在 检测方式,检测到后基本都是通过重建 Selector 来解决。

检测 epoll 空轮询
Netty 使用 NioEventLoop.select() 替代 Selector.select(),检测 epoll bug 的逻辑就在 NioEventLoop.select() 中:

selectCnt = 0; // epoll 空轮询场景下 select 调用次数
long currentTimeNanos = System.nanoTime(); // 每个 for 循环开始时的绝对时间

for (;;) {
timeoutMillis = ... // 初始化超时参数

int selectedKeys = selector.select(timeoutMillis);
selectCnt++;

long time = System.nanoTime();  // 记录执行到此处的绝对时间:

// 检测逻辑
if (time - currentTimeNanos > timeoutMillis) {
    selectCnt = 1;  // 未发生 epoll 空轮询,所以把 selectCnt 重置为 1
} else if (selectCnt >= 重试次数阈值(默认 512)) {
    selector = selectRebuildSelector(selectCnt);  // 解决 epoll bug 的实际逻辑
    selectCnt = 1;  // 解决本次 epoll bug,重置 selectCnt
    break;
}

currentTimeNanos = time;  // 重置下次 for 循环开始时间

}
如果满足以下两个条件,则认为发生 epoll 空轮询:

selector.select(timeoutMillis) 阻塞时间小于 timeoutMillis,且
select 执行次数 > 阈值(默认 512)
因为阻塞时间无法做到很精准,所以若某次阻塞时间大于等于 timeoutMillis 立刻重置 selectCnt 为 1,即需要 连续 512 次 selector.select(timeoutMillis) 阻塞时间都小于 timeoutMillis 才认为发生了 epoll 空轮询。

timeoutMillis 有一套计算逻辑,无法进行配置,而次数阈值可以通过 io.netty.selectorAutoRebuildThreshold 系统配置进行设置,默认值为 512。

解决 epoll 空轮询
检测到 epoll bug 后,通过 selectRebuildSelector 方法来实际解决:

private Selector selectRebuildSelector(int selectCnt) throws IOException {
// The selector returned prematurely many times in a row.
// Rebuild the selector to work around the problem.
logger.warn("Selector.select() returned prematurely {} times in a row; rebuilding Selector {}.", selectCnt, selector);

rebuildSelector();  // 重建逻辑
Selector selector = this.selector;

// Select again to populate selectedKeys.
selector.selectNow();
return selector;

}
重建过程代理给了 rebuildSelector 方法,重建完成后,立即 selectNow 重新监听事件。

而 rebuildSelector 又把重建逻辑代理给了 rebuildSelector0:

/**

  • Replaces the current Selector of this event loop with newly created Selectors to work
  • around the infamous epoll 100% CPU bug.
    */
    public void rebuildSelector() {
    if (!inEventLoop()) {
    execute(new Runnable() {br/>@Override
    public void run() {
    rebuildSelector0(); // 重建逻辑
    }
    });
    return;
    }
    rebuildSelector0(); // 重建逻辑
    }
    inEventLoop() 判断当前线程是否是事件循环线程:

@Override
public boolean inEventLoop() {
return inEventLoop(Thread.currentThread());
}
而 inEventLoop(Thread) 定义在 EventExecutor 中,不同实现类的实现逻辑不同,NioEventLoop.inEventLoop 具体实现在其父类 SingleThreadEventLoop 中:

@Override
public boolean inEventLoop(Thread thread) {
return thread == this.thread;
}
在 Netty 中,一个 IO 线程可以处理多个 channel,但一个 channel 只能被一个 IO 线程处理,重建 Selector 必须 在事件循环线程内完成,如果当前线程是 NioEventLoop 线程,则直接在当前线程执行 Selector 重建,否则将重建任务 submit 给各个 NioEventLoop。

添加该判断的原因是除了 NioEventLoop 检测到 epoll bug 时会调用 rebuildSelector 外,NioEventLoopGroup 也有调用:

public void rebuildSelectors() {
for (EventExecutor e: this) {
((NioEventLoop) e).rebuildSelector();
}
}
该方法被暴露出来,供用户调用,因此最终 rebuildSelector 是可能在非事件循环线程中被调用的。

重建任务最终在 rebuildSelector0 中完成,重建步骤:

新建一个 Selector;
将旧 Selector 的所有 channel 注册到新 Selector 上;
关闭旧 Selector;
至此,完成对 epoll bug 的解决。
ps:其实主要就是这个selector这个类的slelect方法没有block,通过从建selector来解决

Java NIO epoll bug 以及 Netty 的解决之道

原文地址:https://blog.51cto.com/youling87/2480852

时间: 2024-11-10 11:25:22

Java NIO epoll bug 以及 Netty 的解决之道的相关文章

Java NIO框架Mina、Netty、Grizzly介绍与对比(zz)

Mina: Mina(Multipurpose Infrastructure for Network Applications) 是 Apache 组织一个较新的项目,它为开发高性能和高可用性的网络应用程序提供了非常便利的框架.当前发行的 Mina 版本2.04支持基于 Java NIO 技术的 TCP/UDP 应用程序开发.串口通讯程序,Mina 所支持的功能也在进一步的扩展中.目前,正在使用 Mina的应用包括:Apache Directory Project.AsyncWeb.AMQP(A

Java NIO框架Mina、Netty、Grizzly介绍与对比

Mina Mina(Multipurpose Infrastructure for Network Applications) 是 Apache组织一个较新的项目,它为开发高性能和高可用性的网络应用程序提供了非常便利的框架.当前发行的 Mina 版本2.04支持基于 JavaNIO 技术的 TCP/UDP 应用程序开发.串口通讯程序,Mina 所支持的功能也在进一步的扩展中.目前,正在使用Mina的应用包括:Apache Directory Project.AsyncWeb.AMQP(Advan

JAVA NIO Selector 知识三

Selector(选择器) Selector工作流程:我们把想要的soketchannel告诉selector后,我们就去可以做别的事情,当有事件发生的时候,selector会通知我们,然后获取selectionkey,获得我们感兴趣的事件. selecotr是java nio多路复用的关键类,selector实现了一个线程管理多个channel,只需要更少的资源来处理更多的通道,节省线程之间的开销,这么说seletor是以前cpu很贵的时候,现在很多公司的机器都是多核,充分利用cpu才是最好的

Java多线程:Linux多路复用,Java NIO与Netty简述

JVM的多路复用器实现原理 Linux 2.5以前:select/poll Linux 2.6以后: epoll Windows: IOCP Free BSD, OS X: kqueue 下面仅讲解Linux的多路复用. Linux中的IO Linux的IO将所有外部设备都看作文件来操作,与外部设备的操作都可以看做文件操作,其读写都使用内核提供的系统调用,内核会返回一个文件描述符(fd, file descriptor),例如socket读写使用socketfd.描述符是一个索引,指向内核中一个

学习 java netty (一) -- java nio

前言:最近在研究java netty这个网络框架,第一篇先介绍java的nio. java nio在jdk1.4引入,其实也算比较早的了,主要引入非阻塞io和io多路复用.内部基于reactor模式. nio核心: - buffer - channel - selector buffer: 类似网络编程中的缓冲区,有 ByteBuffer 字节 CharBuffer 字符 IntBuffer DoubleBuffer- 常用的有ByteBuffer和CharBuffer java nio buf

Netty 中 IOException: Connection reset by peer 与 java.nio.channels.ClosedChannelException: null

最近发现系统中出现了很多 IOException: Connection reset by peer 与 ClosedChannelException: null 深入看了看代码, 做了些测试, 发现 Connection reset 会在客户端不知道 channel 被关闭的情况下, 触发了 eventloop 的 unsafe.read() 操作抛出 而 ClosedChannelException 一般是由 Netty 主动抛出的, 在 AbstractChannel 以及 SSLHand

JAVA NIO 类库的异步通信框架netty和mina

Netty 和 Mina 我究竟该选择哪个? 根据我的经验,无论选择哪个,都是个正确的选择.两者各有千秋,Netty 在内存管理方面更胜一筹,综合性能也更优.但是,API 变更的管理和兼容性做的不是太好.相比于 Netty,Mina 的前向兼容性.内聚的可维护性功能更多,例如 JMX 的集成.性能统计.状态机等. Netty 是业界最流行的 NIO 框架之一,它的健壮性.功能.性能.可定制性和可扩展性在同类框架中都是首屈一指的,它已经得到成百上千的商用项目验证,例如 Hadoop 的 RPC 框

1. Netty准备知识:Java NIO

前言:我们知道,Netty是基于NIO开发的一套框架,在学习Netty之前,我们先学习下Java NIO. 一.IO多路复用模型 IO多路复用模型使用了Reactor设计模式,主要有三种实现:Reacotr单线程.Reactor多线程.Reactor主从模式. 1. Reactor单线程 在Reactor单线程模式中,所有客户端的请求处理都交给一个线程,串行化处理,效率较低. 2. Reactor多线程 在Reactor多线程模式中,acceptor线程负责接受客户端请求并将请求处理任务交给线程

5. 彤哥说netty系列之Java NIO核心组件之Channel

你好,我是彤哥,本篇是netty系列的第五篇. 简介 上一章我们一起学习了如何使用Java原生NIO实现群聊系统,这章我们一起来看看Java NIO的核心组件之一--Channel. 思维转变 首先,我想说的最重要的一个点是,学习NIO思维一定要从BIO那种一个连接一个线程的模式转变成多个连接(Channel)共用一个线程来处理的这种思维. 1个Connection = 1个Socket = 1个Channel,这几个概念可以看作是等价的,都表示一个连接,只不过是用在不同的场景中. 如果单从阻塞