Selector空轮询bug的原因?
异常事件未被正确处理:当Socket连接突然终止时,epoll会返回POLLHUP或POLLERR事件,但JDK的SelectionKey未定义对应的异常事件类型,导致上层无法感知这些异常,selectedKeys()始终为空。
无效Key残留:若Channel关闭后未及时取消关联的SelectionKey,旧Selector仍会持有无效Key,select方法返回0,导致后续轮询时反复触发空轮询。
while (true) {
int readyChannels = selector.select(); // 阻塞等待事件
if (readyChannels == 0) {
// 空轮询:无事件就绪,但未触发唤醒或超时
continue; // 死循环
}
}
若Selector的轮询结果为空,也没有wakeUp或新消息处理,则发生空轮询,死循环bug,cpu使用率100%。
Netty解决步骤
Select通过超时时间机制实现定时阻塞,每次selectCnt++。
有效IO事件进行正常处理。
如果查询超时,selectCnt重置为1。
解决空轮询BUG,一旦达到阈值512,重建selector,重新注册channel通道。
Netty解决方案核心代码
public final class NioEventLoop extends SingleThreadEventLoop {
private void select(boolean oldWakenUp) throws IOException {
Selector selector = this.selector;
try {
int selectCnt = 0;
long currentTimeNanos = System.nanoTime();
// 本次select的阻塞时间 = 当前时间 + 超时时间
long selectDeadLineNanos = currentTimeNanos + this.delayNanos(currentTimeNanos);
while (true) {
// 计算当前时间与目标截止时间的剩余时间 500000L是0.5ms 为了向上取整
long timeoutMillis = (selectDeadLineNanos - currentTimeNanos + 500000L) / 1000000L;
// 剩余时间为0,表示已经有定时任务快要超时
if (timeoutMillis <= 0L) {
// 如果是第一次循环(selectCnt=0),则调用一次selector.selectNow
if (selectCnt == 0) {
// 处理网络事件
selector.selectNow();
selectCnt = 1;
}
break;
}
// 如果没有定时任务超时,但是有以前注册的任务
// 且成功设置wakenUp为true,则调用selectNow并返回
if (this.hasTasks() && this.wakenUp.compareAndSet(false, true)) {
selector.selectNow(); // 尝试立即获取就绪事件
selectCnt = 1;
break;
}
//调用select方法,阻塞时间为上面算出的最近一个将要超时的定时任务时间
int selectedKeys = selector.select(timeoutMillis);
++selectCnt;
// 正常场景
if (selectedKeys != 0 || oldWakenUp || this.wakenUp.get() || this.hasTasks() || this.hasScheduledTasks()) {
// selectedKeys != 0: selectedKeys个数不为0, 有io事件发生
// oldWakenUp:表示进来时,已经有其他地方对selector进行了唤醒操作
// wakenUp.get():表示selector被唤醒
// hasTasks() || hasScheduledTasks():表示有任务或定时任务要执行
break;
}
//如果线程被中断,计数器置零,直接返回
if (Thread.interrupted()) {
selectCnt = 1;
break;
}
// 判断select返回是否是因为计算的超时时间已过,
// 这种情况下也属于正常返回,计数器置1,进入下次循环
long time = System.nanoTime();
if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) {
// 进入这个分支,表示超时,属于正常的场景
// 说明发生过一次阻塞式轮询, 并且超时
selectCnt = 1;
} else if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 && selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {
// 异常场景,没有超时,同时selectedKeys==0
// 启用了select bug修复机制
// 即配置的io.netty.selectorAutoRebuildThreshold参数大于3,
// select方法返回次数已经大于配置的阈值,默认512
// 512是经过大量测试和工程实践确定的平衡点
// 既能有效避免误判(如短暂空轮询),又能及时响应持续的空轮询问题
// 进行selector重建,重建完之后,尝试调用非阻塞版本select一次,并直接返回
selector = this.selectRebuildSelector(selectCnt);
selectCnt = 1;
break;
}
currentTimeNanos = time;
}
// 关闭select bug修复机制 打印日志提示
if (selectCnt > 3 && logger.isDebugEnabled()) {
logger.debug(...);
}
} catch (CancelledKeyException var13) {
...
}
}
private Selector selectRebuildSelector(int selectCnt) throws IOException {
// 进行selector重建
this.rebuildSelector();
Selector selector = this.selector;
// 重建完之后,尝试调用非阻塞版本select一次,并直接返回
selector.selectNow();
return selector;
}
}
public final class NioEventLoop extends SingleThreadEventLoop {
public void rebuildSelector() {
// 如果不在该线程中,则放到任务队列中
if (!this.inEventLoop()) {
this.execute(new Runnable() {
public void run() {
NioEventLoop.this.rebuildSelector0();
}
});
} else {
// 调用实际重建方法
this.rebuildSelector0();
}
}
private void rebuildSelector0() {
Selector oldSelector = this.selector;
// 如果旧的selector为空,则直接返回
if (oldSelector != null) {
NioEventLoop.SelectorTuple newSelectorTuple;
try {
// 新建一个新的selector
// 调用openSelector()创建原生epoll实例,避免旧Selector的残留状态影响新实例
newSelectorTuple = this.openSelector();
} catch (Exception var9) {
logger.warn("Failed to create a new Selector.", var9);
return;
}
int nChannels = 0;
Iterator var4 = oldSelector.keys().iterator();
//对于注册在旧selector上的所有key,依次重新在新建的selecor上重新注册一遍
while (var4.hasNext()) {
SelectionKey key = (SelectionKey) var4.next();
Object a = key.attachment();
try {
if (key.isValid() && key.channel().keyFor(newSelectorTuple.unwrappedSelector) == null) {
int interestOps = key.interestOps();
key.cancel();
SelectionKey newKey = key.channel().register(newSelectorTuple.unwrappedSelector, interestOps, a);
if (a instanceof AbstractNioChannel) {
((AbstractNioChannel) a).selectionKey = newKey;
}
++nChannels;
}
} catch (Exception var11) {
logger.warn("Failed to re-register a Channel to the new Selector.", var11);
if (a instanceof AbstractNioChannel) {
AbstractNioChannel ch = (AbstractNioChannel) a;
ch.unsafe().close(ch.unsafe().voidPromise());
} else {
NioTask<SelectableChannel> task = (NioTask) a;
invokeChannelUnregistered(task, key, var11);
}
}
}
// 将该NioEventLoop关联的selector赋值为新建的selector
this.selector = newSelectorTuple.selector;
this.unwrappedSelector = newSelectorTuple.unwrappedSelector;
try {
//关闭旧的selector
oldSelector.close();
} catch (Throwable var10) {
if (logger.isWarnEnabled()) {
logger.warn("Failed to close the old Selector.", var10);
}
}
}
}
}
}
Netty解决策略总结
重建selector:新Selector的selectedKeys()集合为空,强制下一次轮询必须依赖真实事件或阻塞,避免旧Selector因残留Key持续触发空轮询。
防御性机制:通过重建隔离异常连接,避免因单个连接故障导致整个IO线程阻塞,保障系统稳定性。
性能优化:Netty维护双Selector(优化版与原生版),重建时优先使用优化版提升遍历效率。
基于JDK NIO的Selector类,其内部使用HashSet存储就绪的SelectionKey。Netty优化为BitMap位图SelectedSelectionKeySet,提升遍历效率,显著提升事件处理效率。这一优化直接解决了JDK原生Selector在高负载下的性能问题。