EventLoop和EventLoopGroup

1、Netty的线程模型
Reactor单线程模型

指所有的I/O操作都在同一个NIO线程上面完成。NIO线程的职责如下:
①作为NIO服务端,接受客户端的TCP链接
②作为NIO客户端,向服务端发起TCP连接
③读取通信对端的请求或者应答消息
④向通信对端发送消息请求或者应答消息
不适用高负载、大并发的场景:
①一个NIO线程同时处理成百上千的链路,性能上无法支撑,即便NIO线程的CPU符合导到100%,也无法满足海量消息编码、解码、读取和发送
②当NIO线程负载过重之后,处理速度变慢,导致大量客户端连接超时,超时之后往往会重发,这更加重勒NIO线程的负载,最终会导致大量消息积压和处理超时,称为系统的性能瓶颈
③可靠性问题。

Reactor多线程模型

有一组NIO线程来处理I/O操作。Reactor多线程模型的特点:
①有专门一个NIO线程-Acceptor线程用于监听服务端,接受客户端的TCP链接请求
②网络I/O操作——读、写等由一个NIO线程池负责
③一个NIO线程可以同时处理N调链路,但是一个链路值对应一个NIO线程,防止发生并发

主从Reactor多线程模型

服务端用于接受客户端的不再是一个单独的NIO线程,而是一个独立的NIO线程池。Acceptor接收到客户端TCP连接请求并处理完成后,将新创建的SocketChannel注册到I/O线程池的某个I/O线程上,由它负责SocketChannel的读写和编解码工作。Acceptor线程池仅仅用于客户端的登录、握手和完全认证,一旦链路建立成功,就将链路注册到后端subReactor线程池的I/O线程上,由I/O线程负责后续额I/O操作。

Netty的线程模型

Netty的线程模型取决于启动参数配置。通过设置不同的启动参数,Netty可以同时支持Reactor单线程模型、到线程模型和主从Reactor多线程模型。

Netty用于接受客户端请求的线程池职责:
①接受客户端TCP连接,初始化Channel参数
②将链路状态变更时间通知给ChannelPipeline

Netty处理I/O操作的Reactor线程池职责:
①异步读取通信对端的数据报,发送读事件到ChannelPipeline
②异步发送消息到通信对端,调用ChannelPipeline的消息发送接口
③执行系统调用Task
④执行定时任务Task

实践
1)创建两个NioEventLoopGroup,用于逻辑隔离NIO Acceptor和NIO I/O线程
2)尽量不要再ChannelHandler中启动用户线程
3)解码要放在NIO线程调用的解码Handler中进行,不要切换到用户线程中
4)如果业务逻辑允许,可以直接在NIO线程上完成业务逻辑编排,不需要切换到用户线程
5)业务逻辑复杂,不要再NIO线程上完成

线程数量计算公式
①线程数量=(线程总时间/瓶颈资源时间)瓶颈资源的线程并行数
②QPS=1000/想成总时间
线程数

2、NioEventLoop设计及类继承关系
EventLoop和EventLoopGroup
实现了EventLoop接口、EventExecutorGroup接口、ScheduledExecutorService接口。
EventLoop和EventLoopGroup

