Reactor 模型
Reactor 模型的 IO
Reactor 模型,其 IO 属于同步非阻塞 IO。下面仍以 channel 发起读操作请求为例来分析整个执行过程:
- 当 channel 的执行线程发起了 read()调用后,其会向 selector 注册了 OPS_READ 事件然后该线程会不停的查看该事件是否就绪。
- 当 selector 接收到这个注册后,其就会不停的查看该 channel 所关联的网卡缓存中是否具有了数据。当 selector 轮询到该 channel 的网卡缓存中具有了数据后,该读操作就绪。
- 此时该线程就查看到了就绪,会发起 system call,将网卡缓存中的数据读取到 user buffer中。这些操作完成后,会再执行 read()后面的逻辑。整个执行过程,该线程未发生阻塞。所以 Reactor 模型是“同步非阻塞 IO”模型。
Reactor 单线程模型
![](https://i.loli.net/2020/11/18/KbPTq8sUxHI2WQz.png#alt=image-20201118113959836)
Reactor 单线程模型,指的是当前的 Sever 会为每一个通信对端形成一个 Channel,而所有这些 Channel 都会与一个线程相绑定,该线程用于完成它们间的所有通信处理。
线程主要的工作是:
如果是 Server,则该线程需要接收并处理 Client 的连接请求。
如果是 Client,则该线程需要向 Server 发起连接。
读取通信对端的消息。
向通信对端发送消息。
Reactor 线程池模型
![](https://i.loli.net/2020/11/18/4MOo2Kg7P8vtd5X.png#alt=image-20201118114523181)
Reactor 单线程模型中使用一个线程处理所有通信对端的所有请求,在高并发场景中会严重影响系统性能。所以将单线程模型里面的这个单线程替换成了一个线程池,既 为每个Selector会为Channel分配一个线程池专门来处理这个Channel的请求以来提高在高并发场景下的性能。
Reactor 多线程池模型
如果客户端的连接的并发量是数以百万计的,而且 IO 操作还比较耗时,此时的 Server 即使采用的是 Reactor 线程池模型,系统性能也会急剧下降。此时,可以将连接操作与 IO 操作分开处理,形成 Reactor 的多线程模型。
当客户端通过处理连接请求的 Channel 连接上 Server 后,系统会为该客户端再生成一个子 Channel 专门用于处理该客户端的 IO 请求。这两类不同的 Channel 连接着两类不同的线程池。而线程池中的线程数量,可以根据需求分别设置。提高了系统性能。
Netty-Server 的 Reactor 模型
中的每一个 EventLoop 都绑定着一个线程,用于处理该 Channel 与当前 Server 间的操作。Netty-Server 采用了多线程模型。不过线程池是由 EventLoopGroup 充当。EventLoopGroup一个 Channel 只能与一个 EventLoop 绑定,但一个 EventLoop 可以绑定多个 Channel。即 Channel与 EventLoop 间的关系是 n:1。
Netty-Client 的 Reactor 模型
Netty-Client 采用的是线程池模型。因为其只需要与 Server 连接一次即可,无需区分连接请求与 IO 请求。
Proactor 模型
在高性能的网络通信设计中,有两个比较著名的网络通信模型 Reactor 和 Proactor 模式,其中 Reactor 模式属于同步非阻塞 I/O 的网络通信模型,而 Proactor 运属于异步非阻塞 I/O的网络通信模型。
Proactor 模型,其 IO 属于异步非阻塞 IO。下面仍以 channel 发起读操作请求为例来分析整个执行过程:
当 channel 的执行线程调用了异步的 read()操作后,其会继续执行 read()后的逻辑。而该 read()会将本次操作注册到一个 Proactor 实例中,注册本次操作关注的事件为 Read Complete。
一个 channel 的所有 IO 操作共享一个 Proactor 实例这个 Proactor 实例是在 channel 创建时完成的初始化。每个Proactor 实例具有一个绑定的线程,用于执行相关 IO 操作。
当 Proactor 实例接收了 read()操作的注册后,其会为网络缓存注册一个监听。若网卡缓存中有了数据,则马上通过 DMA 控制将数据写入到 user buffer 中。一旦数据写入 user buffer完成,则该 IO 操作完毕,产生 Read Complete 事件,此时会将该事件写入到 Proactor 所维护的一个队列。Proactor 实例会将队列中的事件发送给各个 IO 调用者线程,以使他们触发相应的回调。
当前 read()操作的调用线程无需阻塞等待 read()操作的完成,而是直接执行后面的逻辑。由于 read()操作本身是由另外一个线程来执行,所以 Proactor 模型是“异步非阻塞 IO”模型。
Reactor 中的 selector 是一种“事件分离器”的实现。在 Proactor 中不存在事件分离器,但存在一个 Proactor 实例。该实例会根据不同的 IO 操作,监听不同的内容。例如,本例为网卡缓存注册了监听。
Proactor 优缺点
- Proactor 在处理高耗时 IO 时的性能要高于 Reactor,但对于低耗时 IO 的执行效率提升并不明显。
- Proactor 的异步性使其并发处理能力要强于 Reactor。
- Proactor 的实现逻辑复杂,编码成本较 Reactor 要高很多。
- Proactor 的异步依赖于操作系统对于异步的支持。若操作系统对异步的支持不好,Proactor 的性能还不如 Reactor。
Netty 与 Proactor
Netty4 是 NIO 的,其网络通信模型采用的是 Reactor。
Netty5 是 AIO 的,其网络通信模型采用的是 Proactor。但该版本已经被不再维护。主要原因还是 Linux 目前对于异步的支持不完善,导致其执行效率很低。
Proactor 与 Epoll
Proactor 与 Epoll 是没有可比性的
Epoll:是“事件分离器”对就绪事件的发现方式,有 select、poll 与 epoll 三种方式。epoll采用的是回调方式,而不是轮询方式。Reactor 模型中具有“事件分离器”。
Proactor:是一种网络通信模型,该模型中就不存在“事件分离器”。
Netty 中的 Epoll 多路复用器
epoll 的效率在如下场景中并不一定比 poll 的高。
- 当出现大批量的读/写事件切换时,epoll 的效率会远远低于 poll。因为 epoll 需要进行大量的用户空间到内核空间的切换,而 poll 仅需要在用户空间做简单的位运算即可完成。
- 若Client与Server端有大量的仅用于传递少量数据的短连接,则epoll的效率要低于poll。因为 epoll 下的每个 socket 连接都需要发生两次用户空间与内核空间的转换,而 poll 不需要。
- epoll 完全属于 Linux,虽然其它系统平台也有 epoll 的支持,但并不完全相同。
- 高性能处理的代码编写逻辑 epoll 要比 poll 更复杂,更难调试。特别是边缘触发。如果错过额外的读/写操作,很容易导致死锁。