目录

一、前置知识

1、操作系统

2、内核和用户空间

3、IO

4、文件描述符(FD-File Descriptor )

二、Redis之IO多路复用

1、问答

1.1、Redis采用单线程的原因?

1.2、如果万一CPU成为你的Redis瓶颈了,或者,你就是不想让服务器其他核闲置,那怎么办?

1.3、单线程模型

1.4、IO多路复用用来解决Redis的什么问题?

1.5、单线程模型每秒万级别处理能力的原因

2、IO模型(Linux)

2.1、阻塞IO模型

2.2、IO多路复用

3、Ractor设计模式(Redis的具体实现)

3.1、整体设计结构

3.2、IO多路复用模块

3.3、总结


一、前置知识

1、操作系统

操作系统本身可以看作一个特殊的软件,只有操作系统能直接接触计算机硬件,其他软件要访问硬件都必须通过操作系统

操作系统对硬件做抽象,封装接口给软件调用

常见的操作系统有:Windows,Linux,Mac

redis用多路复用IO的原因 redis 多路io复用_Redis

2、内核和用户空间

内核和用户空间都存在内存中

内核:只要计算机处于开启状态,操作系统就会把它的程序和数据放入内存中,由于操作系统的重要性,它会独占内存中的一块区域,这块区域称之为内核,内核就是操作系统常驻内存的区域。

用户空间:用户空间就是某个进程启动后,被分配到的一块内存区域

为了保证内核的安全,处于用户态的程序只能访问用户空间,而处于内核态的程序可以访问用户空间和内核空间。

详细请看Linux章节(还没整理)

3、IO

操作系统提供了一些IO操作,用于操作IO设备的数据,然后应用软件对其进行封装

Linux的缓存IO机制中,操作系统会将IO的数据缓存在文件系统的页缓存中,也就是说,数据会先被拷贝到操作系统内核的缓冲区,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。

对于一次IO访问(以read为例)所以说,当一个read操作发生时,会经历两个阶段:

  1、等待数据准备(Waiting for the data to be ready)

  2、将数据从内核拷贝到进程中(Copy the data from kernel to the process)

数据流转:

网络IO:网卡→内核→用户空间(用户内存)

磁盘IO:磁盘→内核→用户空间(用户内存)

各个操作系统分别提供了哪些IO操作?有哪几种IO模型?

IO操作:select、poll、epoll、kqueue、evport

IO模型:

以Linux为例,它有五种IO模型:阻塞IO模型、非阻塞IO模型、IO多路复用模型、信号驱动IO模型以及异步IO模型

详细请看Linux章节(还没整理)

4、文件描述符(FD-File Descriptor )

Linux将所有设备都当做文件来处理,文件描述符来标识每个文件对象。

当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。

二、Redis之IO多路复用

1、问答

1.1、Redis采用单线程的原因?

  1. 由于 数据存放在内存中且处理逻辑简单,导致即使是单线程,Redis 可支持的 qps 也相当大,而当 qps 相当大的时候,首先限制性能的是带宽,即不需要把 cpu 的性能挖掘出来,因为在这之前,带宽就不够用了。所以没有必要为了提高 cpu 利用率而使用多线程处理业务逻辑。
  2. 如果加入了多线程,反而会导致一些并发安全的问题,增加Redis的复杂度,降低Redis的性能

1.2、如果万一CPU成为你的Redis瓶颈了,或者,你就是不想让服务器其他核闲置,那怎么办?

多起几个Redis进程。Redis是keyvalue数据库,不是关系数据库,数据之间没有约束。只要客户端分清哪些key放在哪个Redis进程上就可以。redis-cluster可以帮你做的更好。

1.3、单线程模型

Redis客户端对服务端的每次调用都经历了发送命令,执行命令,返回结果三个过程。其中执行命令阶段,由于Redis是单线程来处理命令的,所有每一条到达服务端的命令不会立刻执行,所有的命令都会进入一个队列中,然后逐个被执行。并且多个客户端发送的命令的执行顺序是不确定的。但是可以确定的是不会有两条命令被同时执行,不会产生并发问题,这就是Redis的单线程基本模型。

注:Redis6.0后多线程使用,也只是用在网络IO读写模块

1.4、IO多路复用用来解决Redis的什么问题?

首先了解一下Redis的操作过程:每个客户端建立连接时,都需要服务端为其创建socket套接字,建立连接,然后该客户端的每个请求都要经历以下几步:

  1. 等待请求数据从客户端发送过来
  2. 将请求数据从内核复制到用户进程的缓冲区(buffer)
  3. 对请求数据进行处理(对于Redis来说,就是简单的get/set)

