【Redis】多路复用IO、Reactor模型

面试官:说说Redis之I/O多路复用模型实现原理
Redis 多线程网络模型全面揭秘
五分钟快速理解 Reactor 模型

Redis是个单线程程序。因为它所有的数据都存在内存中,所有的运算都是内存级别的运算。那么既然是单线程,如何处理并发的客户端连接?

一、UnixIO的五种类型

一个输入操作通常包括两个阶段:

  1. 等待数据准备好:等待数据从网络中到达。当所等待分组到达时,它被复制到内核中的某个缓冲区。
  2. 从内核向进程复制数据:把数据从内核缓冲区复制到应用进程缓冲区。

一般情况下有5种类型:

1. 阻塞式 I/O

应用进程被阻塞,直到数据被复制到应用进程缓冲区(其他程序还可以执行,因此不消耗 CPU 时间,这种模型的执行效率会比较高)。

2. 非阻塞式 I/O

应用进程进行系统调用后,内核返回一个错误码。应用进程可以继续执行,但是需要不断的执行系统调用来获知 I/O 是否完成,这种方式称为轮询(polling)。由于 CPU 要处理更多的系统调用,因此这种模型是比较低效的。

3. I/O 复用(select 和 poll)

使用 select 或者 poll 等待数据,并且可以等待多个套接字中的任何一个变为可读,这一过程会被阻塞,当某一个套接字可读时返回。之后再使用 recvfrom 把数据从内核复制到进程中。它可以让单个进程具有处理多个 I/O 事件的能力。又被称为 Event Driven I/O,即事件驱动 I/O

如果一个 Web 服务器没有 I/O 复用,那么每一个 Socket 连接都需要创建一个线程去处理。如果同时有几万个连接,那么就需要创建相同数量的线程。并且相比于多进程和多线程技术,I/O 复用不需要进程线程创建和切换的开销,系统开销更小。

4. 信号驱动式 I/O(SIGIO)

应用进程使用 sigaction 系统调用,内核立即返回,应用进程可以继续执行,也就是说等待数据阶段应用进程是非阻塞的。

内核在数据到达时向应用进程发送 SIGIO 信号,应用进程收到之后在信号处理程序中调用 recvfrom 将数据从内核复制到应用进程中。相比于非阻塞式 I/O 的轮询方式,信号驱动 I/O 的 CPU 利用率更高。

5. 异步 I/O(AIO)

进行 aio_read 系统调用会立即返回,应用进程继续执行,不会被阻塞,内核会在所有操作完成之后向应用进程发送信号。异步 I/O 与信号驱动 I/O 的区别在于,异步 I/O 的信号是通知应用进程 I/O 完成,而信号驱动 I/O 的信号是通知应用进程可以开始 I/O。

在这里插入图片描述

IO类型同步/异步等待数据复制数据
阻塞式IO同步阻塞阻塞
非阻塞式IO同步非阻塞、立即返回、轮询阻塞
IO复用同步事件驱动,同时等待多个socket中的数据、其中一个返回了结果即返回阻塞
信号驱动式IO同步非阻塞阻塞
异步IO异步非阻塞非阻塞

二、多路复用IO

Redis 为何选择单线程?

Redis 的核心网络模型选择用单线程来实现,这在一开始就引起了很多人的不解,Redis 官方的对于此的回答是:

It’s not very frequent that CPU becomes your bottleneck with Redis, as usually Redis is either memory or network bound. For instance, using pipelining Redis running on an average Linux system can deliver even 1 million requests per second, so if your application mainly uses O(N) or O(log(N)) commands, it is hardly going to use too much CPU.

