Reactor线程模型
-
Reactor单线程模型:所有的I/O操作都在同一个NIO线程中完成。
例如,通过Acceptor 类接收客户端的TCP连接请求消息,当链路建立成功之后,通过Dispatch将对应的ByteBuffer派发到指定的Handler上,进行消息解码。用户线程消息编码后通过NIO线程将消息发送给客户端。适合小容量场景,对于高负载、大并发的应用场景不适合。
会产生的问题如下:
* 一个NIO线程同时处理成百上千的链路,性能上无法支撑。
* 线程负载过重后,处理速度变慢,导致大量客户端连接超时,超时之后进行重发也会加重NIO线程的负载,导致大量消息积压和处理超时,造成性能瓶颈。
* 可靠性问题:如果线程跑飞,或者进入死循环,会导致整个系统通信模型块不可用,无法接受和处理外部消息,造成节点故障。 -
Reactor多线程模型:有一组NIO线程来处理I/O操作
Reactor 多线程特点如下:- 有专门一个NIO线程一Acceptor 线程用于监听服务端,接收客户端的TCP连接请求。
- 网络I/O操作一读、写等由一个NIO线程池负责,线程池可以采用标准的JDK线程池实现,它包含一个任务队列和N个可用的线程,由这些NIO线程负责消息的读取、解码、编码和发送。
- 一个NIO线程可以同时处理N条链路,但是一个链路只对应一个NIO线程,防止发生并发操作问题。
这种场景能满足大多数情况,但在并发百万客户端连接,或者服务端需要对客户端握手进行安全认证,但认证本身非常损耗性能。
- 主从Reactor多线程模型:服务端用于接收客户端连接的不再是一个单独的NIO线程,而是一个独立的NIO线程池。
Acceptor线程接受客户端TCP连接请求并处理完成后,将新创建的SocketChannel注册到I/O线程(sub Reactor线程池)的某个I/O线程上,负责SocketChannel的读写和编解码工作。
利用主从NIO线程模型,可以解决一个服务端监听线程无法有效处理所有客户端连接的性能不足问题。因此,在Netty的官方Demo中推荐使用该线程模型。
-
Netty线程模型
Netty线程模型是Reactor的哪种模式,取决于用户的启动参数配置。当启动配置为EventLoopGroup bossGroup=new NioEventLoopGroup(); EventLoopGroup workerGroup=new NioEventLoopGroup();
时,表示启动的主从Reactor模型;当启动配置为EventLoopGroup bossGroup=new NioEventLoopGroup(1); EventLoopGroup workerGroup=new NioEventLoopGroup();
,表示启动Reactor多线程模型。服务端启动的时候,创建两个NioEventLoopGroup,实际是两个独立的Reactor线程池,一个用于接收客户端的TCP连接(bossGroup),一个用于处理I/O相关的读写操作,或者执行系统Task、定时Task(workerGroup)。
- bossGroup的职责如下:
* 接收客户端TCP连接,初始化Channel参数。
* 将链路状态变更事件通知ChannelPipeline。 - workerGroup的职责如下:
* 异步读取通信对端的数据报,发送事件到ChannelPIpeline
* 异步发送消息到通信对端,调用ChannelPipeline的消息发送接口
* 执行系统调用Task。
* 执行定时任务Task。例如链路空闲状态检测定时任务。
*Netty中有许多无锁化设计:I/O线程内部进行的串行操作,避免多线程竞争导致性能下降。通过调整NIO线程池的线程数,可以同时启动多个串行化的线程并行运行,提高CPU利用率。设计原理如下
Netty的NioEventLoop读取到消息之后,直接调用ChannelPipeline的fireChannelRead(Object msg)。只要用户不主动切换线程,一直都是由NioEventLoop调用用户的Handler,期间不进行线程切换。这种串行化处理方式避免了多线程操作导致的锁的竞争,从性能角度看是最优的。
- bossGroup的职责如下:
NioEventLoop
NioEventLoop除了负责网络I/O操作,还负责处理系统Task、定时任务。
- NioEventLoop的run方法
run 方法 是整个 EventLoop 的核心,所有的逻辑操作都在for循环进行,只有当NioEventLoop收到退出命令才退出循环。整个run方法做了以下3件事:
1. select获取感兴趣的事件。调用 selector 的 select 方法,默认阻塞一秒钟,如果有定时任务,则在定时任务剩余时间的基础上在加上 0.5 秒进行阻塞。当执行 execute 方法的时候,也就是添加任务的时候,唤醒 selector,防止 selectotr 阻塞时间过长。
2. processSelectedKeys 处理事件。当 selector 返回的时候,回调processSelectedKeys 方法对 selectKey 进行处理。
3. runAllTasks 执行队列中的任务。当 processSelectedKeys 方法执行结束后,则按照ioRatio 的比例执行 runAllTasks 方法,默认是 IO 任务时间和非 IO 任务时间是相同的,你也可以根据你的应用特点进行调优 。