Netty学习篇四——netty线程模型

在学习Netty 之前我们最好先掌握 BIO、NIO、AIO 基础知识

IO 模型

  BIO:同步阻塞模型;

  NIO:基于IO多路复用技术的“非阻塞同步”IO模型。简单来说,内核将可读可写事件通知应用,由应用主动发起读写事件;

  AIO:非阻塞异步IO模型。简单来说,内核将读完成事件通知应用,读操作由内核完成,应用只需要操作数据即可;应用做异步写操作时立即返回,内核会进行写操作排队并执行写操作

  NIO 和 AIO 不同之处在于应用是否进行真正的读写操作。

reactor 和 proactor 模型

  reactor:基于NIO技术,可读可写时通知应用

  proactor:基于AIO技术,读完成时通知应用,写操作应用通知内核。

Reactor模型

1.1 单线程模型

Reactor单线程模型,指的是所有的IO操作都在同一个NIO线程上面完成,NIO线程的职责如下:

1)作为NIO服务端,接收客户端的TCP连接;

2)作为NIO客户端,向服务端发起TCP连接;

3)读取通信对端的请求或者应答消息;

4)向通信对端发送消息请求或者应答消息。

Reactor单线程模型示意图如下所示:

  由于Reactor模式使用的是异步非阻塞IO,所有的IO操作都不会导致阻塞,理论上一个线程可以独立处理所有IO相关的操作。从架构层面看,一个NIO线程确实可以完成其承担的职责。例如,通过Acceptor类接收客户端的TCP连接请求消息,链路建立成功之后,通过Dispatch将对应的ByteBuffer派发到指定的Handler上进行消息解码。用户线程可以通过消息编码通过NIO线程将消息发送给客户端。

对于一些小容量应用场景,可以使用单线程模型。但是对于高负载、大并发的应用场景却不合适,主要原因如下:

  1)一个NIO线程同时处理成百上千的链路,性能上无法支撑,即便NIO线程的CPU负荷达到100%,也无法满足海量消息的编码、解码、读取和发送;

  2)当NIO线程负载过重之后,处理速度将变慢,这会导致大量客户端连接超时,超时之后往往会进行重发,这更加重了NIO线程的负载,最终会导致大量消息积压和处理超时,成为系统的性能瓶颈;

  3)可靠性问题:一旦NIO线程意外跑飞,或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障。

为了解决这些问题,演进出了Reactor多线程模型,下面我们一起学习下Reactor多线程模型。

1.2 多线程模型

Rector多线程模型与单线程模型最大的区别就是有一组NIO线程处理IO操作,它的原理图如下:

 Reactor多线程模型的特点:

  1)有专门一个NIO线程-Acceptor线程用于监听服务端,接收客户端的TCP连接请求;

  2)网络IO操作-读、写等由一个NIO线程池负责,线程池可以采用标准的JDK线程池实现,它包含一个任务队列和N个可用的线程,由这些NIO线程负责消息的读取、解码、编码和发送;

  3)1个NIO线程可以同时处理N条链路,但是1个链路只对应1个NIO线程,防止发生并发操作问题。

  在绝大多数场景下,Reactor多线程模型都可以满足性能需求;但是,在极个别特殊场景中,一个NIO线程负责监听和处理所有的客户端连接可能会存在性能问题。例如并发百万客户端连接,或者服务端需要对客户端握手进行安全认证,但是认证本身非常损耗性能。在这类场景下,单独一个Acceptor线程可能会存在性能不足问题,为了解决性能问题,产生了第三种Reactor线程模型-主从Reactor多线程模型

1.3 主从多线程模型

  主从Reactor线程模型的特点是:服务端用于接收客户端连接的不再是个1个单独的NIO线程,而是一个独立的NIO线程池。Acceptor接收到客户端TCP连接请求处理完成后(可能包含接入认证等),将新创建的SocketChannel注册到IO线程池(sub reactor线程池)的某个IO线程上,由它负责SocketChannel的读写和编解码工作。Acceptor线程池仅仅只用于客户端的登陆、握手和安全认证,一旦链路建立成功,就将链路注册到后端subReactor线程池的IO线程上,由IO线程负责后续的IO操作。

它的线程模型如下图所示:

利用主从NIO线程模型,可以解决1个服务端监听线程无法有效处理所有客户端连接的性能不足问题。

