到目前为止,高性能网络编程都绕不开 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 | // 注意:代码示例中的异常处理内容都被忽略掉了 |
3. 分而治之
在构建高性能可伸缩I/O服务的过程中,希望达到以下的目标:
- 能够在海量负载连接情况下优雅降级;
- 能够随着硬件资源的增加,性能持续改进;
- 具备低延迟、高吞吐量、可调节的服务质量等特点;
分而治之是实现高性能可伸缩目标的最佳方法。
分而治之将处理过程分为多个小任务,每个任务执行一个非阻塞的操作,任务通常由一个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线程完成,如下图:
通过 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操作。
有专门的 Acceptor线程(NIO线程) 用于监听,接收客户端的TCP连接请求。
I/O读写操作有专门的NIO线程池负责,池中的NIO线程负责读取消息、解码、编码和发送。
大体流程:
reactor 通过 epoll监控连接事件,收到事件后通过回调函数进行转发。
如果是连接建立的事件,则交由 Acceptor线程 建立连接,并创建 handler 处理后续I/O事件。
如果不是建立连接事件而是I/O读写事件,reactor 则直接分发调用 handler 来响应。
handler 会完成 read -> 业务处理 -> send 一整套完整业务流程。
单reactor-多线程模型 看起来很不错,但是还是有缺点:一个 Acceptor线程 承担所有事件的监听和响应,在面对瞬间高并发的场景时,容易成为性能的瓶颈的地方。
5.3. 主从reactor-多线程模型
主从reactor-多线程模型 比起 单reactor-多线程模型 ,将 reactor 分成两部分,分工合作提高CPU利用率和I/O速率:
- main reactor: 负责监听客户端的新连接,并将建立的连接分派给 sub reactor。
- sub reactor: 负责处理已建立连接的I/O读写事件,并交给worker线程池处理。
- 服务端不再只用一个 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
- 9.3 高性能网络模式:Reactor 和 Proactor
- 反应器模式 - 维基百科
- Reactor 典型的 NIO 编程模型
- http://gee.cs.oswego.edu/dl/cpjslides/nio.pdf
- Reactor 模式–Scalable IO in Java
- 透彻理解Java网络编程(七)——Reactor模式