参考链接:https://www.leahy.club/archives/netty-thread-model
主从Reactor模型:
Netty里面采用的使用一种称为Reactor的线程模型(可以用来对Java NIO进行进一步的封装,但是大家一般不自己封装而是使用Netty这种框架)。Reactor是将IO多路复用和线程池技术结合的一种线程模型。Netty的线程模型主要是基于一种称为主从Reactor线程模型的改进。主从Reactor线程模型主要包括一个MainReactor和多个SubReactor,MainReactor主要负责进行网络连接,响应客户端的连接请求;SubReactor主要负责处理连接之后的事件,比如读写操作等,且Reactor可以处理多个Client的事件,采用了IO多路复用的机制和线程池来提高效率。如下图所示:
具体的流程如下:
- Reactor主线程MainReactor对象通过select监听连接事件,收到事件之后,通过Acceptor处理连接事件
- 当Acceptor处理连接事件之后,MainReactor将连接分配给SubReactor
- SubReactor将连接加入到队列进行监听,并创建Handler进行各种事件处理
- 当有新的事件发生时,SubReactor就会调用对应的Handler处理
- Handler通过read读取数据,分发给会面的worker线程处理
- worker线程池分配独立的worker线程进行业务处理,并返回结果
- handler 收到响应的结果后,再通过send 将结果返回给client
- Reactor 主线程可以对应多个Reactor 子线程, 即MainRecator 可以关联多个SubReactor
Netty线程模型:
Netty的线程模型就是基于Reactor线程模型的改进,netty的线程模型是可以通过设置启动类的参数来配置的,设置不同的启动参数,netty支持Reactor单线程模型、多线程模型和主从Reactor多线程模型。
服务端启动时创建了两个NioEventLoopGroup,一个是boss,一个是worker。实际上他们是两个独立的Reactor线程池,一个用于接收客户端的TCP连接,另一个用于处理IO相关的读写操作,或则执行系统的Task,定时Task。
可以参见下面的图片:
具体的流程如下:
- Netty 抽象出两组线程池BossGroup 专门负责接收客户端的连接, WorkerGroup 专门负责网络的读写
- BossGroup 和WorkerGroup 类型都是NioEventLoopGroup
- NioEventLoopGroup 相当于一个事件循环组, 这个组中含有多个事件循环,每一个事件循环是NioEventLoop
- NioEventLoop 表示一个不断循环的执行处理任务的线程, 每个NioEventLoop 都有一个selector , 用于监听绑定在其上的socket 的网络通讯
- NioEventLoopGroup 可以有多个线程, 即可以含有多个NioEventLoop
- 每个Boss NioEventLoop 循环执行的步骤
- 轮询accept 事件
- 处理accept 事件, 与client 建立连接, 生成NioScocketChannel , 并将其注册到某个WorkerGroup的 NIOEventLoop 上的selector
- 处理任务队列的任务, 即runAllTasks
- 每个Worker NIOEventLoop 循环执行的步骤
- 轮询read, write 事件
- 处理i/o 事件, 即read , write 事件,在对应NioScocketChannel 处理
- 处理任务队列的任务, 即runAllTasks
- 每个Worker NIOEventLoop 处理业务时,会使用pipeline(管道), pipeline 中包含了channel , 即通过pipeline可以获取到对应通道, 管道中维护了很多的处理器
Netty线程模型的改进以及最佳实践
改进:
为了提升性能,Netty在很多地方都进行了无锁设计。比如在IO线程内部进行串行操作,避免多线程竞争造成的性能问题。表面上似乎串行化设计似乎CPU利用率不高,但是通过调整NIO线程池的线程参数,可以同时启动多个串行化的线程并行运行,这种局部无锁串行线程设计性能更优。
比如,Netty的NioEventLoop接收到Channel的读取事件之后,直接调用ChannelPipeline的fireChannelRead(Object msg),只要用户不主动切换线程,一直都是由这个NioEventLoop调用用户的Handler,期间不进行线程切换,这种串行化设计避免了多线程操作导致的锁竞争,性能角度看是最优的。
最佳实践:
- 创建两个NioEventLoopGroup,隔离NIO Acceptor和NIO的IO线程。
- 解码要放在NIO线程调用的Handler中,不要放在用户线程中解码。(往ChannelPipeline中添加编解码的Handler)
- 如果IO操作非常简单,不涉及复杂的业务逻辑计算,比如仅仅是简单的少量内容的读写操作,可以再NIO线程调用的Handler中完成业务逻辑。
- 如果IO业务操作比较复杂,就不要在NIO的IO线程上完成,因为阻塞可能会导致NIO线程假死,严重降低性能。可以将业务封装成Task,派发到业务线程池中由业务线程处理,以保证NIO线程被尽快的释放,可以处理其余的IO操作。
参考链接:
详细地介绍了Reactor线程模型和Netty的线程模型:https://blog.csdn.net/u010853261/article/details/55805216
Netty的NIO服务端代码调用分析:https://www.cnblogs.com/davidwang456/p/5118802.html
手动实现Reactor模型:
[Reactor单线程模型的实现](https://exceting.github.io/2019/03/27/Java NIO学习与记录(七): Reactor单线程模型的实现/)
[Reactor两种多线程模型的实现](https://exceting.github.io/2019/04/01/Java NIO学习与记录(八): Reactor两种多线程模型的实现/)