netty4核心源码分析第五篇一核心篇NioEventLoop.run详解

前言:基于netty4.0源码分析,不同版本差异较大,大家主要关注select和epoll模型,reactor工作方式,netty与javachannel的衔接等知识点

文章目录

  • Reactor执行过程
    • 原理图
    • 源码分析一NioEventLoop.run
  • selector.selectNow
    • 原理图
    • 源码分析一selectNow
  • NioEventLoop.select
    • 原理图
    • 源码分析一select(wakeup)
  • processSelectedKeys之IO事件处理
    • 原理图
    • 源码分析一processSelectedKeys
  • runAllTasks
    • 原理图
    • 源码分析一runAllTasks
      • fetchFromScheduledTaskQueue
  • 总结
  • 扩展点一wakeUp机制与问题
  • 扩展点一rebuildSelector

Reactor执行过程

原理图

  • 检测队列是否有任务,选择selector不同API
  • 存在任务选择selector.selectNow
  • 不存在任务执行select(wakeup)
  • IO选择器执行选择后,优先处理发生IO事件的selectKeys
  • 随后根据ioRatio比例决定普通任务队列的执行时间,然后执行runAllTasks
  • 如果现在state改变,必要时关闭所有channel通道,停止线程
    netty4核心源码分析第五篇一核心篇NioEventLoop.run详解_第1张图片

源码分析一NioEventLoop.run

  • step-1: hasTask 执行selector.selectNow,没有任务执行select(wakenUp)
  • step-2: 没有任务则执行NioEventLoop.select
  • step-3: selector.wakeup防止并发问题
  • step-4: 如果io处理时间比例为100%则先处理IO事件,后处理非IO事件
  • step-6.2: 计算io事件处理时长 根据ioRatio计算队列普通任务的处理时长并执行runAllTasks
  • step-7: 关闭netty
class NioEventLoop{
    protected void run() {
        自旋处理
        for (;;) {
            try {
                step-1: hasTask 执行selector.selectNow,没有任务执行select(wakenUp)
                switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) {
                    case SelectStrategy.CONTINUE:
                        continue;
                    case SelectStrategy.SELECT: 
                        step-2: 没有任务则执行NioEventLoop.select
                        select(wakenUp.getAndSet(false));
                        step-3: selector.wakeup防止并发问题
                        if (wakenUp.get()) {
                            selector.wakeup();selector未执行,执行了wakeup,下次执行selector.select(time)不会阻塞
                        }
                    default:
                }
                cancelledKeys = 0;
                needsToSelectAgain = false;
                final int ioRatio = this.ioRatio;
                step-4: 如果io处理时间比例为100%
                if (ioRatio == 100) {
                    try {
                        step-5.1: 处理io事件
                        processSelectedKeys();
                    } finally {
                        step-6.1: 处理加入taskQueue和scheduledTaskQueue中的任务
                        runAllTasks();
                    }
                } else {
                    step-5.2: 处理io事件
                    final long ioStartTime = System.nanoTime();
                    try {
                        processSelectedKeys();
                    } finally {
                        step-6.2: 计算io事件处理时长 根据ioRatio计算队列普通任务的处理时长并执行runAllTasks
                        final long ioTime = System.nanoTime() - ioStartTime;
                        runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
                    }
                }
            } catch (Throwable t) {
                handleLoopException(t);
            }
            step-7: 关闭netty            
            try {
                if (isShuttingDown()) {
                    closeAll();
                    if (confirmShutdown()) {
                        return;
                    }
                }
            } catch (Throwable t) {
                handleLoopException(t);
            }
        }
    }
}

selector.selectNow

原理图

前文介绍nio的selectionKeys迭代每一个key处理完后必须执行remove,
这里使用包装类屏蔽了相关nio易错点

  • 使用优化后的选择器执行select
  • 清空selectionKeys.reset并调用目标类SelectorImpl
  • SelectorImpl调用lockAndDoSelect(0)表示无延时
  • 通过doSelect调用JNI方法KQueueArrayWrapper.kevent0完成IO多路复用检查
  • 在doSelect中调用updateSelectedKeys更新selectKey集合
    netty4核心源码分析第五篇一核心篇NioEventLoop.run详解_第2张图片

源码分析一selectNow

