Netty如何解决空轮询BUG

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在高负载下的性能问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值