1.Reactor/Dispatcher模式
1.1 概述
Reactor模式下,服务端的构成为Reactor + 处理资源池
。其中,Reactor负责监听和分发事件,而处理资源池则负责处理事件。
该模式下的组合方案有下面几种(第三种几乎没有被实际应用):
- 1 * Reactor + 1 * Worker。
- 1 * Reactor + n * Worker。
n * Reactor + 1 * Worker。- n * Reactor + n * Worker。
注:
Worker
是指负责处理事件的工作进程/线程。
n * Reactor + 1 * Worker 没有应用的原因(类比
Redis
为什么始终坚持单线程处理(执行)指令的原因):
- 复杂且没有性能优势。
- 复杂:多个Reactor去接收用户请求,这就需要处理好线程并发问题。
- 没有性能优势:多线程并发,如果是在单核上,可能会面临
频繁的线程上下文切换
,开销较大。其次,由于我们基本上是在内存中进行请求接收的,因此主要的瓶颈不在于线程数量而在于网络时延&带宽。- 多个
reactor
去负责接待请求,而真正服务请求的时候确实串行的,你不觉得?。。。
1.2 实现方式
1.2.1 1 * Reactor + 1 * Worker
1.2.1.1 概述
该模式下的三种角色:
- reactor:负责监听和分发事件。
- acceptor:获取连接。
- handler:处理事件。
执行流程:
reactor
通过select
系统调用持续监听IO事件,若监听到有事件发生,则根据事件类型进行分情况处理:- 若为连接事件:则将该事件**分发(dispatch)**给
acceptor
处理,acceptor在收到该事件后,会通过accept
系统调用新建一个连接并同时创建一个handler
来处理后续的事件。 - 若不是连接事件:则交由
handler
进行事件处理。
- 若为连接事件:则将该事件**分发(dispatch)**给
1.2.1.2 评估
- 该方案不适用于计算密集型场景,仅适用于业务耗时短的场景。
- 由于全程采用单进程/线程监听&处理任务,因此无法充分利用多核CPU的性能。而且,一旦事件消费的过程中,某个事件的消费耗时特别长,它将影响到后续事件的响应,一般体现在延迟的增加方面。
C语言编写的服务端程序,其模式为
1 * Reactor + 1 * 进程
,而Java的则是1 * Reactor + 1 * 线程
(JVM是一个进程,你所写好的Java程序是其中的一个线程)。
Redis 6.0- 采用的是1 * Reactor + 1 * 进程
。
1.2.2 1 * Reactor + n * Worker
1.2.2.1 概述
与前面不同的是:
handler
不在负责事件的处理,而是负责数据的接收与发送。
handler
在通过read系统调用
拿到数据后,会将其转交给子线程中的processor
进行处理,然后processor
处理完成后,再将处理好的数据返回给handler
,handler
再通过send系统调用
将结果发回客户端。
1.2.2.2 评估
- 该方案能够充分利用多核CPU的性能。
- 由于该方案采用多进程/线程来进行事件的处理,因此需要注意在多线程环境下的共享数据的安全问题,而这实现起来,因此该方案较前者复杂一些。
- 需要注意在高并发环境下,1个reactor可能会成为性能瓶颈的隐患。
1.2.3 n * Reactor + n * Worker
1.2.3.1 概述
与前面不同的是:
handler
又再次负责事件的处理了。
对于新的连接事件,将首先分发给acceptor,然后acceptor创建出一个连接来,并将其分发给众多子线程中的其中一个线程。
被选中的这个线程中的reactor将通过select系统调用对该连接进行持续监听,一旦监听到有IO事件发生,便分发给该线程所对应的handler去处理…(后续处理过程一样的)
1.2.3.2 评估
- 消除了单
reactor
所带来的潜在的性能瓶颈隐患。 - 该方案看上去比前者复杂,其实其结构是清晰的,实施起来是简单的:
- 主线程、子线程分工明确,主线程负责接收新连接,子线程负责事件处理。
- 不必为主线程与子线程之间的通信而感到苦恼,因为数据流是由主线的单向流动到子线程中,即在子线程拿到新连接,监听并处理好一个IO事件后,不必将数据再返回给主线程,而是直接由子线程自己返回给客户端。
Netty、Memcache、Nginx均采用了此方案。
Nginx的方案与上面的并不完全相同,它选择去掉主线程部分,即连接的接收可以由每个子线程来完成。
2.Proactor模式
2.1 概述
执行流程:
- 进程通过
Proactor Initiator
借助Asynchronous Operatio Processor
注册proactor
和handler
到内核上。 - 后面将由
Asynchronous Operatio Processor
负责请求的接收与IO操作,一旦接收到IO事件,它将自动进行IO操作,当IO完成后,它将通知Proactor
,然后Proactor
再根据具体的事件类型回调handler
进行处理。
2.2 评估
- 实现了异步I/O,即数据的准备和由内核拷贝至用户缓冲区的过程均无需用户进程(或者CPU)参与。
目前仅有
Windows
系统平台完全实现系统级别下的了异步I/O——IOCP
。
Linux
平台尽管也有POSIX
定义的异步IO接口aio
函数,但它是用户层面的实现,而且仅支持本地文件的异步IO操作,不支持网络I/O。但是,在Linux 5.1之后,又引入io_uring
异步I/O操作接口,它绝对是一个实打实的系统基本的实现,并且也可以用于网络I/O,并且它无需中断即可实现I/O操作,非常惊艳。
3.两种模式的对比
reactor
模式下,reactor
所感知到的事件是待完成的I/O事件
,后续handler
处理需要先把数据由内核缓存拷贝至用户缓冲区中才能继续处理事件,这种I/O模式属于非阻塞式I/O
、同步I/O
。proactor
模式下,proactor
所感知到的事件是已完成的I/O事件
,即不需要handler再去拷贝数据了,而是直接去处理事件,这种I/O模式属于非阻塞式I/O
、异步I/O
,效率更高。
参考文档
9.3 高性能网络模式:Reactor 和 Proactor