@Override
public int selectNow() throws IOException {
    清空selectionKeys
    selectionKeys.reset();
    return delegate.selectNow();
}
public int selectNow() throws IOException {
   return this.lockAndDoSelect(0L);
}
  • 加锁调用doSelect
private int lockAndDoSelect(long var1) throws IOException {
    synchronized(this) {
        if (!this.isOpen()) {
            throw new ClosedSelectorException();
        } else {
            int var10000;
            synchronized(this.publicKeys) {
                synchronized(this.publicSelectedKeys) {
                    var10000 = this.doSelect(var1);
                }
            }

            return var10000;
        }
    }
}
  • 调用kqueueWrapper实现多路复用选择
  • 更新selectKeys集合
protected int doSelect(long var1) throws IOException {
    boolean var3 = false;
    if (this.closed) {
        throw new ClosedSelectorException();
    } else {
        this.processDeregisterQueue();

        int var7;
        try {
            this.begin();
            调用kqueueWrapper实现多路复用选择
            var7 = this.kqueueWrapper.poll(var1);
        } finally {
            this.end();
        }
        this.processDeregisterQueue();
        更新selectKeys集合
        return this.updateSelectedKeys(var7);
    }
}
  • 通过本地方法获取所有被激活的selectKey[原理类似前文epoll底层实现]
int poll(long var1) {
    this.updateRegistrations();
    int var3 = this.kevent0(this.kq, this.keventArrayAddress, 128, var1);
    return var3;
}

private native int kevent0(int var1, long var2, int var4, long var5);

NioEventLoop.select

携带可唤醒机制的select,核心方法,下图较为复杂,了解其核心思想

  • 核心思想:
如果有任务,则期望不要阻塞在select方法上
如果没有任务,则期望阻塞在select方法上
怎么控制阻塞,通过wakeUp变量控制
wakeUp为true表示唤醒期望不阻塞
NioEventLoop添加任务,调用execute,则wakeup被置为true

原理图

netty4核心源码分析第五篇一核心篇NioEventLoop.run详解_第3张图片

源码分析一select(wakeup)

  • delayNanos根据延时任务计算自旋等待超时时间
  • 有任务则执行selectNow并返回
  • 检查hasTasks和wakeup标记防止并发导致有任务却阻塞在select(timeout)上
  • selector.select(timeoutMillis)阻塞检测io事件
  • 再次检测是否存在任务或者wakeup唤醒
  • 超过超时时间说明本轮selectCnt没问题,无需rebuildSelector,将selectCnt置为一
  • 未超过超时时间但是select超过512次,则重建selector避免cpu 100%问题
private void select(boolean oldWakenUp) throws IOException {
        Selector selector = this.selector;
    try {
        表示执行过多少次select
        int selectCnt = 0;
        long currentTimeNanos = System.nanoTime();
        没有定时任务返回1秒
        有定时任务任务未到执行时间,返回任务还需要等待多久的执行时间
        定时任务到期,返回0
        long selectDeadLineNanos = currentTimeNanos + delayNanos(currentTimeNanos);
        for (;;) {
            long timeoutMillis = (selectDeadLineNanos - currentTimeNanos + 500000L/* 数学修正 超过半毫秒当做有定时任务 */) / 1000000L;
            if (timeoutMillis <= 0) { 无定时任务或者自旋到timeoutMillis小于0
                if (selectCnt == 0) { 没有自旋选择过
                    io事件处理完 立马处理taskscheduledTask
                    selector.selectNow();
                    selectCnt = 1;
                }
                没有select过则执行一次select,然后break,去处理scheduledTask
                有select过则selectCnt>0,直接中断
                currentTimeNanos更新到小于0
                break;
            }

            if (hasTasks() && wakenUp.compareAndSet(false, true)) {
                1 任务提交时正常都会将wakenUp设置成true,执行唤醒Selector.wakeup
                2 但是wakenUp设置成true是cas操作,要求wakeUp前置状态必须是false
                3 这就造成并发提交任务时,由于wakeUp值没有更新,也就没有触发Selector.wakeup
                4 则在这里二次检查有任务 执行selector.selectNow 而不是含有阻塞的selector.select(timeoutMillis)
                selector.selectNow();
                selectCnt = 1;
                break;
            }
            空轮询bug:    selectedKeys= 0 并且不会发生timeoutMillis的wait 查询后立马返回
            wakeup=true并且执行了selector.wakeup 则这里不会阻塞
            int selectedKeys = selector.select(timeoutMillis);
            选择器选择次数增加
            selectCnt ++;

            if (selectedKeys != 0 || oldWakenUp /* 需要唤醒则break */|| wakenUp.get()/*外部唤醒 比如关闭或者提交任务*/ || hasTasks() || hasScheduledTasks()) {
                break;
            }
            if (Thread.interrupted()) {
                ...... 删除日志代码
                selectCnt = 1;
                break;
            }

            long time = System.nanoTime();
            // currentTimeNanos------------------timeoutMillis时间
            超过了超时时间则selectCnt=1打破重建selector的关键
            if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) {
                selectCnt = 1;
            } else 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);
                如果select超过512,netty认为空轮询bug发生,重建selector
                rebuildSelector();
                selector = this.selector;

                selector.selectNow();
                selectCnt = 1;
                break;
            }

            currentTimeNanos = time;
        }

        ...... 删除日志代码
    } catch (CancelledKeyException e) {
        ...... 删除日志代码
    }
}

