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

高并发网络通信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事件丢失。

http://www.lqws.cn/news/448039.html

相关文章:

  • 【LUT技术专题】采样间隔自适应3DLUT-AdaInt
  • STM32 HAL 初始化I2C启动DS1307
  • 第1章: 伯努利模型的极大似然估计与贝叶斯估计
  • apisix-使用hmac-auth插件进行接口签名身份验证\apisix consumer
  • 机构运动分析系统开发(Python实现)
  • 工程师生活:清除电热水壶(锅)水垢方法
  • 前端HTML学习笔记
  • 《思维力:高效的系统思维》
  • python画三维立体图
  • 【支持向量机】SVM线性可分支持向量机学习算法——硬间隔最大化支持向量机及例题详解
  • 打卡Day55
  • 【lenovo】LEGION 2020款跳过windows账号登录
  • 12.10 在主线程或子线程中更新 UI
  • 退出python解释器的四种方式
  • C#语言入门-task2 :C# 语言的基本语法结构
  • vue3 defineExpose的使用
  • 不同系统修改 Docker Desktop 存储路径(从C盘修改到D盘)
  • 解锁微服务潜能:深入浅出 Nacos
  • 蜻蜓Q系统的技术演进:从Laravel 6到Laravel 8的升级之路-优雅草卓伊凡
  • C# winform教程(二)----GroupBox
  • ROS学习之服务通信
  • 【AI编程】第3期,针对AI生成的改枪码列表创建对应的数据库表
  • AWS CloudFormation深度解析:构建现代云原生应用基础设施
  • SpringBoot扩展——发送邮件!
  • Spring MVC参数绑定终极手册:单多参对象集合JSON文件上传精讲
  • 网络编程及原理(五)
  • Kotlin 2.6 猜数小游戏
  • AI免费工具:promptpilot、今天学点啥、中英文翻译
  • WSL 安装使用和常用命令
  • 设计模式精讲 Day 7:桥接模式(Bridge Pattern)