分析Netty线程模型

Netty线程模型是基于Reactor线程模型的,而Reactor线程模型又分为单线程模型,多线程模型,主从Reactor多线程模型。Reactor的线程模型是基于同步非阻塞的IO实现的,基于异步非阻塞的IO实现的是Proactor(这里我们不分析Proactor)。下面一起来研究一下Reactor线程模型,以及其在Netty线程模型中的应用。

1.Reactor单线程模型

原理图如下:


Reactor单线程模型,顾名思义就是由单个NIO线程来完成所有的IO操作,具体的工作有:

a.负责读取通讯对端的请求/应答消息

b.负责向通讯对端发送请求/应答消息

c.作为服务端,接收客户端的连接请求

d.作为客户端,向服务端发送连接请求

从原理图中可以看到,NIO一个线程把多路复用,事件分发器和处理都自己全部包揽了,Reactor线程模式采用的是同步非阻塞IO,即线程上的IO操作不会受到阻塞的影响,理论上来说,上面的工作,的确可以由一个线程全部搞定(select会主动轮询哪些IO操作就绪),工作过程:Acceptor接收客户端发来的TCP请求,链路成功建立之后,通过dispatcher将ByteBuf交给对应的Handler进行处理。

对于一些小流量的应用程序,可以采用上述单线程模型,但是对于大负载、高并发的情况却不适用,主要原因有:

a.当一个IO线程处理百万甚至千万级别的链路数量时,性能会支撑不足,CPU也可能会被压垮

b.当一个IO线程负载过重,就会出现处理不及时的情况,会导致客户端长时间等待而不断超时重连,客户体验极差

c.一旦这个IO线程出现了死循环,则会导致整个程序不可用。

为了弥补上述短板,提出了Reactor多线程模型

2.Reactor多线程模型

原理图如下:



从原理图中分析如下:

a.有一个专门的NIO线程Acceptor负责监听服务器,处理客户端的TCP连接,包括安全验证等

b.Acceptor处理完之后,将IO操作发到NIO线程池,线程池可使用JDK的线程池实现,包含一个任务队列和N个线程,这些线程负责信息的读取、编码、解码、发送

c.一个NIO线程可以同时处理N个链路,但一个链路只能对应一个线程,避免了并发的问题

Reactor多线程模型满足绝大部分的应用场景,但不足之处在于,当面临百万级别的请求当中需要有安全认证时,因为认证是消耗性能的操作,所以一个Acceptor线程会出现性能瓶颈。

为了解决上述问题,出现了主从Reactor多线程模型

3.主从Reactor多线程模型

原理图如下:


从原理图中以看出,处理客户端连接请求的不在是一个NIO线程,而是一个NIO线程池,Acceptor线程处理完客户端的TCP接入请求(可能包含验证)之后,再将新创建的SocketChannel注册到IO线程池(Sub Reactor)中某个IO线程来处理读取、编码、解码、发送等操作。Acceptor线程只负责监听服务端,处理客户端的TCP连接与认证请求,链路成功建立之后,便把链路注册到Sub Reactor Thread Pool的线程中。

主从Reactor多线程模型,很多地解决性能瓶颈的问题,这时Netty推荐使用的线程模型

4.Netty的线程模型

原理图如下:


Netty的线程模型可以通过设置启动类参数来选择线程模型,netty支持Reactor的单线程模型、多线程、主从Reactor多线程模型。

从原理图中可以看到,服务端启动时创建了两个NioEventLoopGroup,这是两个相互独立的Reactor线程池,一个是boss线程池,一个是work线程池。两个分工明确,一个负责接收客户端连接请求,一个负责处理IO相关的读写操作,或者执行Task任务,或执行定时Task任务。其中NioEventLoopGroup中的NioEventLoop个数默认为处理器核数*2。

通过配置boss线程池和worker线程池的线程个数以及是否共享线程池,来配置单线程、多线程、主从Reactor多线程。

Boss线程池的任务:

a.接收客户端的连接请求,初始化channel参数

b.将链路状态变化时间通知给ChannelPipeline

Worker线程池的作用

a.异步读取通信对端的消息,发送读事件到ChannelPipeline

b.异步发送消息到通信对端,调用ChannelPipeline的发送消息接口

c.执行系统Task任务

d.执行系统定时Task任务

4.1系统Task

创建它们的原因是当IO线程和用户线程都在操作同一个资源时,会发生锁竞争的问题,所以将用户线程封装为一个Task,交给IO线程串行处理,实现局部无锁化

4.2定时Task

主要用于监控和检查等定时动作

基于以上原因,NioEventLoop不仅仅是一个IO线程,它还负责用户线程的调度。

NioEventLoop处理链原理图如下:

为了提升性能,Netty内部许多地方都采用了局部无锁化设计,比如IO线程内部采用串行化操作来避免多线程的锁竞争问题。表面上看这样会导致CUP利用率低下,但只要配置多个串行化IO线程并发操作,就能提高CUP利用率,且性能比更优。

Netty的NioEventLoop读取完数据之后,将直接调用ChannelPipeline的fireChannelRead(Object msg)方法,只要用户不切换线程,NioEventLoop将一直调用用户的handler,期间不进行线程切换。这就是串行化的具体表现,可以很好地避免了锁竞争的问题,提升了性能。

4.3Netty线程模型设置的要点

a.创建两个NioEventLoopGroup,分别为NIO Acceptor线程池和NIO IO线程池

b.编解码需要放置在NIO的Handler中进行,而不是用户线程中

c.尽量不要在ChannelHandler中创建用户线程,可以在解码之后,将POJO派发到后端业务线程池

d.如果IO操作不复杂,没有涉及可能阻塞的数据库读取、磁盘读取、网络读写等操作,可以直接在NIO调用的handler中完成业务逻辑,而不需要放在用户线程中进行,相反地,如果IO业务逻辑复杂, 将会严重影响性能,所以需要将POJO打包成Task对象,派发到业务线程池中处理,确保NIO线程不被长时间占用而导致假死。



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章