由于操作简单且只涉及内存,所以在第3步的处理时很快速的,时间主要消耗在第1步,所以如果采用普通的BIO模式,每个请求都要经历这几步,那么处理数十万条数据,就要在第1步话费大量的时间,这样会限制QPS的增加。所以需要采用一种更高效的IO处理方式,这就有了IO多路复用模式,即将第1步统一交给第三方(也就是操作系统,操作系统提供了select、poll、epoll、kqueue、iocp等系统调用函数),流程则变化为:

当并发请求到达Redis时,由统一的线程将请求交给操作系统去处理,让操作系统帮忙完成第1步,等到这些请求里的一个或者多个完成了第1步,就将一个集合交给这个线程,让该线程去执行第2、3步,线程遍历处理完集合,又去检查是否有已经就绪的IO,如此循环往复。(不过有些IO模式是将第1、2步都交给了操作系统处理,线程本身只需要处理第3步)

 

简而言之:

由于Redis是跑在单线程中的,所有的操作都是线性执行的,但是由于读写操作等待用户输入或者输出都是阻塞的,所以IO操作一般情况下都是不能直接返回的,这回导致某一文件的IO阻塞导致整个进程无法对其他客户提供服务,而IO多路复用就是为了解决整个问题出现的

1.5、单线程模型每秒万级别处理能力的原因

  1. 纯内存访问。 数据存放在内存中,内存的响应时间大约是 100纳秒 ,这是Redis每秒万亿级别访问的重要基础。
  2. 非阻塞I/O ,Redis采用epoll做为I/O多路复用技术的实现 ,再加上Redis自身的事件处理模型将epoll中的连接,读写,关闭都转换为了事件,不在I/O上浪费过多的时间。
  3. 单线程 避免了线程切换和竞态产的消耗 。
  4. Redis采用单线程模型,每条命令执行如果占用大量时间, 会造成其他线程阻塞,对于Redis这种高性能服务是致命的,所以Redis是面向高速执行的数据库。

2、IO模型(Linux)

 

2.1、阻塞IO模型

当使用 read 或者 write 对某一个文件描述符进行读写时,如果当前 FD 不可读或不可写,整个 Redis 服务就不会对其它的操作作出响应,导致整个服务不可用。

这也就是传统意义上的,也就是我们在编程中使用最多的阻塞模型:

redis用多路复用IO的原因 redis 多路io复用_redis_02

 

阻塞模型在开发中非常常见易于理解,但是它会影响其他FD对应的服务,所以在需要处理多个客户端任务的时候,都不会使用阻塞模型。

2.2、IO多路复用

Redis涉及的重点来了,其他模型在Linux篇再讲。

阻塞式IO模型并不能满足这里的需求,我们需要一种更加高效的IO模型来支撑Redis的多个客户(Redis-cli),Redis采用了IO多路复用模型

1、IO多路复用是什么?

IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程。

2、适用场合

  1. 当客户处理多个描述符时(一般是交互式输入和网络套接口),必须使用I/O复用。
  2. 当一个客户同时处理多个套接口时,而这种情况是可能的,但很少出现。
  3. 如果一个TCP服务器既要处理监听套接口,又要处理已连接套接口,一般也要用到I/O复用。
  4. 如果一个服务器即要处理TCP,又要处理UDP,一般要使用I/O复用。
  5. 如果一个服务器要处理多个服务或多个协议,一般要使用I/O复用。

与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。

目前支持I/O多路复用的系统调用有 select,pselect,poll,epoll,I/O多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,pselect,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。

epoll跟select都能提供多路I/O复用的解决方案。在现在的Linux内核里有都能够支持,其中epoll是Linux所特有,而select则应该是POSIX所规定,一般操作系统均有实现。

POSIX:可移植操作系统接口(英语:Portable Operating System Interface,缩写为POSIX)是IEEE为要在各种UNIX操作系统上运行软件,而定义API的一系列互相关联的标准的总称,其正式称呼为IEEE Std 1003,而国际标准名称为ISO/IEC 9945。此标准源于一个大约开始于1985年的项目。POSIX这个名称是由理查德·斯托曼(RMS)应IEEE的要求而提议的一个易于记忆的名称。它基本上是Portable Operating System Interface(可移植操作系统接口)的缩写,而X则表明其对Unix API的传承。

先看看IO多路复用模型:

redis用多路复用IO的原因 redis 多路io复用_Redis_03

