最近学习 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