Reactor 模型|为啥 Redis 单线程模型也能效率这么高?

Reactor 模型是网络服务器端用来处理高并发网络 IO 请求的一种编程模型,这个模型的特征可以用两个“三”来总结:

  • 三类处理事件,即连接事件、写事件、读事件
  • 三个关键角色,即reactoracceptorhandler

Reactor 模型处理的是客户端和服务器端的交互过程,而这三类事件正好对应了客户端和服务器端交互过程中,不同类请求在服务器端引发的待处理事件:

  • 连接事件: 当一个客户端要和服务器端进行交互时,客户端会向服务器端发送连接请求,以建立连接。
  • 写事件: 一旦连接建立后,客户端会给服务器端发送读请求,以便读取数据。服务器端在处理读请求时,需要向客户端写回数据。
  • 读事件: 无论客户端给服务器端发送读或写请求,服务器端都需要从客户端读取请求内容,所以在这里。

这三类事件是由谁来处理的呢?这其实就是模型中三个关键角色的作用了:

  1. 首先,连接事件由 acceptor 来处理,负责接收连接;acceptor 在接收连接后,会创建 handler,用于网络连接上对后续读写事件的处理;
  2. 其次,读写事件由 handler 处理;
  3. 最后,在高并发场景中,连接事件、读写事件会同时发生,所以,我们需要有一个角色专门监听和分配事件,这就是 reactor 角色。当有连接请求时,reactor 将产生的连接事件交由 acceptor 处理;当有读写请求时,reactor 将读写事件交由 handler 处理。

Reactor 模型的基本工作机制:

客户端的不同类请求会在服务器端触发连接、读、写三类事件,这三类事件的监听、分发和处理又是由 reactor、acceptor、handler 三类角色来完成的,然后这三类角色会通过事件驱动框架来实现交互和事件处理。

事件驱动框架

所谓的事件驱动框架,就是在实现 Reactor 模型时,需要实现的代码整体控制逻辑。简单来说,事件驱动框架包括了两部分:

  • 事件初始化: 创建需要监听的事件类型,以及该类事件对应的 handler。
  • 事件捕获、分发和处理主循环

事件驱动框架的基本执行过程:

Reactor 编程模式

单 Reactor 单线程

accept -> read -> 处理业务逻辑 -> write 都在一个线程

单 Reactor 多线程

accept/read/write 在一个线程,处理业务逻辑在另一个线程

在极特殊应用场景中,一个NIO线程负责监听和处理所有的客户端连接可能会存在性能问题。例如百万客户端并发连接,或者服务端需要对客户端的握手信息进行安全认证,认证本身非常损耗性能。

多 Reactor 多线程/进程

accept 在一个线程/进程,read/处理业务逻辑/write 在另一个线程/进程

哪些软件使用了 Reactor 模型?

  • Redis: 早期版本属于单 Reactor 单线程模型。监听请求、读取数据、处理请求、写回数据都在一个线程中执行,这样会有 3 个问题:

    1. 单线程无法利用多核
    2. 处理请求发生耗时,会阻塞整个线程,影响整体性能
    3. 并发请求过高,读取/写回数据存在瓶颈

      针对问题 3,Redis 6.0(单 Reactor 多线程模型)进行了优化,引入了 IO 多线程,把读写请求数据的逻辑,用多线程处理,提升并发性能,但处理请求的逻辑依旧是单线程处理。

  • Memcached:多 Reactor 多线程模型
  • Nginx:多 Reactor 多进程模型。主Reacotr不处理网络IO,只用来初始化 socket,由 Wroker子进程 accept 连接,之后这个连接的所有处理都在子进程中完成。每个Wroker进程是一个独立的单Reacotr单线程模型。
  • Netty:多 Reactor 多线程模型。主Reacotr只负责建立连接,然后把建立好的连接给从Reactor,从Reactor负责IO读写。
  • Kafka:多 Reactor 多线程模型。但因为Kafka主要与磁盘IO交互,因此真正的读写数据不是从Reactor 处理的,而是有一个worker线程池,专门处理磁盘IO,从Reactor负责网络IO,然后把任务交给worker线程池处理。

为什么 Redis 单线程却能支撑高并发?

Redis 内部使用 文件事件处理器(file event handler),这个文件事件处理器是 单线程的。它采用 IO 多路复用机制同时监听多个 socket,根据 socket 上的事件来选择对应的事件处理器进行处理。

文件事件处理器的结构包含 4 个部分:

  • 多个 socket
  • IO 多路复用程序
  • 文件事件分派器
  • 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)

多个 socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 socket,会将 socket 产生的事件放入 队列 中排队,事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理。

为啥 Redis 单线程模型也能效率这么高?

  1. 纯内存操作
  2. 核心是基于非阻塞的 IO 多路复用机制
  3. 单线程避免了多线程的频繁上下文切换问题

参考

  1. http://gee.cs.oswego.edu/dl/cpjslides/nio.pdf
  2. https://www.cnblogs.com/z-qinfeng/p/11968148.html
彦祖老师 wechat