核心意思就是,对于一个 DB 来说,CPU 通常不会是瓶颈,因为大多数请求不会是 CPU 密集型的,而是 I/O 密集型。具体到 Redis 的话,如果不考虑 RDB/AOF 等持久化方案,Redis 是完全的纯内存操作,执行速度是非常快的,因此这部分操作通常不会是性能瓶颈,Redis 真正的性能瓶颈在于网络 I/O,也就是客户端和服务端之间的网络传输延迟,因此 Redis 选择了单线程的 I/O 多路复用来实现它的核心网络模型。

为什么Redis中要使用 I/O 多路复用?

因为 Redis 是跑在单线程中的,所有的操作都是按照顺序线性执行。但是由于读写操作等待用户输入或输出都是阻塞的,所以 I/O 操作在一般情况下往往不能直接返回,这会导致某一文件的 I/O 阻塞导,致整个进程无法对其它客户提供服务。

而 I/O 多路复用就是为了解决这个问题而出现的。为了让单线程(进程)的服务端应用同时处理多个客户端的事件,Redis 采用了 IO 多路复用机制。

Redis中的多路复用模型

  • 文件描述符(file descriptor)
    Linux 系统中,把一切都看做是文件,当进程打开现有文件或创建新文件时,内核向进程返回一个文件描述符。可以理解文件描述符是一个索引,这样,要操作文件的时候,我们直接找到索引就可以对其进行操作了。我们将这个索引叫做文件描述符(file descriptor),简称fd。
    在这里插入图片描述
  1. 一个socket客户端与服务端连接时,会生成对应一个套接字描述符(套接字描述符是文件描述符的一种),每一个socket网络连接其实都对应一个文件描述符。
  2. 多个客户端与服务端连接时,Redis 使用 I/O 多路复用程序将客户端socket对应的FD注册监听列表(一个队列) 中。当客服端执行 read、write 等操作命令时,I/O 多路复用程序会将命令封装成一个事件,并绑定到对应的 fd 上。
  3. 文件事件处理器 使用 I/O 多路复用模块同时监控多个文件描述符(fd)的读写情况,当 accept、read、write 和 close 文件事件产生时,文件事件处理器就会回调 fd 绑定的事件处理器进行处理相关命令操作。
  4. 整个文件事件处理器是在单线程上运行的,但是通过 I/O 多路复用模块的引入,实现了同时对多个fd读写的监控,当其中一个 client 端达到写或读的状态,文件事件处理器就马上执行,从而就不会出现 I/O 堵塞的问题,提高了网络通信的性能。
  5. 如上图,Redis 的 I/O 多路复用模式使用的是Reactor模式的方式来实现。

三、Reactor模型

阻塞IO模型的特点:

  • 采用阻塞式 I/O 模型获取输入数据
  • 每个连接都需要独立的线程完成数据输入,业务处理,数据返回的完整操作。

其存在的问题:

  • 当并发数较大时,需要创建大量线程来处理连接,系统资源占用较大;
  • 连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在 Read 操作上,造成线程资源浪费。

针对传统阻塞 I/O 服务模型的 2 个缺点,比较常见的有如下解决方案:

  1. I/O 复用模型
    多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象上等待,无需阻塞等待所有连接。当某条连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理;

  2. 线程池复用线程资源
    不必再为每个连接创建线程,将连接完成后的业务处理任务分配给线程进行处理,一个线程可以处理多个连接的业务。

I/O 复用结合线程池,这就是 Reactor 模式基本设计思想

在这里插入图片描述
Reactor 模式中有 2 个关键组成:

  • Reactor:Reactor 在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序来对 IO 事件做出反应。
  • Handlers:处理程序执行 I/O 事件要完成的实际事件。

根据 Reactor 的数量和处理资源池线程的数量不同,有 3 种典型的实现:

  1. 单 Reactor 单线程;
  2. 单 Reactor 多线程;
  3. 主从 Reactor 多线程。

1. 单Reactor单线程

从 Redis 的 v1.0 到 v6.0 版本之前,Redis 的核心网络模型一直是一个典型的单 Reactor 模型:利用 epoll/select/kqueue 等多路复用技术,在单线程的事件循环中不断去处理事件(客户端请求),最后回写响应数据到客户端。

