前面聊过了逻辑上下文和线程上下文, 其中Pipeline代表了逻辑上下文的组织, 本篇咱们聊聊Netty的线程上下文组织, EventLoop&EventLoopGroup的。首先回顾下Netty的整体框架。
关于该框架咱们应该知道几个点:
接下来进入NioEventLoop内部。
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 |
系统调用 | 调用效果 |
---|---|
epoll_create | 创建eventpoll对象, 其中包含关联等待线程队列和rdlist |
epoll_ctl | 将eventpoll对象引用加入到所有的socket中 |
epoll_wait | 将线程加入epoll对象的等待队列中。当网卡数据到达中断产生时, 将socket加入到rdlist中, 同时唤醒eventpoll对象上关联的等待队列中的线程 |
每次执行 select 操作之前记录当前时间 currentTimeNanos。如果事件轮询的持续时间大于等于 timeoutMillis,那么说明是正常的,否则表明阻塞时间并未达到预期,可能触发了空轮询的 Bug。Netty 引入了计数变量 selectCnt。在正常情况下,selectCnt 会重置,否则会对 selectCnt 自增计数。当 selectCnt 达到 SELECTOR_AUTO_REBUILD_THRESHOLD(默认512) 阈值时,会触发重建 Selector 对象。
如果开启优化, selectKey的集合类会被Netty替换为自定义的集合类SelectedSelectionKeySet。默认的集合类是HashSet实际基于HashMap实现, 而Netty SelectedSelectionKeySet基于数组实现, 读效率比前者高。
处理IO事件, 包括读事件、ACCEPT事件、写事件和OP_CONNECT事件:
CONNECT事件:调用finishConnect函数完成connection。
WRITE事件:正常情况下一般是不会注册写事件的,如果Socket发送缓冲区中没有空闲内存时,在写入会导致阻塞,此时可以注册写事件,当有空闲内存(或者可用字节数大于等于其低水位标记)时,再响应写事件,并触发对应回调。
READ事件:从channel中读取数据,存放到byteBuf中,循环调用对应pipeline的fireChannelRead通知Pipeline处理数据, 如果没有新的数据触发fireChannelReadComplete。
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 中任务后会去获取尾部队列中任务执行。尾部任务并不常用,主要用于做一些收尾工作,例如统计事件循环的执行时间、监控信息上报等。
与平时通过线程池来消费多任务的方式不同, EventLoop中仅有一个线程来消费队列中的任务。
首先, 得益于epoll这种高级的内核函数, 用户侧的线程可以用一个线程来监控所有的socket, 加上socket读写对CPU本身消耗非常低, 因此EventLoop非常适合网络场景。然而抛开业务本身, 单线程减少了线程调度, 线程间数据访问同步的开销, 以及由此带来的复杂性。在Cache层面, 单线程的命中率会更高。因此在日常的工程中也可以作为一种任务处理方式。当然, 这里的任务是轻量级的, 无阻塞的或者阻塞时间非常端的任务。此时, 我们需要除了网络IO外的一个EventLoop, 此时可以使用DefaultEventExecutor和DefaultEventExecutorGroup。典型的场景是Pipeline中做业务处理。
以上我们聊了下EventLoop相关类的层级结构、任务调度和IO处理, 其中IO处理包括连接和数据读写。最后, 个人聊了下EventLoop的在非网络场景下使用的思考。