EventLoopGroup&EventLoop

写在前面

前面聊过了逻辑上下文和线程上下文, 其中Pipeline代表了逻辑上下文的组织, 本篇咱们聊聊Netty的线程上下文组织, EventLoop&EventLoopGroup的。首先回顾下Netty的整体框架。
EventLoopGroup&EventLoop_第1张图片
关于该框架咱们应该知道几个点:

  1. BossEventLoop负责处理连接并创建channel, WorkEventLoop负责处理channel的IO任务处理;
  2. 1个EventLoopGroup对应多个EventLoop;
  3. 1个EventLoop上面register了多个Channel, 考虑到Channel关联了对应的Pipeline, 进而关联了多个Pipeline;
  4. 在channel注册到EventLoop之后, 理论上对channel以及pipeline等对象的更新都应该由EventLoop去处理;
  5. 由于EventLoop内部仅有一个线程, 因此耗时阻塞性任务不适合在EventLoop中直接处理;

接下来进入NioEventLoop内部。

1. select实现

1.1 系统调用支持

Java中select的实现与OS层面的系统调用支持有关。

\ select poll epoll
底层实现 数组 链表 哈希表
最大连接数 1024(x86)或 2048(x64) 无上限 无上限
ready识别方式 线性遍历 线性遍历 事件回调
ready识别时间复杂度 O(n) O(n) O(1)
ready fd处理 每次调用,把fd集合从用户态拷贝到内核态 每次调用,把fd集合从用户态拷贝到内核态 调用epoll_ctl时拷贝进内核并保存,之后每次epoll_wait不拷贝
Kernel 要求 >=2.4 >=2.4 >=2.6

1.2 epoll过程

系统调用 调用效果
epoll_create 创建eventpoll对象, 其中包含关联等待线程队列和rdlist
epoll_ctl 将eventpoll对象引用加入到所有的socket中
epoll_wait 将线程加入epoll对象的等待队列中。当网卡数据到达中断产生时, 将socket加入到rdlist中, 同时唤醒eventpoll对象上关联的等待队列中的线程

1.3 空轮训

每次执行 select 操作之前记录当前时间 currentTimeNanos。如果事件轮询的持续时间大于等于 timeoutMillis,那么说明是正常的,否则表明阻塞时间并未达到预期,可能触发了空轮询的 Bug。Netty 引入了计数变量 selectCnt。在正常情况下,selectCnt 会重置,否则会对 selectCnt 自增计数。当 selectCnt 达到 SELECTOR_AUTO_REBUILD_THRESHOLD(默认512) 阈值时,会触发重建 Selector 对象。

2. IO处理

2.1 selectedKey优化

如果开启优化, selectKey的集合类会被Netty替换为自定义的集合类SelectedSelectionKeySet。默认的集合类是HashSet实际基于HashMap实现, 而Netty SelectedSelectionKeySet基于数组实现, 读效率比前者高。

2.2 processSelectedKey

处理IO事件, 包括读事件、ACCEPT事件、写事件和OP_CONNECT事件:
CONNECT事件:调用finishConnect函数完成connection。
WRITE事件:正常情况下一般是不会注册写事件的,如果Socket发送缓冲区中没有空闲内存时,在写入会导致阻塞,此时可以注册写事件,当有空闲内存(或者可用字节数大于等于其低水位标记)时,再响应写事件,并触发对应回调。
READ事件:从channel中读取数据,存放到byteBuf中,循环调用对应pipeline的fireChannelRead通知Pipeline处理数据, 如果没有新的数据触发fireChannelReadComplete。

3. 任务调度

NioEventLoop的主要父类有SingleThreadEventLoop、SingleThreadEventExecutor和AbstractScheduledEventExecutor。父类内部都定义了任务队列, 这就需要做多优先级的任务调度, 调度策略如下。

因素 SingleThreadEventExecutor#taskQueue AbstractScheduledEventExecutor#priorityQueue SingleThreadEventLoop#tailTasks
任务类型 普通任务 计划任务 统计/监控类任务
队列间优先级 1 2 3
队列内优先级 FIFO deadline based priority FIFO
调度方式 FIFO 移动到taskQueue中 直接处理

a. 普通任务
通过调用execute()向任务队列 taskQueue 中添加任务。例如 Netty 在写数据时会封装 WriteAndFlushTask 提交给 taskQueue。taskQueue 的实现类是多生产者单消费者队列 MpscChunkedArrayQueue,在多线程并发添加任务时,可以保证线程安全。

b. 定时任务
通过调用schedule()向定时任务队列 scheduledTaskQueue 添加一个定时任务,用于周期性执行该任务。例如,心跳消息发送等。

c. 尾部队列
tailTasks 相比于普通任务队列优先级较低,在每次执行完 taskQueue 中任务后会去获取尾部队列中任务执行。尾部任务并不常用,主要用于做一些收尾工作,例如统计事件循环的执行时间、监控信息上报等。

4. EventLoopGroup

  1. 创建/销毁EventLoop;
  2. 基于EventLoopChooser choose EventLoop, channel register时就会触发该过程;
  3. 作为EventLoop的Context, 由EventLoop持有索引。类似于在ChannelHanderContext中持有对Pipeline的引用,通过fireChannelRead推动数据进入下一个ChannelHandlerContext;

5. EventLoop应用

与平时通过线程池来消费多任务的方式不同, EventLoop中仅有一个线程来消费队列中的任务。
首先, 得益于epoll这种高级的内核函数, 用户侧的线程可以用一个线程来监控所有的socket, 加上socket读写对CPU本身消耗非常低, 因此EventLoop非常适合网络场景。然而抛开业务本身, 单线程减少了线程调度, 线程间数据访问同步的开销, 以及由此带来的复杂性。在Cache层面, 单线程的命中率会更高。因此在日常的工程中也可以作为一种任务处理方式。当然, 这里的任务是轻量级的, 无阻塞的或者阻塞时间非常端的任务。此时, 我们需要除了网络IO外的一个EventLoop, 此时可以使用DefaultEventExecutor和DefaultEventExecutorGroup。典型的场景是Pipeline中做业务处理。

小结

以上我们聊了下EventLoop相关类的层级结构、任务调度和IO处理, 其中IO处理包括连接和数据读写。最后, 个人聊了下EventLoop的在非网络场景下使用的思考。

你可能感兴趣的:(Netty网络应用,java,开发语言,Netty)