在这里插入图片描述

  1. Reactor 对象通过 Select 监控客户端请求事件,收到事件后通过 Dispatch 进行分发;
  2. 如果是建立连接请求事件,则由 Acceptor 通过 Accept 处理连接请求,然后创建一个 Handler 对象处理连接完成后的后续业务处理;
  3. 如果不是建立连接事件,则 Reactor 会分发调用连接对应的 Handler 来响应;
  4. Handler 会完成 Read→业务处理→Send 的完整业务流程。

缺点:只有一个线程,无法完全发挥多核 CPU 的性能。Handler 在处理某个连接上的业务时,整个进程无法处理其他连接事件,很容易导致性能瓶颈。但是Redis的操作一般为O(1),所以任务执行速度非常快。

2. 单Reactor多线程

Redis 在 v4.0 版本的时候就已经引入了的多线程来做一些异步操作,此举主要针对的是那些非常耗时的命令,通过将这些命令的执行进行异步化避免阻塞单线程的事件循环

在这里插入图片描述
我们知道 Redis 的 DEL 命令是用来删除掉一个或多个 key 储存的值,它是一个阻塞的命令,大多数情况下你要删除的 key 里存的值不会特别多,最多也就几十上百个对象,所以可以很快执行完。

但是如果你要删的是一个超大的键值对,里面有几百万个对象,那么这条命令可能会阻塞至少好几秒,又因为事件循环是单线程的,所以会阻塞后面的其他事件,导致吞吐量下降。

于是,在 Redis v4.0 之后增加了一些的非阻塞命令如 UNLINK、FLUSHALL ASYNC、FLUSHDB ASYNC

UNLINK 命令其实就是 DEL 的异步版本,它不会同步删除数据,而只是把 key 从 keyspace 中暂时移除掉,然后将任务添加到一个异步队列,最后由后台线程去删除。

不过这里需要考虑一种情况是如果用 UNLINK 去删除一个很小的 key,用异步的方式去做反而开销更大,所以它会先计算一个开销的阀值,只有当这个值大于 64 才会使用异步的方式去删除 key,对于基本的数据类型如 List、Set、Hash 这些,阀值就是其中存储的对象数量。

3. 多Reactor多线程

Redis 的核心网络模型在 6.0 版本之前,一直是单 Reactor 模式:所有事件的处理都在单个线程内完成,虽然在 4.0 版本中引入了多线程,但是那个更像是针对特定场景(删除超大 key 值等)而打的补丁,并不能被视作核心网络模型的多线程。

通常来说,单 Reactor 模式,引入多线程之后会进化为 Multi-Reactors 模式,基本工作模式如下

在这里插入图片描述
区别于单 Reactor 模式,这种模式不再是单线程的事件循环,而是由 Main Reactor 负责接收新连接并分发给 Sub Reactors 去独立处理,子线程(Sub Reactors)各自维护一个独立的事件循环

  1. Reactor 主线程 MainReactor 对象通过 Select 监控建立连接事件,收到事件后通过 Acceptor 接收,处理建立连接事件;
  2. Acceptor 处理建立连接事件后,MainReactor 将连接分配 Reactor 子线程给 SubReactor 进行处理;
  3. SubReactor 将连接加入连接队列进行监听,并创建一个 Handler 用于处理各种连接事件;
  4. 当有新的事件发生时,SubReactor 会调用连接对应的 Handler 进行响应;

在 Reactor 模式中,Reactor 等待某个事件或者可应用或者操作的状态发生(比如文件描述符可读写,或者是 Socket 可读写)。然后把这个事件传给事先注册的 Handler(事件处理函数或者回调函数),由后者来做实际的读写操作。

其中的读写操作都需要应用程序同步操作,所以 Reactor 是非阻塞同步网络模型。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值