高并发网络通信Netty之空轮询问题
一、问题背景
在 NioEventLoop 事件循环中,Selector 一次次 select() 返回为 0,且没有事件被触发,形成空转,导致 CPU 占用 100%,系统资源白白浪费。这种情况尤其在 高并发、连接数多、IO事件少 的场景下更容易出现。
源码位置:NioEventLoop.java
Netty(基于 Java NIO)的底层用到了 Selector.select()
方法来阻塞等待事件。
private int select(long deadlineNanos) throws IOException {if (deadlineNanos == NONE) {return selector.select();}// Timeout will only be 0 if deadline is within 5 microsecslong timeoutMillis = deadlineToDelayNanos(deadlineNanos + 995000L) / 1000000L;return timeoutMillis <= 0 ? selector.selectNow() : selector.select(timeoutMillis);}
二、可能出现的原因
- Linux内核(主要问题):在部分Linux的2.6的kernel中,poll和epoll对于突然中断的连接socket(如强制断网、防火墙中断连接)会对返回的eventSet事件集合置为POLLHUP,也可能是POLLERR,eventSet事件集合发生了变化,这就可能导致Selector会被唤醒(
select()
/epoll_wait()
立即返回0次事件); - JDKBug:尤其是 JDK 1.7~1.8 的 Selector 在 epoll 上的 bug:Selector.select() 会在 epoll 上不断空转;
- 其他线程调用
Selector.wakeup():
会唤醒正在阻塞的 select(),导致返回 0; - 注册的 Channel 被频繁地取消但未及时清理:导致 select 无法正确感知就绪事件;
- 并发线程操作 Selector:多线程并发注册或唤醒 Selector 造成竞争问题。
三、危害
-
CPU 飙高(100%)
-
线程空转
-
延迟提升,任务堆积
四、Netty空轮询问题排查
1.找出进程中CPU高的线程
记录下 CPU 高的线程 ID(十进制)
top -Hp <pid>
转换为十六进制线程ID,用于分析
printf "%x\n" <线程ID>
2.使用 jstack
查看线程栈
jstack <pid> > jstack.txt
搜索高 CPU 线程对应的十六进制线程ID(如 0x57q
),定位其线程栈:
grep -A 30 "nid=0x57q" jstack.txt
3.Netty空轮询的典型栈信息
如果是空轮询,堆栈信息一般会打印Netty以下这些类信息
sun.nio.ch.SelectorImpl.select
io.netty.channel.nio.NioEventLoop.select
io.netty.channel.nio.NioEventLoop.run
或类似以下堆栈信息结构:
"nioEventLoopGroup-3-1" #35 daemon prio=5 os_prio=0 tid=0x00007fba2c002000 nid=0x57q runnable [0x00007fba18ffd000]java.lang.Thread.State: RUNNABLEat sun.nio.ch.SelectorImpl.select(Native Method)at io.netty.channel.nio.NioEventLoop.select(NioEventLoop.java:752)at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:420)at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:905)
注意:线程状态为 RUNNABLE,但没有执行业务逻辑,说明在空轮询。
五、Netty官方解决方案
Netty 从 4.0.20.Final 之后 引入了自动修复机制
源码位置:NioEventLoop.java
int selectorAutoRebuildThreshold = SystemPropertyUtil.getInt("io.netty.selectorAutoRebuildThreshold", 512);
SELECTOR_AUTO_REBUILD_THRESHOLD = selectorAutoRebuildThreshold;private boolean unexpectedSelectorWakeup(int selectCnt) {if (Thread.interrupted()) {if (logger.isDebugEnabled()) {logger.debug("Selector.select() returned prematurely because " +"Thread.currentThread().interrupt() was called. Use " +"NioEventLoop.shutdownGracefully() to shutdown the NioEventLoop.");}return true;}// 触发条件:连续 select() 返回 0 超过阈值(默认 512 次)if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 &&selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {logger.warn("Selector.select() returned prematurely {} times in a row; rebuilding Selector {}.",selectCnt, selector);rebuildSelector();return true;}return false;}
连续 512 次空轮询(无事件)会触发一次 Selector
的重建。
// NioEventLoop.java
public void rebuildSelector() {// 1. 创建新Selectorfinal Selector newSelector = openSelector();// 2. 将旧Selector的Channel注册到新Selectorfor (SelectionKey key: oldSelector.keys()) {Channel ch = key.channel();// 重新注册Channel,并迁移监听事件ch.register(newSelector, key.interestOps(), key.attachment());}// 3. 替换旧Selectorselector = newSelector;// 4. 关闭旧Selector(延迟执行,避免阻塞)oldSelector.close();
}
六、实际项目中的优化建议
1.升级 JDK / Netty 版本
-
Netty 建议使用 4.1+ 版本。
-
JDK 尽量使用 1.8u60+ 或 JDK11+,避免老版本
epoll
的空轮询 bug。
2.合理配置 Selector 空轮询重建阈值
默认值是 512 次,如果你希望更快响应,可配置为更小值(代价是频繁 rebuild 会影响性能),比如 64,观察 CPU 是否下降。
System.setProperty("io.netty.selectorAutoRebuildThreshold", "128");// 通过系统参数调整阈值(默认512)
-Dio.netty.selectorAutoRebuildThreshold=1024
3.避免不必要的 wakeup()调用
-
如果业务线程调用
eventLoop.wakeup()
,注意不要过于频繁。 -
特别是 非 Netty 管理的线程调用时,要注意时机和频率。
七、总结
1.分层表述
- 现象 → “Selector在无事件时被频繁唤醒,导致线程空转”;
- 原因 → “JDK的
epoll
实现在连接异常中断时存在Bug”; - 解决方案 → “Netty通过计数空轮询次数,超过阈值后重建Selector”。
2.关联设计思想
-
“这是Fail-Safe机制的体现,通过自动重建避免单点故障”;
-
“类似Kafka处理ZooKeeper会话过期,都是通过重建恢复状态”
3.延伸扩展
-
“类似问题在Tomcat NIO中也有,需配置
selectorTimeout
参数”; -
强调操作原子性:重建过程需保证Channel事件丢失。