processSelectedKeys之IO事件处理

原理图

netty4核心源码分析第五篇一核心篇NioEventLoop.run详解_第4张图片

源码分析一processSelectedKeys

private void processSelectedKeys() {
    if (selectedKeys != null) {
        处理优化过的select数据结构[优化包括数组代替set提高遍历性能等]
        processSelectedKeysOptimized();
    } else {
        processSelectedKeysPlain(selector.selectedKeys());
    }
}
  • 遍历处理SelectedKey事件
private void processSelectedKeysOptimized() {
    for (int i = 0; i < selectedKeys.size; ++i) {
        final SelectionKey k = selectedKeys.keys[i];
        selectedKeys.keys[i] = null;
        NETTY的 NioSocketChannel或者NioServerSocketChannel
        final Object a = k.attachment();
        遍历处理
        processSelectedKey(k, (AbstractNioChannel) a);
        ...... 删除其他代码
        
    }
}
  • 根据事件类型分发unsafe处理
  • 再往下就是pipeline.fireXXX机制[进入netty业务接口层]
  • 如果是服务端ACCEPT事件,其会触发客户端Channel的handler初始化,注册selector等工作
private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
        final NioUnsafe unsafe = ch.unsafe();
    ...... 删除其他代码

    int readyOps = k.readyOps();
    if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
        int ops = k.interestOps();
        ops &= ~SelectionKey.OP_CONNECT;
        k.interestOps(ops);
        底层调用firechannelActive
        unsafe.finishConnect();
    }
    if ((readyOps & SelectionKey.OP_WRITE) != 0) {
        将netty缓冲区写入javachannel缓冲区
        ch.unsafe().forceFlush();
    }
    if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
        处理客户端读事件或者服务端ACCEPT事件
        客户端NioByteUnsafe 服务端NioMessageUnsafe
        unsafe.read();
    }
}

runAllTasks

原理图

netty4核心源码分析第五篇一核心篇NioEventLoop.run详解_第5张图片

源码分析一runAllTasks

  • scheduledTaskQueue队列任务转移到taskQueue
  • 判断taskQueue是否存在任务,不存在直接返回
  • 计算任务执行到何时被打破
  • 遍历处理任务
  • 每运行64个任务检测下是否超时,超时中断自旋
  • 有任务继续自旋
protected boolean runAllTasks(long timeoutNanos) {
    scheduledTaskQueue队列任务转移到taskQueue
    fetchFromScheduledTaskQueue();
    判断taskQueue是否存在任务
    Runnable task = pollTask();
    if (task == null) {
        return false;
    }
    计算任务执行到何时被打破
    final long deadline = ScheduledFutureTask.nanoTime() + timeoutNanos;
    long runTasks = 0;
    long lastExecutionTime;
    遍历处理任务
    for (;;) {
        task.run();
        runTasks ++;
        每运行64个任务检测下是否超时
        if ((runTasks & 0x3F) == 0) {
            lastExecutionTime = ScheduledFutureTask.nanoTime();
            超时中断任务执行
            if (lastExecutionTime >= deadline) {
                break;
            }
        }
        无任务中断执行,有任务继续自旋
        task = pollTask();
        if (task == null) {
            lastExecutionTime = ScheduledFutureTask.nanoTime();
            break;
        }
    }
    this.lastExecutionTime = lastExecutionTime;
    return true;
}

