网络编程 Reactor 模式
2025-01-22 08:19:30    2.1k 字   
This post is also available in English and alternative languages.

到目前为止,高性能网络编程都绕不开 Reactor(反应器)模式,很多著名的服务器软件或者中间件都是基于反应器模式实现的,如 Nginx、Redis、Netty 等。

Reactor模式之所以高效是因为采用了分而治之事件驱动的设计。


1. Reactor编程模式

DougLea 在《Scalable IO in Java》中描述了Reactor模式的思想,大部分NIO框架和一些中间件的NIO编程都与它一样或是它的变体。

DougLea 在《Scalable IO in Java》中对 Reactor模式 做了定义:

  • Reactor模式 由 Reactor线程 和 Handlers处理器 两大角色组成。
  • Reactor线程 负责响应IO时间,并分发到 Handlers处理器。
  • Handlers处理器 负责非阻塞的执行业务逻辑。

2. 传统服务设计模式

在传统服务设计模式中会为每个连接的处理开启一个新的线程,如下图:

传统的服务设计模式

传统服务设计模式缺点很明显,需要开启大量线程。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 注意:代码示例中的异常处理内容都被忽略掉了
class Server implements Runnable {
@Override
public void run() {
try {
ServerSocket ss = new ServerSocket(PORT);
while (!Thead.interrupted()) {
new Thread(new Handler(ss.accept())).start();
// or, single-threaded, or a thread pool
}
} catch (IOException ex) {/* ... */}
}

static class Handler implements Runnable {
final Socket socket;
Handler(Socket s) {
socket = s;
}
@Override
public void run() {
try {
byte[] input = new byte[MAX_INPUT];
socket.getInputStream().read(input);
byte[] output = process(input);
socket.getOutputStream().write(output);
} catch (IOException ex) {/* ... */}
}
private byte[] process(byte[] cmd) {/* ... */}
}
}

3. 分而治之

在构建高性能可伸缩I/O服务的过程中,希望达到以下的目标:

  1. 能够在海量负载连接情况下优雅降级;
  2. 能够随着硬件资源的增加,性能持续改进;
  3. 具备低延迟、高吞吐量、可调节的服务质量等特点;

分而治之是实现高性能可伸缩目标的最佳方法。

分而治之将处理过程分为多个小任务,每个任务执行一个非阻塞的操作,任务通常由一个I/O事件触发执行;
这种机制在java.nio中提供了支持:

  • 非阻塞的读写
  • 通过感知I/O事件分发任务的执行。

分而治之的关键在于非阻塞,这样就能充分利用线程、压榨CPU,提高系统的吞吐能力。


4. 事件驱动设计

基于事件驱动的架构设计通常比其他架构模型更加高效,因为可以节省一定的性能资源;

事件驱动模式下通常不需要为每一个客户端建立一个线程,这意味这更少的线程开销,更少的上下文切换和更少的锁互斥;

但任务的调度可能会慢一些,而且通常实现的复杂度也会增加,相关功能必须分解成简单的非阻塞操作,当然也不可能把所有阻塞都消除掉,特别是发生GC、page faults(内存缺页中断)等情况;

由于是基于事件驱动的,所以需要跟踪服务的相关状态(因为你需要知道什么时候事件会发生);


NIO中总共设计了4种事件,每个事件发生都会调度关联的任务,分别是:

  • OP_ACCEPT:服务端监听到了一个连接,准备接收
  • OP_CONNECT:客户端与服务器连接建立成功
  • OP_READ:读事件就绪,通道有数据可读
  • OP_WRITE:写事件就绪,可以向通道写入数据

NIO对事件驱动也提供了支持:

  • Channels:连接到文件、Socket等,支持非阻塞读取
  • Buffers:类似数组的对象,可由通道直接读取或写入
  • Selectors:通知哪组通道有I/O事件
  • SelectionKeys:维护I/O事件的状态和绑定信息

5. 三种Reactor模型

Reactor是一种事件处理模式,wiki中对其定义如下:Reactor是一个或多个输入事件的处理模式,用于处理并发传递给服务处理程序的服务请求。服务处理程序判断传入请求发生的事件,并将它们同步的分派给关联的请求处理程序。

Reactor模式一共有三种,单reactor-单线程模型、单reactor-多线程模型、主从reactor-多线程模型。


5.1. 单reactor-单线程模型

单reactor-单线程模型 中,所有的I/O操作都由同一个NIO线程完成,如下图:

单Reactor单线程模型

通过 acceptor 接收客户端的TCP连接请求,当连接建立成功之后,通过 dispatch 将对应的ByteBuffer派发到指定的 handler 上,进行业务处理,handler 处理完成后再通过NIO线程将消息发送给客户端。

由于Reactor模式使用的是异步非阻塞I/O,所有的I/O操作都不会导致阻塞,理论上一个线程可以独立处理所有I/O相关的操作。从架构层面看,一个NIO线程确实可以完成其承担的职责。

单reactor-单线程模型 ,适用于 handler 链路中业务处理组件能快速完成的场景,并且这种模型不能充分利用多核资源。


5.2. 单reactor-多线程模型

单reactor-单线程模型单reactor-多线程模型 最大的区别就是有一组NIO线程专门处理I/O操作。

单Reactor多线程模型
  • 有专门的 Acceptor线程(NIO线程) 用于监听,接收客户端的TCP连接请求。

  • I/O读写操作有专门的NIO线程池负责,池中的NIO线程负责读取消息、解码、编码和发送。


大体流程:

  1. reactor 通过 epoll监控连接事件,收到事件后通过回调函数进行转发。

  2. 如果是连接建立的事件,则交由 Acceptor线程 建立连接,并创建 handler 处理后续I/O事件。

  3. 如果不是建立连接事件而是I/O读写事件,reactor 则直接分发调用 handler 来响应。

  4. handler 会完成 read -> 业务处理 -> send 一整套完整业务流程。

单reactor-多线程模型 看起来很不错,但是还是有缺点:一个 Acceptor线程 承担所有事件的监听和响应,在面对瞬间高并发的场景时,容易成为性能的瓶颈的地方。


5.3. 主从reactor-多线程模型

主从reactor-多线程模型 比起 单reactor-多线程模型 ,将 reactor 分成两部分,分工合作提高CPU利用率和I/O速率:

  • main reactor: 负责监听客户端的新连接,并将建立的连接分派给 sub reactor
  • sub reactor: 负责处理已建立连接的I/O读写事件,并交给worker线程池处理。
主从Reactor多线程模型
  • 服务端不再只用一个 Acceptor线程 接收客户端连接,而是一个独立的 main reactor线程池。
  • main reactor线程池 中的每个线程都可以接收客户端的TCP连接请求,并将建立完连接后的 SocketChannel 交给 sub reactor线程池 处理;
  • sub reactor线程池 中的线程会将接收到的 SocketChannel 注册到自己的Selector上,并负责监听和处理该SocketChannel的 I/O读写事件;
  • main reactor线程池 只用于客户端的登录、握手和安全认证,一旦链路建立成功,就将链路注册到 sub reactor线程池 的线程上,由它们负责后续的I/O操作。

6. 小结

Reactor编程模式 可以理解为「来了事件,操作系统通知应用进程,让应用进程来处理」,这里的「事件」指的是有新连接、有数据可读、有数据可写的这些 I/O 事件,这里的「处理」包含从驱动读取到内核以及从内核读取到用户空间。


7. Reference


8. 推荐阅读