【IO】Reactor模式

先看前两篇:
一、IO的四种基本模式了解相关的概念:【IO】SBIO,SNBIO,ANIO,AIO
二、IO多路复用:【IO】IO多路复用及select,poll,epoll运行机制

接下来继续总结Reactor模式。
IO多路复用只是简单的介绍了在网络请求中的前半部分,而后半部分的请求处理和响应就是接下来的reactor模式演化。

最最原始的网络编程思路就是服务器用一个while循环,不断监听端口是否有新的套接字连接,如果有,那么就调用一个处理函数处理。这样做单线程阻塞严重。

后来考虑可以用多线程并发模式,这样来一个请求创建一个线程进行处理。这样可以大大提高服务器的吞吐量,但是线程创建和销毁消耗了太多的资源,如果连接数很高系统性能也会受到很大影响。

改进方法:采用基于事件驱动的设计,单线程模式,当有事件触发时,才会调用处理器进行数据处理。使用Reactor模式,对线程的数量进行控制,一个线程处理大量的事件。

单线程模式Reactor

下图来源于网络,认为更形象更好理解。
在这里插入图片描述
(1)Reactor:负责响应IO事件,当检测到一个新的事件,将其发送给相应的Handler去处理;新的事件包含连接建立就绪、读就绪、写就绪等。

(2)Handler:将自身(handler)与事件绑定,负责事件的处理,完成channel的读入,完成处理业务逻辑后,负责将结果写出channel。acceptor是一个特殊的handler。

下图取自“Scalable IO in Java”
在这里插入图片描述
事件流程:

1、服务器端的Reactor是一个线程对象,该线程会启动事件循环,并使用Selector来实现IO的多路复用。
注册一个Acceptor事件处理器到Reactor中,Acceptor事件处理器所关注的事件是ACCEPT事件,
这样Reactor会监听客户端向服务器端发起的连接请求事件(ACCEPT事件)。

2、客户端向服务器端发起一个连接请求,Reactor监听到了该ACCEPT事件的发生并将该ACCEPT事件派发给相应的Acceptor处理器来进行处理。Acceptor处理器通过accept()方法得到与这个客户端对应的连接(SocketChannel),
然后将该连接所关注的READ事件以及对应的READ事件处理器注册到Reactor中,
这样一来Reactor就会监听该连接的READ事件了。或者当你需要向客户端发送数据时,
就向Reactor注册该连接的WRITE事件和其处理器。

3、当Reactor监听到有读或者写事件发生时,将相关的事件派发给对应的处理器进行处理。
比如,读处理器会通过SocketChannel的read()方法读取数据,此时read()操作可以直接读取到数据,
而不会堵塞与等待可读的数据到来。

4、每当处理完所有就绪的感兴趣的I/O事件后,Reactor线程会再次执行select()阻塞等待新的事件就绪
并将其分派给对应处理器进行处理。

缺点:
1、 当其中某个 handler 阻塞时, 会导致其他所有的 client 的 handler 都得不到执行, 并且更严重的是, handler 的阻塞也会导致整个服务不能接收新的 client 请求(因为 acceptor 也被阻塞了)。 因为有这么多的缺陷, 因此单线程Reactor 模型用的比较少。这种单线程模型不能充分利用多核资源,所以实际使用的不多。

因此,单线程模型仅仅适用于handler 中业务处理组件能快速完成的场景。

改进:创建一个线程池,将非IO处理移交工作线程池处理。

工作者线程池Reactor

在线程Reactor模式基础上,做如下改进:

(1)将Handler处理器的执行放入线程池,多线程进行业务处理。

(2)而对于Reactor而言,可以仍为单个线程。如果服务器为多核的CPU,为充分利用系统资源,可以将Reactor拆分为两个线程。
下图来源于网络:
在这里插入图片描述
在这里插入图片描述
与单线程Reactor模式不同的是,添加了一个工作者线程池,并将 非I/O 操作从Reactor线程中移出转交给工作者线程池来执行。这样能够提高Reactor线程的I/O响应,不至于因为一些耗时的业务逻辑而延迟对后面I/O请求的处理。

注意,在上图的改进的版本中,所有的I/O操作依旧由一个Reactor来完成,包括I/O的accept()、read()、write()以及connect()操作。

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

多Reactor线程模式

netty中采用的就是这种模式。
在这里插入图片描述
Reactor线程池中的每一Reactor线程都会有自己的Selector、线程和分发的事件循环逻辑。
mainReactor可以只有一个,但subReactor一般会有多个。mainReactor线程主要负责接收客户端的连接请求,然后将接收到的SocketChannel传递给subReactor,由subReactor来完成和客户端的通信。

事件流程:

1、注册一个Acceptor事件处理器到mainReactor中,Acceptor事件处理器所关注的事件是ACCEPT事件,
这样mainReactor会监听客户端向服务器端发起的连接请求事件(ACCEPT事件)。
启动mainReactor的事件循环。

2、客户端向服务器端发起一个连接请求,mainReactor监听到了该ACCEPT事件并将该ACCEPT事件
派发给Acceptor处理器来进行处理。Acceptor处理器通过accept()方法得到与这个客户端对应的连接
(SocketChannel),然后将这个SocketChannel传递给subReactor线程池。

3、subReactor线程池分配一个subReactor线程给这个SocketChannel,即,将SocketChannel关注的READ事件
以及对应的READ事件处理器注册到subReactor线程中。当然你也注册WRITE事件以及WRITE事件处理器到
subReactor线程中以完成I/O写操作。Reactor线程池中的每一Reactor线程都会有自己的Selector、线程
和分发的循环逻辑。

4、当有I/O事件就绪时,相关的subReactor就将事件派发给响应的处理器处理。注意,这里subReactor线程
只负责完成I/O的read()操作,在读取到数据后将业务逻辑的处理放入到线程池中完成,若完成业务逻辑后
需要返回数据给客户端,则相关的I/O的write操作还是会被提交回subReactor线程来完成。

多Reactor线程模式将“接受客户端的连接请求”和“与该客户端的通信”分在了两个Reactor线程来完成。mainReactor完成接收客户端连接请求的操作,它不负责与客户端的通信,而是将建立好的连接转交给subReactor线程来完成与客户端的通信,这样一来就不会因为read()数据量太大而导致后面的客户端连接请求得不到即时处理的情况。并且多Reactor线程模式在海量的客户端并发请求的情况下,还可以通过实现subReactor线程池来将海量的连接分发给多个subReactor线程,在多核的操作系统中这能大大提升应用的负载和吞吐量。

以上是关于reactor模式的演化过程。

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