3、NioEventLoop源码分析
1)定义Selector并初始化:Selector openSelector()
2) run()方法:
①当前的消息队列里有尚未处理的消息,调用 selectNow()
Selector的selectNow方法会立即触发Selector的选择操作。如果有准备就绪的Channel,则返回就绪的Channel的集合,否则返回0.选择完成之后,再次判断用户是否调用了Selector的wakeup方法,如果调用,则执行selector.wakeup操作。
②当前的消息队列里没有尚未处理的消息,调用select(oldWakenUp),又Selector多路复用器轮询,看是否有准备就绪的Channel。
取当前系统时间,调用delayNanos方法计算获得NioEventLoop中定时任务的触发时间
计算下一个将要触发的定时任务的剩余超时时间,将它转换成毫秒,为超时时间增加0.5毫秒的调整值。对剩余的超时时间进行判断.如果需要立即执行或者已经超时,则调用selector.selectNow进行轮询,将selectCnt设置为1,并退出当前循环
将任务剩余的超时时间作为参数进行select操作,每完成一次select操作,对select计数器 selectCnt加1
select操作完成之后,需要对结果进行判断,如果存在以下任意一种情况,则退出当前循环
有Channel处于就绪状态,selectKeys不为0,说明有读写事件需要处理
oldWakenUp为true
系统或者用户调用wakeup操作,唤醒当前的多路复用器
消息队列中有新的任务需要处理
如果本次Selector的轮询结果为空,也没有wakeup操作或者新的消息需要处理,则说明是个空轮询,有可能触发jdk的epoll bug,会导致Selector的空轮询,使I/O线程一直处于100%状态。Netty需要对jdk的bug进行规避
对Selector的select操作周期进行统计
每完成一次空的select操作进行一次计数
在某个周期内如果连续发生N次空轮询,说明触发了JDK NIO的epoll死循环。
检测到Selector处于死循环后,需要通过重建Selector的方式让系统恢复正常rebuildSelector
首先通过inEventLoop方法判断是否是其他线程发起的rebuildSelector。如果由其他线程发起,为了避免多线程并发操作Selector和其他资源,需要将rebuildSelector封装成Task,放到NioEventLoop的消息队列中,由NioEventLoop线程负责调用
调用openSelector方法创建并打开新的Selector,通过循环,将原Selector上注册的SocketChannel从旧的Selector上去注册,重新注册到新的Selector上,并将老的Selector关闭
通过销毁旧的、有问题的多路复用器,使用新建的Selector,就可以解决空轮询Selector导致的I/O线程CPU占用100%的问题
③如果轮询到了处于就绪状态的SocketChannel,则需要处理网络I/O事件processSelectedKeys
默认没有开启selectKeys优化功能,所以会进入processSelectedKeysPlain分支执行
对SelectionKey进行保护性判断,如果为空则返回。
SelectionKey不为空,获取SelectionKey的迭代器进行循环操作,通过迭代器获取SelectionKey和SocketChannel的附件对象,将已选择的选择键从迭代器中删除,防止下次被重复选择和处理
多SocketChannel的附件类型进行判读,如果是AbstractNioChannel类型,说明它是NioServerSocketChannel或者NioSocketChannel,需要进行I/O读写操作;
首先从NioServerSocketChannel或者NioSocketChannel中获取其内部类NioUnsafe
当前选择键不可用,调用unsafe.close方法,释放连接资源
当前选择键可用,则继续对网络操作位进行判断
如果是读或者连接操作,则调用unsafe.read方法,此处的read方法是多态的
如果网络操作位为写,则说明有半包消息上位发送完成,主要继续调用flush方法进行发送
如果操作位为连接状态,则需要对连接结果进行判断
需要注意的是,在进行finishConnect判断之前,需要将网络状态操作位进行修改,注销掉SelectionKey.OP_CONNECT
如果它是NioTask接口,通常情况下系统不会执行该分支,除非用户自行注册该Task到多路复用器上
④执行非I/O操作的系统Task和定时任务
⑥如果I/O操作多余定时任务和Task,则可以将I/O比率调大,反之调小,默认值为50%
⑦runAllTask方法的执行,
从定时任务消息队列中弹出消息进行处理,如果消息队列为空,则退出循环。
根据当前的时间戳进行判断,如果该定时任务已经或者正在处于超时状态,则将其加入到执行的TaskQueue中,同时从延时队列中删除。定时任务如果没有超时,说明本轮循环不需要处理,直接退出即可
执行Task Queue中原有的任务和从延迟队列中复制的已经超时或者正在超时的定时任务
由于获取系统时间耗时,降低性能。每执行60次循序判断一次,如果当前系统时间已经到了分配给非I/O操作的超时时间,则退出循环,防止由于非I/O任务过多导致I/O操作被长时间阻塞
⑧最后判断系统是否进入优雅停机状态,如果处于关闭状态,则需要调用closeAll方法,释放资源,并让NioEventLoop线程退出循环,结束运行
⑨closeAll的时候,遍历获取所有的Channel,调用它的Unsafe.close方法关闭所有链路,释放线程池、ChannelPipeline和ChannelHandler等资源
EventLoop和EventLoopGroup