在 I/O 多路复用模型中,最重要的函数调用就是 select,该方法的能够同时监控多个文件描述符的可读可写情况,当其中的某些文件描述符可读或者可写时,select 方法就会返回可读以及可写的文件描述符个数。

这边简略列一下用来实现IO多路复用的几个方法,具体的看Linux详细章节:

select,poll,epoll都是IO多路复用的机制。I/O多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。

  1、select

    select函数监视文件描述符,调用后select函数会阻塞,直到有描述符就绪,或者超时,函数返回,当select函数返回后,就可以遍历描述符,找到就绪的描述符。

    select的一个缺点在于单个进程能够监视的文件描述符的数量也存在最大限制,在Linux上一般为1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制。但是这样也会造成效率的降低。

  2、poll

    没有最大限制(但是数量过大后性能也是会下降)。和select函数一样,poll返回后,需要轮询来获取就绪的描述符。

    select和poll都需要在返回后,通过遍历文件描述符来获取已经就绪的socket。事实上,同时连接的大量客户端在同一时刻可能只有很少的就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。

  3、epoll

    相对于select和poll来说,epoll更加灵活,没有描述符限制(采用的红黑树实现的)。epoll使用一个文件描述符管理多个描述符。

3、Ractor设计模式(Redis的具体实现)

3.1、整体设计结构

Redis 服务采用 Reactor 的方式来实现文件事件处理器(每一个网络连接其实都对应一个文件描述符)

 

redis用多路复用IO的原因 redis 多路io复用_Redis_04

流程概述:当向Redis请求建立连接的时候,会向FD绑定AcceptTCPHandler回调,当连接建立完成绑定ReadQueryFromClient回调,当读取文件完毕,命令完整可执行,进行命令执行(此处是单线程)执行结果写入缓冲区,将该连接加入到client_pending_write,表示数据可回写。while循环处理回写操作(多线程版本),为本次未写完的连接,绑定SendReplyToClient回调,等待下次写就绪

 

文件事件处理器使用 I/O 多路复用模块同时监听多个 FD,当 accept、read、write 和 close 文件事件产生时,文件事件处理器就会回调 FD 绑定的事件处理器。

虽然整个文件事件处理器是在单线程上运行的,但是通过 I/O 多路复用模块的引入,实现了同时对多个 FD 读写的监控,提高了网络通信模型的性能,同时也可以保证整个 Redis 服务实现的简单。

3.2、IO多路复用模块

I/O 多路复用模块封装了底层的 select、epoll、avport 以及 kqueue 这些 I/O 多路复用函数,为上层提供了相同的接口。

整个 I/O 多路复用模块抹平了不同平台上 I/O 多路复用函数的差异性,提供了相同的接口.

redis用多路复用IO的原因 redis 多路io复用_文件描述符_05

子模块的选择

因为 Redis 需要在多个平台上运行,同时为了最大化执行的效率与性能,所以会根据编译平台的不同选择不同的 I/O 多路复用函数作为子模块,提供给上层统一的接口;在 Redis 中,我们通过宏定义的使用,合理的选择不同的子模块

因为 select 函数是作为 POSIX 标准中的系统调用,在不同版本的操作系统上都会实现,所以将其作为保底方案:

redis用多路复用IO的原因 redis 多路io复用_文件描述符_06

Redis 会优先选择时间复杂度为 O(1) 的 I/O 多路复用函数作为底层实现,包括 Solaries 10 中的 evport、Linux 中的 epoll 和 macOS/FreeBSD 中的 kqueue,上述的这些函数都使用了内核内部的结构,并且能够服务几十万的文件描述符。

但是如果当前编译环境没有上述函数,就会选择 select 作为备选方案,由于其在使用时会扫描全部监听的描述符,所以其时间复杂度较差 O(n),并且只能同时服务 1024 个文件描述符,所以一般并不会以 select 作为第一方案使用。

3.3、总结

Redis 对于 I/O 多路复用模块的设计非常简洁,通过宏保证了 I/O 多路复用模块在不同平台上都有着优异的性能,将不同的 I/O 多路复用函数封装成相同的 API 提供给上层使用。

整个模块使 Redis 能以单进程运行的同时服务成千上万个文件描述符,避免了由于多进程应用的引入导致代码实现复杂度的提升,减少了出错的可能性。

注:文章里很多东西其实都可以单独开篇去研究了,以后我们再看能不能整理一下吧,这里就先把Redis的IO多路复用先聊聊,其实这里还有一点没过多的去说,就是Redis的事件驱动模型,之后细说。回见

 

整理有错误的地方欢迎大家指出来哈!