它的工作流程总结如下:

1.从主线程池中随机选择一个Reactor线程作为Acceptor线程,用于绑定监听端口,接收客户端连接;

2.Acceptor线程接收客户端连接请求之后创建新的SocketChannel,将其注册到主线程池的其它Reactor线程上,由其负责接入认证、IP黑白名单过滤、握手等操作

3..步骤2完成之后,业务层的链路正式建立,将SocketChannel从主线程池的Reactor线程的多路复用器上摘除,重新注册到Sub线程池的线程上,用于处理I/O的读写操作。

2. Netty中Reactor模型的实现

  Netty同时支持Reactor的单线程、多线程和主从多线程模型,在不同的应用中通过启动参数的配置来启动不同的线程模型。通过线程池的线程个数、是否共享线程池方式来切换不同的模型

2.1.Netty中的Reactor模型

Netty中的Reactor模型如下图:

  • Acceptor中的NioEventLoop用于接收TCP连接,初始化参数
  • I/O线程池中的NioEventLoop异步读取通信对端的数据报,发送读事件到channel
  • 异步发送消息到对端,调用channel的消息发送接口
  • 执行系统调用Task
  • 执行定时Task

2.2 NioEventLoop

NioEventLoop是Netty的Reactor线程,它在Netty Reactor线程模型中的职责如下:

1. 作为服务端Acceptor线程,负责处理客户端的请求接入

2. 作为客户端Connecor线程,负责注册监听连接操作位,用于判断异步连接结果

3. 作为IO线程,监听网络读操作位,负责从SocketChannel中读取报文

4. 作为IO线程,负责向SocketChannel写入报文发送给对方,如果发生写半包,会自动注册监听写事件,用于后续继续发送半包数据,直到数据全部发送完成

 处理链中的处理方法是串行化执行的

一个客户端连接只注册到一个NioEventLoop上,避免了多个IO线程并发操作

2.2.1 Task

Netty Reactor线程模型中有两种Task:系统Task和定时Task

  • 系统Task:创建它们的主要原因是,当IO线程和用户线程都在操作同一个资源时,为了防止并发操作时锁的竞争问题,将用户线程封装为一个Task,在IO线程负责执行,实现局部无锁化
  • 定时Task:主要用于监控和检查等定时动作

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

2.2.2 IO线程的分配细节

  线程池对IO线程进行资源管理,是通过EventLoopGroup实现的。线程池平均分配channel到所有的线程(循环方式实现,不是100%准确),一个线程在同一时间只会处理一个通道的IO操作,这种方式可以确保我们不需要关心同步问题。

2.2.3 Selector

NioEventLoop是Reactor的核心线程,那么它就就必须实现多路复用。

Selector的过程如下:

首先oldWakenUp = wakenUp.getAndSet(false)

如果队列中有任务, selectNow()

如果没有select(),直达channel准备就绪,但此过程中循环次数超过限值也将rebuidSelectoror退出循环

执行processSelectedKeys和runAllTasks

epoll-bug的处理

在netty中对java nio的epoll bug进行了处理,就是设置一个阀值,如果超过了就rebuidSelector来避免epoll()死循环

2.2.4 NioEevntLoopGroup

  EventExecutorGroup:提供管理EevntLoop的能力,他通过next()来为任务分配执行线程,同时也提供了shutdownGracefully这一优雅下线的接口

  EventLoopGroup继承了EventExecutorGroup接口,并新添了3个方法

    •  EventLoop next()
    •  ChannelFuture register(Channel channel)
    •  ChannelFuture register(Channel channel, ChannelPromise promise)

  EventLoopGroup的实现中使用next().register(channel)来完成channel的注册,即将channel注册时就绑定了一个EventLoop,然后EvetLoop将channel注册到EventLoop的Selector上。

  NioEventLoopGroup还有几点需要注意:

    •  NioEventLoopGroup下默认的NioEventLoop个数为cpu核数 * 2,因为有很多的io处理
    •  NioEventLoop和java的single线程池在5里差异变大了,它本身不负责线程的创建销毁,而是由外部传入的线程池管理
    •  channel和EventLoop是绑定的,即一旦连接被分配到EventLoop,其相关的I/O、编解码、超时处理都在同一个EventLoop中,这样可以确保这些操作都是线程安全的
原文地址:https://www.cnblogs.com/wffzk/p/15557436.html