EventLoopGroup&EventLoop

系列文章目录

1. Netty网络应用基础
2. Java I/O
3. IO/模型
4. 网络应用编解码
5. Netty Pipeline
6. Netty EventLoopGroup&EventLoop
7. Netty ThreadLocal&FastThreadLocal
8. Netty Future&Promise
9. Netty内存管理–(旧)PoolChunk&伙伴分配
10. Netty内存管理–内存池空间规格化SizeClasses
11. Netty内存管理–PoolChunk&PoolSubPage
12. Netty内存管理–内存池PoolArena
13. Netty内存管理–内存分配器PooledByteBufAllocator
14. Netty ObjectPool

写在前面

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

  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层面的系统调用支持有关。

\selectpollepoll
底层实现数组链表哈希表
最大连接数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#taskQueueAbstractScheduledEventExecutor#priorityQueueSingleThreadEventLoop#tailTasks
任务类型普通任务计划任务统计/监控类任务
队列间优先级123
队列内优先级FIFOdeadline based priorityFIFO
调度方式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的在非网络场景下使用的思考。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值