fetchFromScheduledTaskQueue

  • 将scheduledTaskQueue队列中到达处理时间的延迟任务转移到taskQueue
private boolean fetchFromScheduledTaskQueue() {
    Runnable scheduledTask  = pollScheduledTask(nanoTime);
    while (scheduledTask != null) {
        if (!taskQueue.offer(scheduledTask)) {
            scheduledTaskQueue().add((ScheduledFutureTask<?>) scheduledTask);
            return false;
        }
        scheduledTask  = pollScheduledTask(nanoTime);
    }
    return true;
}

 protected final Runnable pollScheduledTask(long nanoTime) {
        assert inEventLoop();
    Queue<ScheduledFutureTask<?>> scheduledTaskQueue = this.scheduledTaskQueue;
    ScheduledFutureTask<?> scheduledTask = scheduledTaskQueue == null ? null : scheduledTaskQueue.peek();
    if (scheduledTask == null) {
        return null;
    }
	到达处理时间
    if (scheduledTask.deadlineNanos() <= nanoTime) {
        scheduledTaskQueue.remove();
        return scheduledTask;
    }
    return null;
}

总结

  • 介绍reactor模型的落地,NioEventLoop.run
  • 介绍select(wakeup)选择机制以及processSelectedKeys对io事件处理,runAllTasks处理队列中的普通事件
  • EpollEventLoop与NioEventLoop是完全不同的实现,但netty通过eventLoop等抽象类的设计屏蔽了底层差异性,使业务开发只需关注Handler以及编解码等工作

扩展点一wakeUp机制与问题

  • wakeup是为了确保select时候不阻塞,到wakeup=true执行selectNow
  • 或者selector.wakeup唤醒selector.select(timeoutMillis)
  • 但存在一个问题就是wakeup设置成true是个并发cas操作,比如其他线程大量提交task[提交task会执行wakeup = true的cas]
  • 此时可能出现并发导致未执行 selector.selectNow而是selector.select(timeoutMillis)
  • 同时wakenUp.compareAndSet(false, true)失败导致selector.wakeup未执行
  • 造成wakeUp虽然是true但是但是阻塞在selector.select(timeoutMillis)

为解决wakeup为true但依旧select挂起现象,采用以下方式

 if (wakenUp.get()) {
     // 如果在下次执行select(wakeup)有可能会走到selector.select(timeoutMillis),为确保wakeup=true不应该阻塞(并发干扰),这里在每次自旋前执行selector.wakeup
     selector.wakeup();1 selector阻塞中会被唤醒  2 selector未执行,执行了wakeup,下次执行select.select(time)会被唤醒,不会阻塞
 }
 //

扩展点一rebuildSelector

Q: cpu100%
A: 操作系统层面bug导致 selector.select(timeoutMillis) 返回0也不会阻塞,
造成select(wakeup)方法空转 导致cpu!00%,通过重建selector解决

netty4核心源码分析第五篇一核心篇NioEventLoop.run详解_第6张图片

private void rebuildSelector0() {
    final Selector oldSelector = selector;
    final SelectorTuple newSelectorTuple;
    int nChannels = 0;
    获取所有注册的key和其附件上的netty channel对象
    for (SelectionKey key: oldSelector.keys()) {
        NioServerSocketChannel或者 NioSocketChannel
        Object a = key.attachment();
        ...... 删除其他代码
        int interestOps = key.interestOps();
        key.cancel();
        重新注册
        SelectionKey newKey = key.channel().register(newSelectorTuple.unwrappedSelector, interestOps, a);
        if (a instanceof AbstractNioChannel) {
            更新channel的selectKey 相互持有
            ((AbstractNioChannel) a).selectionKey = newKey;
        }
        ...... 删除其他代码
    }
    ...... 删除其他代码
    关闭老的选择器
    oldSelector.close();

}

你可能感兴趣的:(netty4源码分析,netty4,netty)