进一步看,当上升到整个网络模块时,另一个常常听说的模式出现了 ---- 「Reactor 模式」,也叫反应器模式,本质是一个事件转发器,是网络模块核心中枢,负责将读写事件分发给对应的读写事件处理者。
大名鼎鼎的 Java 并发包作者 Doug Lea,在 Scalable I/O in Java 一文中阐述了服务端开发中 I/O 模型的演进过程。netty 中三种 reactor 线程模型也来源于这篇经典文章。
通常情况下,reactor 模式的网络 IO 层会采用经典的 IO 多路复用方案,强强联手,妥妥的高性能的代名词、扛把子!!!是众多开源项目普遍的解决方案。
可以看到,网络请求先后经历 服务器网卡、内核、连接建立、数据读取、业务处理、数据写回等一系列过程。
其中,连接建立(accept)、数据读取(read)、数据写回(write)等操作都需要操作系统内核提供的系统调用,最终由内核与网卡进行数据交互,这些 IO 调用消耗一般是比较高的,比如 IO 等待、数据传输等。
最初的处理方式是,每个连接都用独立的一个线程来处理这一系列的操作,即 建立连接、数据读写、业务逻辑处理;这样一来最大的弊端在于,N 个连接就需要 N 个线程资源,消耗巨大。
所以,在网络模型演化过程中,不断的对这几个阶段进行拆分,比如,将建立连接、数据读写、业务逻辑处理等关键阶段分开处理。这样一来,每个阶段都可以考虑使用单线程或者线程池来处理,极大的节约线程资源,又能获得超高性能。
非阻塞IO :与阻塞 IO 相反,如果数据未就绪会直接返回,应用层轮询读取/查询,直到成功读取数据。
I/O多路复用: 是非阻塞IO的一种特例,也是目前最经典、最常用的高性能IO模型。其具体处理方式是:先查询 IO 事件是否准备就绪,当 IO 事件准备就绪了,则会真正的通过系统调用实现数据读写;
查询操作,不管是否数据准备就绪都会立即返回,即非阻塞;因此,通常情况下,会通过轮训来不断监听 IO 事件是否准备就绪;因为操作是非阻塞的,这个过程中通常只需及少量线程(一般一个线程即可)来处理这个轮训操作,极大的解决阻塞模式下 IO 枯竭问题。
这种一个线程就可以监听所有网络连接的 IO 事件是否准备就绪的模式,就是大名鼎鼎的IO多路复用。
需要C/C++ Linux服务器架构师学习资料加qun812855908获取(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享
事件驱动的核心是,以事件为连接点,当有IO事件准备就绪时,以事件的形式通知相关线程进行数据读写,进而业务线程可以直接处理这些数据,这一过程的后续操作方,都是被动接收通知,看起来有点像回调操作;
这种模式下,IO 读写线程、业务线程工作时,必有数据可操作执行,不会在 IO 等待上浪费资源,这便是事件驱动的核心思想。
举个简单例子,10个士兵接到命令,在接下来将执行秘密任务,但具体时间待定;一种方式时,这10个士兵自己掌握主动权,隔一段时间就会自己询问将军是否准备执行任务,这种模式比较低下,因为士兵需要花很多精力自己去确认任务执行时间,同时也会耽搁自己的训练时间。
另一种方式为,士兵接到即将执行秘密任务的通知后,会自己做好准备随时执行,在最终执行命名没下达之前,会继续自己的日常训练;等需要执行任务时,将军会立刻通知士兵们立即行动;很显然,这种模式,士兵们的时间资源并没有浪费。这便是事件驱动的优势所在。
好了,到此相信你已经明白了什么是事件驱动了。Reactor 模型的核心便是事件驱动,同时,为了让其网络 IO 层拥有了高性能的能力,一般会采用 IO 多路复用处理方案。
Reactor 模式由 Reactor 线程、Handlers 处理器两大角色组成,两大角色的职责分别如下:
Reactor 线程的职责:主要负责连接建立、监听IO事件、IO事件读写以及将事件分发到Handlers 处理器。
Handlers 处理器(业务处理)的职责:非阻塞的执行业务处理逻辑。
从某些方面来说,其实主要有单线程
和多线程
两种模型;其中,多线程模型就包含了多线程模型(Woker线程池)和主从多线程模型。多线程 Reactor 的演进分为两个方面:
而主从多线程模型,将 建立连接 和 IO事件监听/读写以及事件分发 两部分用不同的线程处理,这样各司其职,能有效利用系统多核资源;同时为提高事件处理的效率,通常可以使用线程池来处理 IO事件监听/读写以及事件分发这部分操作。
另外,主从多线程模型通常情况下,Worker 端也会采用线程池来处理业务。这样一看,这三种 Reactor 模型其实是层层递进,不断的提升系统的吞吐量。当然,这一系列变换使用都需要结合实际场景考虑,但终究万变不离其宗。
接下来我们将详细分析这几种模型,继续往下看。
由于单线程模型有性能方面的瓶颈,多线程模型作为解决方案就应运而生了。
Reactor 多线程模型将业务逻辑交给多个线程进行处理。除此之外,多线程模型其他的操作与单线程模型是类似的,比如连接建立、IO事件读写以及事件分发等都是由一个线程来完成。
当客户端有数据发送至服务端时,Select 会监听到可读事件,数据读取完毕后提交到业务线程池中并发处理。
一般的请求中,耗时最长的一般是业务处理,所以用一个线程池(worker 线程池)来处理业务操作,在性能上的提升也是非常可观的。
当然,这种模型也有明显缺点,连接建立、IO 事件读取以及事件分发完全有单线程处理;比如当某个连接通过系统调用正在读取数据,此时相对于其他事件来说,完全是阻塞状态,新连接无法处理、其他连接的 IO、查询 IO 读写以及事件分发都无法完成。
对于像 Nginx、Netty 这种对高性能、高并发要求极高的网络框架,这种模式便显得有些吃力了。因为,无法及时处理新连接、就绪的 IO 事件以及事件转发等。
简言之,主从多线程模型由多个 Reactor 线程组成,每个 Reactor 线程都有独立的 Selector 对象。MainReactor 仅负责处理客户端连接的 Accept 事件,连接建立成功后将新创建的连接对象注册至 SubReactor。再由 SubReactor 分配线程池中的 I/O 线程与其连接绑定,它将负责连接生命周期内所有的 I/O 事件。
在海量客户端并发请求的场景下,主从多线程模式甚至可以适当增加 SubReactor 线程的数量,从而利用多核能力提升系统的吞吐量。
将负责数据传输处理的 IOHandler 处理器的执行放入独立的线程池中。这样,业务处理线程与负责新连接监听的反应器线程就能相互隔离,避免服务器的连接监听受到阻塞。
如果服务器为多核的 CPU,可以将反应器线程拆分为多个子反应器(SubReactor)线程;同时,引入多个选择器,并且为每一个SubReactor引入一个线程,一个线程负责一个选择器的事件轮询。这样充分释放了系统资源的能力,也大大提升了反应器管理大量连接或者监听大量传输通道的能力。
Reactor(反应器)模式是高性能网络编程在设计和架构层面的基础模式,算是基础的原理性知识。只有彻底了解反应器的原理,才能真正构建好高性能的网络应用、轻松地学习和掌握高并发通信服务器与框架(如Nginx服务器)。