gitbook/从0开始学架构/docs/8805.md
2022-09-03 22:05:03 +08:00

168 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 19 | 单服务器高性能模式Reactor与Proactor
[专栏上一期](http://time.geekbang.org/column/article/8697)我介绍了单服务器高性能的PPC和TPC模式它们的优点是实现简单缺点是都无法支撑高并发的场景尤其是互联网发展到现在各种海量用户业务的出现PPC和TPC完全无能为力。今天我将介绍可以应对高并发场景的单服务器高性能架构模式Reactor和Proactor。
## Reactor
PPC模式最主要的问题就是每个连接都要创建进程为了描述简洁这里只以PPC和进程为例实际上换成TPC和线程原理是一样的连接结束后进程就销毁了这样做其实是很大的浪费。为了解决这个问题一个自然而然的想法就是资源复用即不再单独为每个连接创建进程而是创建一个进程池将连接分配给进程一个进程可以处理多个连接的业务。
引入资源池的处理方式后会引出一个新的问题进程如何才能高效地处理多个连接的业务当一个连接一个进程时进程可以采用“read -> 业务处理 -> write”的处理流程如果当前连接没有数据可以读则进程就阻塞在read操作上。这种阻塞的方式在一个连接一个进程的场景下没有问题但如果一个进程处理多个连接进程阻塞在某个连接的read操作上此时即使其他连接有数据可读进程也无法去处理很显然这样是无法做到高性能的。
解决这个问题的最简单的方式是将read操作改为非阻塞然后进程不断地轮询多个连接。这种方式能够解决阻塞的问题但解决的方式并不优雅。首先轮询是要消耗CPU的其次如果一个进程处理几千上万的连接则轮询的效率是很低的。
为了能够更好地解决上述问题很容易可以想到只有当连接上有数据的时候进程才去处理这就是I/O多路复用技术的来源。
I/O多路复用技术归纳起来有两个关键实现点
* 当多条连接共用一个阻塞对象后进程只需要在一个阻塞对象上等待而无须再轮询所有连接常见的实现方式有select、epoll、kqueue等。
* 当某条连接有新的数据可以处理时,操作系统会通知进程,进程从阻塞状态返回,开始进行业务处理。
I/O多路复用结合线程池完美地解决了PPC和TPC的问题而且“大神们”给它取了一个很牛的名字Reactor中文是“反应堆”。联想到“核反应堆”听起来就很吓人实际上这里的“反应”不是聚变、裂变反应的意思而是“**事件反应**”的意思,可以通俗地理解为“**来了一个事件我就有相应的反应**”这里的“我”就是Reactor具体的反应就是我们写的代码Reactor会根据事件类型来调用相应的代码进行处理。Reactor模式也叫Dispatcher模式在很多开源的系统里面会看到这个名称的类其实就是实现Reactor模式的更加贴近模式本身的含义即I/O多路复用统一监听事件收到事件后分配Dispatch给某个进程。
Reactor模式的核心组成部分包括Reactor和处理资源池进程池或线程池其中Reactor负责监听和分配事件处理资源池负责处理事件。初看Reactor的实现是比较简单的但实际上结合不同的业务场景Reactor模式的具体实现方案灵活多变主要体现在
* Reactor的数量可以变化可以是一个Reactor也可以是多个Reactor。
* 资源池的数量可以变化:以进程为例,可以是单个进程,也可以是多个进程(线程类似)。
将上面两个因素排列组合一下理论上可以有4种选择但由于“多Reactor单进程”实现方案相比“单Reactor单进程”方案既复杂又没有性能优势因此“多Reactor单进程”方案仅仅是一个理论上的方案实际没有应用。
最终Reactor模式有这三种典型的实现方案
* 单Reactor单进程/线程。
* 单Reactor多线程。
* 多Reactor多进程/线程。
以上方案具体选择进程还是线程更多地是和编程语言及平台相关。例如Java语言一般使用线程例如NettyC语言使用进程和线程都可以。例如Nginx使用进程Memcache使用线程。
1.单Reactor单进程/线程
单Reactor单进程/线程的方案示意图如下(以进程为例):
![](https://static001.geekbang.org/resource/image/c2/c0/c2fafab3yybd83e97027b3e3f987f9c0.jpg)
注意select、accept、read、send是标准的网络编程APIdispatch和“业务处理”是需要完成的操作其他方案示意图类似。
详细说明一下这个方案:
* Reactor对象通过select监控连接事件收到事件后通过dispatch进行分发。
* 如果是连接建立的事件则由Acceptor处理Acceptor通过accept接受连接并创建一个Handler来处理连接后续的各种事件。
* 如果不是连接建立事件则Reactor会调用连接对应的Handler第2步中创建的Handler来进行响应。
* Handler会完成read->业务处理->send的完整业务流程。
单Reactor单进程的模式优点就是很简单没有进程间通信没有进程竞争全部都在同一个进程内完成。但其缺点也是非常明显具体表现有
* 只有一个进程无法发挥多核CPU的性能只能采取部署多个系统来利用多核CPU但这样会带来运维复杂度本来只要维护一个系统用这种方式需要在一台机器上维护多套系统。
* Handler在处理某个连接上的业务时整个进程无法处理其他连接的事件很容易导致性能瓶颈。
因此单Reactor单进程的方案在实践中应用场景不多**只适用于业务处理非常快速的场景**目前比较著名的开源软件中使用单Reactor单进程的是Redis。
需要注意的是C语言编写系统的一般使用单Reactor单进程因为没有必要在进程中再创建线程而Java语言编写的一般使用单Reactor单线程因为Java虚拟机是一个进程虚拟机中有很多线程业务线程只是其中的一个线程而已。
2.单Reactor多线程
为了克服单Reactor单进程/线程方案的缺点,引入多进程/多线程是显而易见的这就产生了第2个方案单Reactor多线程。
单Reactor多线程方案示意图是
![](https://static001.geekbang.org/resource/image/73/da/73a2d97c63c143a01b2e671942024fda.jpg)
我来介绍一下这个方案:
* 主线程中Reactor对象通过select监控连接事件收到事件后通过dispatch进行分发。
* 如果是连接建立的事件则由Acceptor处理Acceptor通过accept接受连接并创建一个Handler来处理连接后续的各种事件。
* 如果不是连接建立事件则Reactor会调用连接对应的Handler第2步中创建的Handler来进行响应。
* Handler只负责响应事件不进行业务处理Handler通过read读取到数据后会发给Processor进行业务处理。
* Processor会在独立的子线程中完成真正的业务处理然后将响应结果发给主进程的Handler处理Handler收到响应后通过send将响应结果返回给client。
单Reator多线程方案能够充分利用多核多CPU的处理能力但同时也存在下面的问题
* 多线程数据共享和访问比较复杂。例如子线程完成业务处理后要把结果传递给主线程的Reactor进行发送这里涉及共享数据的互斥和保护机制。以Java的NIO为例Selector是线程安全的但是通过Selector.selectKeys()返回的键的集合是非线程安全的对selected keys的处理必须单线程处理或者采取同步措施进行保护。
* Reactor承担所有事件的监听和响应只在主线程中运行瞬间高并发时会成为性能瓶颈。
你可能会发现我只列出了“单Reactor多线程”方案没有列出“单Reactor多进程”方案这是什么原因呢主要原因在于如果采用多进程子进程完成业务处理后将结果返回给父进程并通知父进程发送给哪个client这是很麻烦的事情。因为父进程只是通过Reactor监听各个连接上的事件然后进行分配子进程与父进程通信时并不是一个连接。如果要将父进程和子进程之间的通信模拟为一个连接并加入Reactor进行监听则是比较复杂的。而采用多线程时因为多线程是共享数据的因此线程间通信是非常方便的。虽然要额外考虑线程间共享数据时的同步问题但这个复杂度比进程间通信的复杂度要低很多。
3.多Reactor多进程/线程
为了解决单Reactor多线程的问题最直观的方法就是将单Reactor改为多Reactor这就产生了第3个方案多Reactor多进程/线程。
多Reactor多进程/线程方案示意图是(以进程为例):
![](https://static001.geekbang.org/resource/image/6c/ba/6cfe3c8785623f93da18ce3390e524ba.jpg)
方案详细说明如下:
* 父进程中mainReactor对象通过select监控连接建立事件收到事件后通过Acceptor接收将新的连接分配给某个子进程。
* 子进程的subReactor将mainReactor分配的连接加入连接队列进行监听并创建一个Handler用于处理连接的各种事件。
* 当有新的事件发生时subReactor会调用连接对应的Handler即第2步中创建的Handler来进行响应。
* Handler完成read→业务处理→send的完整业务流程。
多Reactor多进程/线程的方案看起来比单Reactor多线程要复杂但实际实现时反而更加简单主要原因是
* 父进程和子进程的职责非常明确,父进程只负责接收新连接,子进程负责完成后续的业务处理。
* 父进程和子进程的交互很简单,父进程只需要把新连接传给子进程,子进程无须返回数据。
* 子进程之间是互相独立的无须同步共享之类的处理这里仅限于网络模型相关的select、read、send等无须同步共享“业务处理”还是有可能需要同步共享的
目前著名的开源系统Nginx采用的是多Reactor多进程采用多Reactor多线程的实现有Memcache和Netty。
我多说一句Nginx采用的是多Reactor多进程的模式但方案与标准的多Reactor多进程有差异。具体差异表现为主进程中仅仅创建了监听端口并没有创建mainReactor来“accept”连接而是由子进程的Reactor来“accept”连接通过锁来控制一次只有一个子进程进行“accept”子进程“accept”新连接后就放到自己的Reactor进行处理不会再分配给其他子进程更多细节请查阅相关资料或阅读Nginx源码。
## Proactor
Reactor是非阻塞同步网络模型因为真正的read和send操作都需要用户进程同步操作。这里的“同步”指用户进程在执行read和send这类I/O操作的时候是同步的如果把I/O操作改为异步就能够进一步提升性能这就是异步网络模型Proactor。
Proactor中文翻译为“前摄器”比较难理解与其类似的单词是proactive含义为“主动的”因此我们照猫画虎翻译为“主动器”反而更好理解。Reactor可以理解为“来了事件我通知你你来处理”而Proactor可以理解为“**来了事件我来处理,处理完了我通知你**”。这里的“我”就是操作系统内核“事件”就是有新连接、有数据可读、有数据可写的这些I/O事件“你”就是我们的程序代码。
Proactor模型示意图是
![](https://static001.geekbang.org/resource/image/f4/fe/f431b2674eb0881df6a1d1f77a3729fe.jpg)
详细介绍一下Proactor方案
* Proactor Initiator负责创建Proactor和Handler并将Proactor和Handler都通过Asynchronous Operation Processor注册到内核。
* Asynchronous Operation Processor负责处理注册请求并完成I/O操作。
* Asynchronous Operation Processor完成I/O操作后通知Proactor。
* Proactor根据不同的事件类型回调不同的Handler进行业务处理。
* Handler完成业务处理Handler也可以注册新的Handler到内核进程。
理论上Proactor比Reactor效率要高一些异步I/O能够充分利用DMA特性让I/O操作与计算重叠但要实现真正的异步I/O操作系统需要做大量的工作。目前Windows下通过IOCP实现了真正的异步I/O而在Linux系统下的AIO并不完善因此在Linux下实现高并发网络编程时都是以Reactor模式为主。所以即使Boost.Asio号称实现了Proactor模型其实它在Windows下采用IOCP而在Linux下是用Reactor模式采用epoll模拟出来的异步模型。
## 小结
今天我为你讲了单服务器支持高并发的高性能架构模式Reactor和Proactor希望对你有所帮助。
这就是今天的全部内容,留一道思考题给你吧,针对“前浪微博”消息队列架构的案例,你觉得采用何种并发模式是比较合适的,为什么?
欢迎你把答案写到留言区,和我一起讨论。相信经过深度思考的回答,也会让你对知识的理解更加深刻。(编辑乱入:精彩的留言有机会获得丰厚福利哦!)