概要
Reactor 反应器模式是高性能网络编程在设计和架构层面的基础模式。很多服务器软件或者中间件都是基于反应器模式实现的,如 Ngnix Web 服务器、Redis、Netty等,只有掌握了它,才能真正理解掌握 Nginx、Redis、Netty 这些中间件技术。总之,反应器模式是高性能网络编程的必知必会的模式。
什么是 Reactor 反应器模式
Doug Lea 在 文章 Scalable IO in Java 中对反应器模式的定义:反应器模式由 Reactor 反应器线程、Handlers 处理器两大角色组成:
- Reactor 反应器线程的职责 - 负责响应 IO 事件,并且分发到 Handlers 处理器;
- Handlers 处理器的职责:非阻塞的执行业务处理逻辑。
Reactor 反应器模式的演进
Connection Per Thread 模式
Connection Per Thread,顾名思义,即对于每一个新的网络连接都分配一个线程,每个线程独立处理自己负责的输入和输出。当然,服务器的监听线程也是独立的,任何 socket 连接的输入和输出处理,不会阻塞后面新来连接的监听和建立。
Connection Per Thread 模式的优点是解决了前面的新连接被严重阻塞的问题,在一定程度上,极大地提高了服务器的吞吐量。
Connection Per Thread 模式的缺点是:对应于大量的连接,需要耗费大量的线程资源,在操作系统中,线程的创建、销毁、切换都需要不菲的代价。因此,在高并发的应用场景下,这种模式的缺陷是致命的。
单线程 Recator 反应器模式
为了解决 Connection Per Thread 模式的缺陷,我们使用 Reactor 反应器模式对线程的数量进行控制,做到一个线程处理大量的连接。
前面已经提到,在反应器模式中,有 Reactor 反应器和 Handler 处理器两个重要的组件:
- Reactor 反应器:负责查询 IO 事件,当检测到一个 IO 事件,将其发送给相应的 Handler 处理器去处理。这里的 IO 事件,就是 NIO 中选择器监控的通道 IO 事件。
- Handler 处理器:与 IO 事件(或者选择键)绑定,负责 IO 事件的处理。完成真正的连接建立、通道数据的读取、处理业务逻辑、负责将结果写出到通道等。
单线程 Reactor 反应器模式 指的是 Reactor 反应器和 Handlers 处理器处于一个线程中执行,它是最简单的反应器模型。基于 Java NIO 实现单线程 Reactor 反应器模式的核心 API 是类 SelectionKey 的以下两个方法:
1 | void attach(Object o); |
其中,Handler 处理器实例将作为附件添加到 SelectionKey 实例中,当 IO 事件发生时,选择键被 select() 方法选到后,可以直接将事件处理器实例从附件取出,然后调用方法完成相应的处理。
单线程 Reactor反应器模式的缺点:
单线程 Reactor 反应器模式,是基于 Java 的 NIO 实现的,相对于传统的多线程 OIO,反应器模式不再需要启动很多线程,取而代之,Reactor 反应器和 Handler 处理器执行在同一条线程上,这样,带来了一个问题,当其中某个 Handler 阻塞时,会导致其它所有的 Handler 都得不到执行。在这种场景下,如果被阻塞的 Handler 不仅仅负责输入和输出处理的业务,还包括负责连接监听的 AcceptHandler 处理器,这个是非常严重的问题。一旦 AcceptHandler 处理器阻塞,会导致整个服务不能接收新的连接,使得服务器变得不可用。正因如此,在高性能服务器应用场景中,单线程反应器模式实际使用的很少。
多线程 Recator 反应器模式
针对单线程 Reactor 反应器的缺点,使用多线程,对基础反应器模式进行改造升级,即将负责输入输出的 Handler 处理器执行,放入独立的线程池中。这样,业务处理线程与负责监听和 IO 事件查询的反应器线程相互隔离,避免服务器的连接监听受到阻塞。
理论上,这种模式依然有一个地方是单点的,那就是处理客户端连接的线程。因为大多数服务端应用或多或少在连接时都会处理一些业务,如鉴权之类的,当连接的客户端越来越多时这一个线程依然会存在性能问题。
主从多线程的 Reactor 反应器模式
这种模式是在多线程 Reactor 反应器模式的基础上,将 Reactor 反应器拆分为多个子反应器线程,每一个 SubReactor 子线程负责一个选择器。这样可以充分使用系统处理能力,提高反应器管理连接的数量,提升选择大量通道的能力。
小结
反应器模式和生产者消费者模式对比
相似之处:在一定程度上,反应器模式有点类似生产者消费者模式。在生产者消费者模式中,一个或多个生产者将事件加入到一个队里中,一个或多个消费者主动地从这个队列中提取事件来处理。
不同之处:反应器模式是基于查询的,没有专门的队列去缓冲存储 IO 事件,查询到 IO 事件之后,反应器会根据不同 IO 选择键(事件)将其分发给对应的 Handler 处理。
反应器模式优缺点
反应器模式优点:
- 响应快,虽然同一反应器线程本身是同步的,但不会被单个连接的同步 IO 所阻塞;
- 编程相对简单,最大程度避免了复杂的多线程同步,也避免了多线程的切换的开销;
- 可扩展,可以方便地通过增加反应器线程的个数来充分利用 CPU 资源;
反应器模式缺点:
- 反应器模式增加了一定的复杂性,因而有一定的门槛,并且不易于调试;
- 反应器模式需要操作系统底层的 IO 多路复用的支持,如 Linux 中的 epoll,如果操作系统的底层不支持 IO 多路复用,反应器模式不会有那么高效;
- 同一个 Handler 业务线程中,如果出现一个长时间的数据读写,会影响这个反应器中其它通道的 IO 处理。因而,对于这种类型的处理,还需要进一步对反应器模式进行改进。
参考资料
《Netty、Redis、Zookeeper 高并发实战》
高性能网络框架:Reactor 和 Proactor