在高性能的I/O设计中,有两个比较著名的模式Reactor和Proactor模式,其中Reactor模式用于同步I/O,Proactor用于异步I/O操作。
Reactor模式称之为响应器模式,通常用于NIO非阻塞IO的网络通信框架中。
C/C++ Linux后台服务器开发高级架构师免费学习链接:C/C++Linux服务器开发高级架构师/Linux后台架构师-学习视频
epoll原理剖析以及reactor模型应用
linux epoll网络编程细节处理
几个概念:
阻塞和非阻塞是针对于进程在访问数据时,根据IO操作的就绪状态而采取的不同方式,简单来说是一种读取或写入操作函数的实现方式,阻塞方式下读取或写入函数将一直等待。非阻塞方式下,读取和写入函数会立即返回一个状态值。
同步和异步是针对应用程序和内核的交互而言的,同步是指用户进程触发IO操作并等待或轮询的查看IO操作是否就绪,异步是指用户进程触发IO操作以后便开始做自己的事情,当IO操作完成时会得到通知,换句话说异步的特点就是通知。
一般而言,IO模型可以分为四种:同步阻塞、同步非阻塞、异步阻塞、异步非阻塞
NIO非阻塞IO处理流程
NIO非阻塞IO的优点在于性能瓶颈高,缺点在于模型复杂、编码复杂、需要处理半包问题。简单来说非阻塞IO不需要一个连接建立一个线程,它可以在一个线程中处理所有的连接。但是由于是非阻塞的,所以应用无法知道什么时候消息读完了,也就会存在半包的问题。
什么是半包问题呢?
TCP/IP在发送消息时可能会拆包,拆包会导致接收端无法得知什么时候接收到的数据是一个完整的数据。在BIO阻塞性IO模型中,当读取步到数据后会阻塞,而在NIO非阻塞IO中则不会,所以需要自行进行处理。比如以换行符作为判断依据,或者是定长消息发送,或者是自定义协议等。
什么是Reactor模式?
Reactor模式是处理并发I/O常见的一种模式,用于同步I/O,其中心思想是将所有要处理的I/O事件注册到一个中心I/O多路复用器上,同时主线程阻塞在多路复用器上,一旦有I/O事件到来或是准备就绪,多路复用器将返回并将相应I/O事件分发到对应的处理器中。
Reactor是一种事件驱动机制,和普通函数调用不同的是应用程序不是主动的调用某个API来完成处理,恰恰相反的是Reactor逆置了事件处理流程,应用程序需提供相应的接口并注册到Reactor上,如果有相应的事件发生,Reactor将主动调用应用程序注册的接口(回调函数)。
The reactor design pattern is an event handling pattern for handling service requests delivered concurrently by one or more inputs. The service handler then demultiplexes the incoming requests and dispatches them synchronously to associated request handlers.
Reactor
Reactor模式称为反应器模式或应答者模式,是基于事件驱动的设计模式,拥有一个或多个并发输入源,有一个服务处理器和多个请求处理器,服务处理器会同步的将输入的请求事件以多路复用的方式分发给相应的请求处理器。
Reactor设计模式是一种为处理并发服务请求,并将请求提交到一个或多个服务处理程序的事件设计模式。当客户端请求抵达后,服务处理程序使用多路分配策略,由一个非阻塞的线程来接收所有请求,然后将请求派发到相关的工作线程并进行处理的过程。
在事件驱动的应用中,将一个或多个客户端的请求分离和调度给应用程序,同步有序地接收并处理多个服务请求。对于高并发系统经常会使用到Reactor模式,用来替代常用的多线程处理方式以节省系统资源并提高系统的吞吐量。
关于C/C++ Linux后端开发网络底层原理知识 点击 后端开发学习资料 获取,内容知识点包括Linux,Nginx,ZeroMQ,MySQL,Redis,线程池,MongoDB,ZK,Linux内核,CDN,P2P,epoll,Docker,TCP/IP,协程,DPDK等等。C/C++ Linux后台服务器开发高级架构师免费学习链接:C/C++Linux服务器开发高级架构师/Linux后台架构师-学习视频
什么是C/S架构?
C/S架构
C/S架构的工作流程
什么是套接字Socket?
Socket之间建立连接并通信的过程
C/S架构中Socket之间是如何建立连接并通信的呢?
当前分布式计算Web服务盛行天下,网络服务的底层都离不开对Socket的操作,而它们都具有一个共同的结构。
不同于传统IO的串行调度方式,NIO非阻塞IO操作会将整个服务请求划分为五个阶段。
在网络服务和分布式中对于网络中请求的处理,处理流程大致可划分为五个阶段。
在这五个阶段中,以read和send阶段IO操作最为频繁。
网络中请求的处理流程
在处理网络请求时,通常具有两种体系结构。
基于线程的体系结构会使用多线程来处理客户端的请求,每当接收一个请求便开启一个独立的线程来处理。这种方式虽然简单直观,但仅适用于并发访问不大的场景。因为线程是需要占用一定的内存资源,而且操作系统在线程之间的切换也需要一定的开销。当线程过多时显然会降低网络服务器的性能。另外,当线程在处理IO操作时,在等待输出的这段时间内线程是处于空闲状态,造成CPU资源浪费。
事件驱动体系结构是目前广泛使用的一种方式,这种方式定义了一系列的事件处理程序来响应事件的发生,而且将服务端接收连接和事件处理分离,事件本身只是一种状态的改变。在事件驱动的应用中,会将一个或多个客户端的服务请求分离demultiplex和调度dispatch给应用程序。
Reactor设计模式是event-driven architecture的一种实现方式,用于处理多个客户端并发的向服务器请求服务的场景。每种服务在服务器上可能由多个方法组成。Reactor会解耦并发请求的服务并分发给对应的时间处理器来处理。
从结构上看,Reactor类似于生产消费模式,也就是一个或多个生产者会将事件放入一个队列中,一个或多个消费者主动从队列中poll拉取事件进行处理。Reactor并没有使用队列来做缓冲,每当一个事件输入到服务处理程序之后,服务处理程序会主动根据不同的事件类型将其分发给对应的请求处理程序进行处理。
Reactor模式和生产者和消费者之间最大的区别在于
Reactor模式的优点很明显:解耦、提升复用性、模块化、可移植性、事件驱动、细粒度的开发控制等。Reactor模式的缺点也很明显:模型复杂,涉及到内部回调、多线程处理、不容易调试、需要操作系统底层支持,因此导致不同操作系统可能会产生不一样的结果。总来而言,如果并发要求不是很高,可使用传统的阻塞线程池足够了。如果使用场景是产生瞬间大并发可使用Reactor模式来实现。
最原始的网络编程思路是服务器使用一个while循环并不断监听端口是否有新的socket套接字连接,如果有就会去调用一个处理函数。
while(true)
{
socket = accept();
handle(socket);
}
这种方式最大的问题是无法并发且效率太低,如果当前请求没有处理完毕后续请求只能被阻塞,因此服务器的吞吐量太低。
导致服务器阻塞的原因是什么呢?
由于IO在阻塞时会处于等待状态,因此在用户负载增加时,性能下降的非常快。
改进的方式是使用多线程,也就是经典的connection per thread,每一个连接拥有一个线程处理。
while(true)
{
socket = accept();
new thread(socket);
}
对于传统的服务设计,每个抵达的请求系统会分配一个线程去处理,Tomcat服务器早期版本是这样实现的。
当系统请求量瞬间暴增时(高并发情况下),会直接把系统拖垮,因为系统能够创建线程的数量是有限的。
多线程并发模式采用一个连接一个线程的方式,优点是确实一定程度上提高了服务器的吞吐量,因为之前的请求在read读阻塞后不会影响到后续的请求,由于它们在不同的线程中,而且一个线程只能对应一个套接字socket,每一个套接字socket都是阻塞的,所以一个线程中只能处理一个套接字。就算accept多个socket,如果前一个socket被阻塞其后的socket是无法被执行到的。
多线程的服务器模型
多线程并发模式的缺点在于资源要求太高,系统中创建线程是需要消耗系统资源的,如果连接数过高系统将无法承受。另外,线程反复被创建和销毁也是需要代价的。
关于C/C++ Linux后端开发网络底层原理知识 点击 后端开发学习资料 获取,内容知识点包括Linux,Nginx,ZeroMQ,MySQL,Redis,线程池,MongoDB,ZK,Linux内核,CDN,P2P,epoll,Docker,TCP/IP,协程,DPDK等等。
虽然利用线程池可以缓解线程创建和销毁的代价,不过还是存在一些问题,线程的粒度太大。每一个线程会将一次交互操作全部处理完成,包括读取和返回甚至是连接。表面上似乎连接不在线程里面,但是如果线程不够,新连接将无法得到处理。所以线程的任务可以简化为做三件事:连接、读取、写入。
显然传统一对一的线程处理无法满足需求的变化,对此考虑使用线程池使得线程可以被复用,大大降低创建线程和销毁线程的时间。然而,线程池并不能很好满足高并发线程的需求。当海量请求抵达时线程池中的工作线程达到饱和状态,此时可能就导致请求被抛弃,无法完成客户端的请求。对此,考虑到将一次完整的请求切分为几个小的任务,每个小任务都是非阻塞的,对于读写操作使用NIO非阻塞IO对其进行读写,不同的任务将被分配到与之关联的处理程序上进行处理,每个处理器通过异步回调机制来实现。这样可以大大提高系统吞吐量,减少响应时间。
由于线程同步的粒度太大限制了吞吐量,所以应该将一次连接操作拆分为更细的粒度或过程,这些更细的粒度则是更小的线程。这样做之后,整个线程池中线程的数量将会翻倍增加,但线程更加简单且任务更为单一。这也是Reactor出现的原因。
在Reactor中这些被拆分的小线程或子过程对应的处理程序,每一种处理程序会去处理一种事件。Reactor中存在一个全局管理者Selector,开发者需要将Channel注册到感兴趣的事件上,Selector会不断在Channel上检测是否有该类型的事件发生,如果没有主线程会被阻塞,否则会调用相应的事件处理函数来处理。
由于典型的事件包括连接、读取、写入,因此需要为这些事件分别提供对应的处理程序,每个处理程序可以采用线程的方式实现。一旦连接来了,而且显示被读取线程或处理程序处理了,则会再执行写入。那么之前的读取就可以被后面的请求复用,因此吞吐量就提高了。
传统的thread per connection中线程在真正处理请求之间是需要从socket中读取网络请求,由于读取完成之前线程本身是被阻塞的不能做任何事情,这就导致线程资源被占用,而线程资源本身很珍贵的,尤其是在处理高并发请求时。Rector模式指出在等待IO时,线程可以先退出,这样就会因为有线程等待IO而占用资源。但是这样原先的执行流程就没法还原了。因此可以利用事件驱动的方式,要求线程在退出之前向event loop事件循环中注册回调函数,这样IO完成时event loop事件循环就可以调用回调函数完成剩下的操作。所以Reactor模式通过减少服务器的资源消耗提供并发能力。
Reactor从线程池和Reactor的选择上可细分为:Reactor单线程模型、Reactor多线程模型,Reactor主从模型
单线程的Reactor模式对于客户端的所有请求使用一个专门的线程去处理,这个线程无限循环地监听是否有客户端的请求抵达,一旦收到客户端的请求,就将其分发给响应处理程序进行处理。
事件驱动设计
采用基于事件驱动的设计,当有事件触发时才会调用处理器进行数据处理。使用Reactor模式可以对线程的数量进行控制,可以使用一个线程去处理大量的事件。
-Reactor 负责响应IO事件,当检测到一个新的事件会将其发送给相应的处理程序去处理。
Each handler may be started in its own thread
Reactor是单个线程,需要处理accept连接,同时发送请求到处理器中。由于只是单个线程,所以处理器中的业务需要能够快速处理完毕。
单线程的Reactor
单线程的Reactor与NIO流程类似,只是将消息相关处理独立到Handler中。虽然NIO中一个线程可以支持所有的IO处理,但瓶颈也是显而易见的。如果某个客户端多次进行请求时在Handler中的处理速度较慢,那么后续的客户端请求都会被积压,导致响应变慢。所以需要引入Reactor多线程模型。
单线程的Reactor的特点是只有一个Reactor线程,也就是说只有一个Selector事件通知器,因此字节的读取I/O和后续的业务处理process()均由Reactor线程来做,很显然业务的处理影响后续事件的分发,所以引出多线程版本进行优化。
从性能角度来看,单线程的Reactor没有过多的提升空间,因为IO和CPU的速度严重不匹配。
单线程的Reactor模式并没有解决IO和CPU处理速度不匹配问题,所以多线程的Reactor模式引入了线程池的概念,将耗时的IO操作交由线程池处理,处理完毕后再同步到selectionkey中。
考虑到工作线程的复用,可以将工作线程设计线程池。将处理器的执行放入线程池,并使用多线程处理业务逻辑,Reactor仍然是单个线程。
使用多线程处理业务逻辑
Reactor读线程模型是将Handler中的IO操作和非IO操作分开,操作IO的线程称为IO线程,非IO操作的线程称为工作线程。客户端的请求会被直接丢到线程池中,因此不会发生堵塞。
Reactor多线程模型
多线程的Reactor的特点是一个Reactor线程和多个处理线程,将业务处理即process交给线程池进行了分离,Reactor线程只关注事件分发和字节的发送和读取。需要注意的是,实际的发送和读取还是由Reactor来处理。当在高并发环境下,有可能会出现连接来不及接收。
当用户进一步增加时Reactor也会出现瓶颈,因为Reactor既要处理IO操作请求也要响应连接请求。为了分担Reactor的负担,可以引入主从Reactor模型。
主从Reactor模型
对于多个CPU的机器,为了充分利用系统资源会将Reactor拆分为两部分。
多处理器
主从Reactor的特点是使用 一个Selector池,通常有一个主Reactor用于处理接收连接事件,多个从Reactor处理实际的IO。整体来看,分工合作,分而治之,非常高效。
为什么需要单独拆分一个Reactor来处理监听呢?
因为像TCP这样需要经过3次握手才能建立连接,这个建立的过程也是需要消耗时间和资源的,单独拆分一个Reactor来处理,可以提高性能。
Reactor模式的核心是解决多请求问题,如果有特别多的请求同时发生,不会因为线程池被短时间占满而拒绝服务。
Reactor模式的优点是什么呢?
Reactor模式的缺点是什么呢?
Reactor中的核心组件有哪些呢?
在Reactor模式中有五个关键的参与者:描述符handle、同步事件分离器demultiplexer、事件处理器接口event handler、具体的事件处理器、Reactor管理器
Reactor的结构
Reactor网路编程设计模式
Reactor模式要求主线程(I/O处理单元)只负责监听文件描述符上是否有事件发生,如果有的话立即将该事件通知给工作线程(逻辑单元)。除此之外,主线程不做任何其它实质性的工作。读写数据、接收新连接、处理客户端请求均在工作线程中完成。
Handle在Linux中一般称为文件描述符,在Windows中称为句柄,两者含义一样。Handle是事件的发源地。比如网络socket、磁盘文件等。发生在Handle上的事件可以有connection、ready for read、ready for write等。
Handle是操作系统的句柄,是对资源在操作系统上的一种抽象,它可以是打开的文件、一个Socket连接、Timer定时器等。由于Rector模式一般使用在网络编程中,因而这里一般指的是Socket Handle,也就是一个网络连接(connection/channel)。这个channel注册到同步事件分离器中,以监听Handle中发生的事件,对ServerSocketChannel可以是CONNECT事件,对SocketChannel可以是read、write、close事件等。
同步事件分离器本质上是系统调用,比如Linux中的select、poll、epoll等。比如select()方法会一致阻塞直到文件描述符handle上有事件发生时才会返回。
无限循环等待新请求的到来,一旦发现有新的事件到来就会通知初始事件分发器去调取特定的时间处理器。
事件处理器,定义一些回调方法或称为钩子函数,当handle文件描述符上有事件发生时,回调方法便会执行。供初始事件分发器回调使用。
具体的事件处理器,实现了Event Handler,在回调方法中实现具体的业务逻辑。
初始事件分发器,提供了注册、删除、转发Event Handler的方法。当Synchronous Event Demultiplexer检测到handler上有事件发生时,便会通知initiation dispatcher调用特定的event handler的回调方法。
初始事件分发器用于管理Event Handler,定义注册、移除EventHandler等。它还作为Rector模式的入口调用Synchronous Event Demultiplexer同步多路事件分离器的select方法以阻塞等待事件返回,当阻塞等待返回时,根据事件发生的Handle将其分发给对应的Event Handle事件处理器进行处理,也就是回调EventHandler中的handle_event方法。
IO框架工作时序图
事件多路分解器
现代操作系统大多提供了一种本机机制,该机制通过一种有效的方式处理并发和非阻塞资源,这种机制称为同步事件多路分解器或事件通知接口。
Reactor启动流程
使用同步IO模型(以epoll_wait为例)实现的Reactor模式的工作流程
Reactor模式的工作流程
例如:使用Reactor实现的日志服务器
日志服务器中的Reactor模式实现分为两部分
客户端连接到日志服务器
客户端向日志服务器写入日志
例如:需要建立一个提供分布式日志服务的事件驱动服务器,客户端向服务器发送请求记录自己的状态信息,信息包括错误通知、调试信息、表现诊断等。日志服务器对于收到的信息进行分类和分发,具体包括显示屏显示、打印机打印、数据库存储等。
日志服务器
为了保证数据可靠性,客户端和服务器之间的通信协议通常选用TCP等面向连接的协议,通过IP和端口的四元组来确认客户端和服务器。日志服务器被多个客户端同时使用,为此日志服务器需要保证多用户连接请求和日志记录的并发性。
为了保证并发性,可采用多线程的方式去实现该服务器,即每个线程专门针对一个连接。然而使用多线程的方式实现服务器存在着以下问题:
多线程导致的上下文切换、同步、数据移动等可能带来效率的下降。
多线程需要考虑复杂的并发设计,包括线程安全等诸多因素。
多线程在不同的操作系统下是不同的,因此会影响到可移植性。