Reactor模式详解
在学习Reactor模式之前,我们需要对“I/O的四种模型”以及“什么是I/O多路复⽤”进⾏简单的介绍,因为Reactor是⼀个使⽤了同步⾮阻塞的I/O 多路复⽤机制的模式。
I/O的四种模型
I/0 操作主要分成两部分
①数据准备,将数据加载到内核缓存
②将内核缓存中的数据加载到⽤户缓存
Synchronous blocking I/O
Typical flow of the synchronous blocking I/O model
Synchronous non-blocking I/0
Typical flow of the synchronous non-blocking I/O model
Asynchronous blocking I/0
Typical flow of the asynchronous blocking I/O model (select)
Asynchronous non-blocking I/0
Typical flow of the asynchronous non-blocking I/O model
堵塞、⾮堵塞的区别是在于第⼀阶段,即数据准备阶段。⽆论是堵塞还是⾮堵塞,都是⽤应⽤主动内核要数据,⽽read数据的过程是‘堵塞’的,直到数据读取完。
同步、异步的区别在于第⼆阶段,若由请求者主动的去获取数据,则为同步操作,需要说明的是:read/write操作也是‘堵塞’的,直到数据读取完。
若数据的read都由kernel内核完成了(在内核read数据的过程中,应⽤进程依旧可以执⾏其他的任务),这就是异步操作。
换句话说,BIO⾥⽤户最关⼼“我要读”,NIO⾥⽤户最关⼼"我可以读了",在AIO模型⾥⽤户更需要关注的是“读完了”。
NIO⼀个重要的特点是:socket主要的读、写、注册和接收函数,在等待就绪阶段都是⾮阻塞的,真正的I/O操作是同步阻塞的(消耗CPU但性能⾮常⾼)。
NIO是⼀种同步⾮阻塞的I/O模型,也是I/O多路复⽤的基础。
I/O多路复⽤
I/O多路复⽤是指使⽤⼀个线程来检查多个⽂件描述符(Socket)的就绪状态,⽐如调⽤select和poll函数,传⼊多个⽂件描述符,如果有⼀个⽂件描述符就绪,则返回,否则阻塞直到超时。得到就绪状态后进⾏真正的操作可以在同⼀个线程⾥执⾏,也可以启动线程执⾏(⽐如使⽤线程池)。
⼀般情况下,I/O 复⽤机制需要事件分发器。事件分发器的作⽤,将那些读写事件源分发给各读写事件的处理者。
涉及到事件分发器的两种模式称为:Reactor和Proactor。 Reactor模式是基于同步I/O的,⽽Proactor模式是和异步I/O相关的。本⽂主要介绍的就是 Reactor模式相关的知识。
经典的I/O服务设计 ———— BIO模式
这就是经典的每连接对应⼀个线程的同步阻塞I/O模式。
流程:
①服务器端的Server是⼀个线程,线程中执⾏⼀个死循环来阻塞的监听客户端的连接请求和通信。
②当客户端向服务器端发送⼀个连接请求后,服务器端的Server会接受客户端的请求,ServerSocket.accept()从阻塞中返回,得到⼀
个与客户端连接相对于的Socket。
③构建⼀个handler,将Socket传⼊该handler。创建⼀个线程并启动该线程,在线程中执⾏handler,这样与客户端的所有的通信以及
数据处理都在该线程中执⾏。当该客户端和服务器端完成通信关闭连接后,线程就会被销毁。
④然后Server继续执⾏accept()操作等待新的连接请求。
优点:
①使⽤简单,容易编程
②在多核系统下,能够充分利⽤了多核CPU的资源。即,当I/O阻塞系统,但CPU空闲的时候,可以利⽤多线程使⽤CPU资源。
缺点:
该模式的本质问题在于严重依赖线程,但线程Java虚拟机⾮常宝贵的资源。随着客户端并发访问量的急剧增加,线程数量的不断膨胀将服务器端的性能将急剧下降。
①线程⽣命周期的开销⾮常⾼。线程的创建与销毁并不是没有代价的。在Linux这样的操作系统中,线程本质上就是⼀个进程,创建和
销毁都是重量级的系统函数。
②资源消耗。内存:⼤量空闲的线程会占⽤许多内存,给垃圾回收器带来压⼒。;CPU:如果你已经拥有⾜够多的线程使所有CPU保
持忙碌状态,那么再创建更过的线程反⽽会降低性能。
③稳定性。在可创建线程的数量上存在⼀个限制。这个限制值将随着平台的不同⽽不同,并且受多个因素制约:a)JVM的启动参数、
b)Threa的构造函数中请求的栈⼤⼩、c)底层操作系统对线程的限制等。如果破坏了这些限制,那么很可能抛出OutOfMemoryError异
常。
④线程的切换成本是很⾼的。操作系统发⽣线程切换的时候,需要保留线程的上下⽂,然后执⾏系统调⽤。如果线程数过⾼,不仅会
带来许多⽆⽤的上下⽂切换,还可能导致执⾏线程切换的时间甚⾄会⼤于线程执⾏的时间,这时候带来的表现往往是系统负载偏⾼、CPU sy(系统CPU)使⽤率特别⾼,导致系统⼏乎陷⼊不可⽤的状态。
⑤容易造成锯齿状的系统负载。⼀旦线程数量⾼但外部⽹络环境不是很稳定,就很容易造成⼤量请求的结果同时返回,激活⼤量阻塞
reactor在3d里面中文什么意思线程从⽽使系统负载压⼒过⼤。
⑥若是长连接的情况下并且客户端与服务器端交互并不频繁的,那么客户端和服务器端的连接会⼀直保留着,对应的线程也就⼀直存
在在,但因为不频繁的通信,导致⼤量线程在⼤量时间内都处于空置状态。
适⽤场景:如果你有少量的连接使⽤⾮常⾼的带宽,⼀次发送⼤量的数据,也许典型的IO服务器实现可能⾮常契合。
Reactor模式
Reactor模式(反应器模式)是⼀种处理⼀个或多个客户端并发交付服务请求的事件设计模式。当请求抵达后,服务处理程序使⽤I/O多路复⽤策略,然后同步地派发这些请求⾄相关的请求处理程序。
Reactor结构
Reactor模式的⾓⾊构成(Reactor模式⼀共有5中⾓⾊构成):
Handle(句柄或描述符,在Windows下称为句柄,在Linux下称为描述符):本质上表⽰⼀种资源(⽐如说⽂件描述符,或是针对⽹络编程中的socket描述符),是由操作系统提供的;该资源⽤于表⽰⼀个个的事件,事件既可以来⾃于外部,也可以来⾃于内部;外部事件⽐如说客户端的连接请求,客户端发送过来的数据等;内部事件⽐如说操作系统产⽣的定时事件等。它本质上就是⼀个⽂件描述
符,Handle是事件产⽣的发源地。
Synchronous Event Demultiplexer(同步事件分离器):它本⾝是⼀个系统调⽤,⽤于等待事件的发⽣(事件可能是⼀个,也可能是多个)。调⽤⽅在调⽤它的时候会被阻塞,⼀直阻塞到同步事件分离器上有事件产⽣为⽌。对于Linux来说,同步事件分离器指的就是常⽤的I/O多路复⽤机制,⽐如说select、poll、epoll等。在Java NIO领域中,同步事件分离器对应的组件就是Selector;对应的阻塞⽅法就是select⽅法。
Event Handler(事件处理器):本⾝由多个回调⽅法构成,这些回调⽅法构成了与应⽤相关的对于某个事件的反馈机制。在Java NIO领域中并没有提供事件处理器机制让我们调⽤或去进⾏回调,是由我们⾃⼰编写代码完成的。Netty相⽐于Java NIO来说,在事件处理器这个⾓⾊上进⾏了⼀个升级,它为我们开发者提供了⼤量的回调⽅法,供我们在特定事件产⽣时实现相应的回调⽅法进⾏业务逻辑的处理,即,ChannelHandler。ChannelHandler中的⽅法对应的都是⼀个个事件的回调。
Concrete Event Handler(具体事件处理器):是事件处理器的实现。它本⾝实现了事件处理器所提供的各种回调⽅法,从⽽实现了特定于业务的逻辑。它本质上就是我们所编写的⼀个个的处理器实现。
Initiation Dispatcher(初始分发器):实际上就是Reactor⾓⾊。它本⾝定义了⼀些规范,这些规范⽤于控制事件的调度⽅式,同时⼜提供了应⽤进⾏事件处理器的注册、删除等设施。它本⾝是整个事件处理器的核⼼所在,Initiation Dispatcher会通过Synchronous Event Demultiplexer来等待事件的发⽣。⼀旦事件发⽣,Initiation Dispatcher⾸先会分离出每⼀个事件,然后调⽤事件处理器,最后调⽤相关的回调⽅法来处理这些事件。Netty中ChannelHandler⾥的⼀个个回调⽅法都是由bossGroup或workGroup中的某个EventLoop来调⽤的。
Reactor模式流程
①初始化Initiation Dispatcher,然后将若⼲个Concrete Event Handler注册到Initiation Dispatcher中。当
应⽤向Initiation Dispatcher注册
Concrete Event Handler时,会在注册的同时指定感兴趣的事件,即,应⽤会标识出该事件处理器希望Initiation Dispatcher在某些事件发⽣时向其发出通知,事件通过Handle来标识,⽽Concrete Event Handler⼜持有该Handle。这样,事件 ————> Handle ————> Concrete Event Handler 就关联起来了。
② Initiation Dispatcher 会要求每个事件处理器向其传递内部的Handle。该Handle向操作系统标识了事件处理器。
③当所有的Concrete Event Handler都注册完毕后,应⽤会调⽤handle_events⽅法来启动Initiation Dispatcher的事件循环。这是,Initiation Dispatcher会将每个注册的Concrete Event Handler的Handle合并起来,并使⽤Synchronous Event Demultiplexer(同步事件分离器)同步阻塞的等待事件的发⽣。⽐如说,TCP协议层会使⽤select同步事件分离器操作来等待客户端发送的数据到达连接的socket handler上。
⽐如,在Java中通过Selector的select()⽅法来实现这个同步阻塞等待事件发⽣的操作。在Linux操作系统下,select()的实现中 a)会将已经注册到Initiation Dispatcher的事件调⽤epollCtl(epfd, opcode, fd, events)注册到linux系统中,这⾥fd表⽰Handle,events表⽰我们所感兴趣的Handle的事件;b)通过调⽤
epollWait⽅法同步阻塞的等待已经注册的事件的发⽣。不同事件源上的事件可能同时发⽣,⼀旦有事件被触发了,epollWait⽅法就会返回;c)最后通过发⽣的事件到相关联的SelectorKeyImpl对象,并设置其发⽣的事件为就绪状态,然后将SelectorKeyImpl放⼊selectedSet中。这样⼀来我们就可以通过Selector.selectedKeys()⽅法得到事件就绪的SelectorKeyImpl集合了。
④当与某个事件源对应的Handle变为ready状态时(⽐如说,TCP socket变为等待读状态时),Synchronous Event Demultiplexer就会通知Initiation Dispatcher。
⑤ Initiation Dispatcher会触发事件处理器的回调⽅法,从⽽响应这个处于ready状态的Handle。当事件发⽣时,Initiation Dispatcher会将被事件源激活的Handle作为『key』来寻并分发恰当的事件处理器回调⽅法。
⑥ Initiation Dispatcher会回调事件处理器的handle_event(type)回调⽅法来执⾏特定于应⽤的功能(开发者⾃⼰所编写的功能),从⽽相应这个事件。所发⽣的事件类型可以作为该⽅法参数并被该⽅法内部使⽤来执⾏额外的特定于服务的分离与分发。
Reactor模式的实现⽅式
单线程Reactor模式
流程:
①服务器端的Reactor是⼀个线程对象,该线程会启动事件循环,并使⽤Selector来实现IO的多路复⽤。注册⼀个Acceptor事件处理器到Reactor中,Acceptor事件处理器所关注的事件是ACCEPT事件,这样Reactor会监听客户端向服务器端发起的连接请求事件(ACCEPT事件)。
②客户端向服务器端发起⼀个连接请求,Reactor监听到了该ACCEPT事件的发⽣并将该ACCEPT事件派发给相应的Acceptor处理器来进⾏处理。Acceptor处理器通过accept()⽅法得到与这个客户端对应的连接(SocketChannel),然后将该连接所关注的READ事件以及对应的READ事件处理器注册到Reactor中,这样⼀来Reactor就会监听该连接的READ事件了。或者当你需要向客户端发送数据时,就向Reactor注册该连接的WRITE事件和其处理器。
③当Reactor监听到有读或者写事件发⽣时,将相关的事件派发给对应的处理器进⾏处理。⽐如,读处理器会通过SocketChannel的read()⽅法读取数据,此时read()操作可以直接读取到数据,⽽不会堵塞与等待可读的数据到来。
④每当处理完所有就绪的感兴趣的I/O事件后,Reactor线程会再次执⾏select()阻塞等待新的事件就绪并将其分派给对应处理器进⾏处理。
注意,Reactor的单线程模式的单线程主要是针对于I/O操作⽽⾔,也就是所以的I/O的accept()、read()、write()以及connect()操作都在⼀个线程上完成的。
但在⽬前的单线程Reactor模式中,不仅I/O操作在该Reactor线程上,连⾮I/O的业务操作也在该线程上进⾏处理了,这可能会⼤⼤延迟I/O请求的响应。所以我们应该将⾮I/O的业务逻辑操作从Reactor线程上卸载,以此来加速Reactor线程对I/O请求的响应。
改进:使⽤⼯作者线程池
与单线程Reactor模式不同的是,添加了⼀个⼯作者线程池,并将⾮I/O操作从Reactor线程中移出转交给⼯作者线程池来执⾏。这样能够提⾼Reactor线程的I/O响应,不⾄于因为⼀些耗时的业务逻辑⽽延迟对后⾯I/O请求的处理。
使⽤线程池的优势:
①通过重⽤现有的线程⽽不是创建新线程,可以在处理多个请求时分摊在线程创建和销毁过程产⽣的巨⼤开销。
②另⼀个额外的好处是,当请求到达时,⼯作线程通常已经存在,因此不会由于等待创建线程⽽延迟任务的执⾏,从⽽提⾼了响应性。
③通过适当调整线程池的⼤⼩,可以创建⾜够多的线程以便使处理器保持忙碌状态。同时还可以防⽌过多线程相互竞争资源⽽使应⽤程序耗尽内存或失败。
注意,在上图的改进的版本中,所以的I/O操作依旧由⼀个Reactor来完成,包括I/O的accept()、read()、write()以及connect()操作。
对于⼀些⼩容量应⽤场景,可以使⽤单线程模型。但是对于⾼负载、⼤并发或⼤数据量的应⽤场景却不合适,主要原因如下:
①⼀个NIO线程同时处理成百上千的链路,性能上⽆法⽀撑,即便NIO线程的CPU负荷达到100%,也⽆法满⾜海量消息的读取和发送;
②当NIO线程负载过重之后,处理速度将变慢,这会导致⼤量客户端连接超时,超时之后往往会进⾏重发,这更加重了NIO线程的负载,最终会导致⼤量消息积压和处理超时,成为系统的性能瓶颈;
多Reactor线程模式
Reactor线程池中的每⼀Reactor线程都会有⾃⼰的Selector、线程和分发的事件循环逻辑。
mainReactor可以只有⼀个,但subReactor⼀般会有多个。mainReactor线程主要负责接收客户端的连接请求,然后将接收到的SocketChannel传递给subReactor,由subReactor来完成和客户端的通信。
流程:
①注册⼀个Acceptor事件处理器到mainReactor中,Acceptor事件处理器所关注的事件是ACCEPT事件,这样mainReactor会监听客户端向服务器端发起的连接请求事件(ACCEPT事件)。启动mainReactor的事件循环。
②客户端向服务器端发起⼀个连接请求,mainReactor监听到了该ACCEPT事件并将该ACCEPT事件派发给Acceptor处理器来进⾏处理。Acceptor处理器通过accept()⽅法得到与这个客户端对应的连接(SocketChannel),然后将这个SocketChannel传递给subReactor线程池。
③ subReactor线程池分配⼀个subReactor线程给这个SocketChannel,即,将SocketChannel关注的READ事件以及对应的READ事件处理器注册到subReactor线程中。当然你也注册WRITE事件以及WRITE事件处理器到subReactor线程中以完成I/O写操作。Reactor线程池中的每⼀Reactor线程都会有⾃⼰的Selector、线程和分发的循环逻辑。
④当有I/O事件就绪时,相关的subReactor就将事件派发给响应的处理器处理。注意,这⾥subReactor线程只负责完成I/O的read()操作,在读取到数据后将业务逻辑的处理放⼊到线程池中完成,若完成业务逻辑后需要返回数据给客户端,则相关的I/O的write操作还是会被提交回subReactor线程来完成。
注意,所以的I/O操作(包括,I/O的accept()、read()、write()以及connect()操作)依旧还是在Reactor线程(mainReactor线程或 subReactor线程)中完成的。Thread Pool(线程池)仅⽤来处理⾮I/O操作的逻辑。
多Reactor线程模式将“接受客户端的连接请求”和“与该客户端的通信”分在了两个Reactor线程来完成。mainReactor完成接收客户端连接请求的操作,它不负责与客户端的通信,⽽是将建⽴好的连接转交给subReactor线程来完成与客户端的通信,这样⼀来就不会因为read()数据量太⼤⽽导致后⾯的客户端连接请求得不到即时处理的情况。并且多Reactor线程模式在海量的客户端并发请求的情况下,还可以通过实现subReactor线程池来将海量的连接分发给多个subReactor线程,在多核的操作系统中这能⼤⼤提升应⽤的负载和吞吐量。
Netty 与 Reactor模式
Netty的线程模式就是⼀个实现了Reactor模式的经典模式。
结构对应:
NioEventLoop ———— Initiation Dispatcher
Synchronous EventDemultiplexer ———— Selector
Evnet Handler ———— ChannelHandler
ConcreteEventHandler ———— 具体的ChannelHandler的实现
模式对应:
Netty服务端使⽤了“多Reactor线程模式”
mainReactor ———— bossGroup(NioEventLoopGroup) 中的某个NioEventLoop
subReactor ———— workerGroup(NioEventLoopGroup) 中的某个NioEventLoop
acceptor ———— ServerBootstrapAcceptor
ThreadPool ———— ⽤户⾃定义线程池
流程:
①当服务器程序启动时,会配置ChannelPipeline,ChannelPipeline中是⼀个ChannelHandler链,所有的事件发⽣时都会触发
Channelhandler中的某个⽅法,这个事件会在ChannelPipeline中的ChannelHandler链⾥传播。然后,从bossGroup事件循环池中获取⼀个NioEventLoop来现实服务端程序绑定本地端⼝的操作,将对应的Se
rverSocketChannel注册到该NioEventLoop中的Selector上,并注册ACCEPT事件为ServerSocketChannel所感兴趣的事件。
② NioEventLoop事件循环启动,此时开始监听客户端的连接请求。
③当有客户端向服务器端发起连接请求时,NioEventLoop的事件循环监听到该ACCEPT事件,Netty底层会接收这个连接,通过
accept()⽅法得到与这个客户端的连接(SocketChannel),然后触发ChannelRead事件(即,ChannelHandler中的channelRead⽅法会得到回调),该事件会在ChannelPipeline中的ChannelHandler链中执⾏、传播。
④ ServerBootstrapAcceptor的readChannel⽅法会该SocketChannel(客户端的连接)注册到workerGroup(NioEventLoopGroup)
中的某个NioEventLoop的Selector上,并注册READ事件为SocketChannel所感兴趣的事件。启动SocketChannel所在
NioEventLoop的事件循环,接下来就可以开始客户端和服务器端的通信了。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论