redis基础和使用(三)--单线程与IO多路复用

文章目录

  • 1 Redis为什么是单线程的
    • 1.1 官方解释
    • 1.2 Redis单线程优势
    • 1.3 Redis 不仅仅是单线程
    • 1.4 Redis的性能瓶颈
  • 2 IO多路复用
    • 2.1 文件描述符和文件句柄
    • 2.2 什么是IO多路复用?
    • 2.3 select/poll/epoll比较
    • 2.4 Redis的IO多路复用
  • 3 多线程IO多路复用
    • 3.1 产生背景
    • 3.2 Redis多线程实现思路
    • 3.3 Redis多线程具体实现

1 Redis为什么是单线程的

1.1 官方解释

Redis官网–>Documentation–>Tutorials & FAQ–>FAQ中是这样解释的:

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.

However, to maximize CPU usage you can start multiple instances of Redis in the same box and treat them as different servers. At some point a single box may not be enough anyway, so if you want to use multiple CPUs you can start thinking of some way to shard earlier.

You can find more information about using multiple Redis instances in the Partitioning page.

However with Redis 4.0 we started to make Redis more threaded. For now this is limited to deleting objects in the background, and to blocking commands implemented via Redis modules. For future releases, the plan is to make Redis more and more threaded.

简单翻译理解就是,我们实际应用场景中使用Redis时,CPU成为瓶颈的情况并不常见,因为Redis通常是内存或网络受限的。例如,使用在普通Linux系统上运行的单线程Redis每秒甚至可以发送100万个请求,因此,如果您的应用程序主要使用O(N)或O(log(N))命令,它几乎不会占用太多CPU。
当然,为了最大限度地提高CPU使用率,redis也提供了拓展的解决方案,那就是集群部署的方式,基于这点,我们在使用redis的时候也要尽早进行服务分片算法的设计。

1.2 Redis单线程优势

  • CPU在切换线程的时候,有一个上下文切换时间,而这个上下文切换时间是非常耗时的比如一个CPU主频是 2.6GHz,这意味着每秒可以执行:2.6*10^9 个指令,换算每个指令的时间大概是0.38ns,而一次上下文切换,将近需要耗时2000ns。而这个时间内,CPU什么都干不了,只是做了保存上下文都动作!
  • 多线程的目的,就是通过并发的方式来提升I/O的利用率和CPU的利用率,但是Redis是基于内存的,CPU资源不是Redis的性能瓶颈多线程确实可以提升效率,原因是I/O操作可以分为两个阶段:即等待I/O准备就绪和真正操作I/O资源,在等待就绪阶段,线程是在“阻塞”着等待磁盘。但是提升I/O利用率,并不是只有采用多线程技术这一条路可以走,Redis底层是基于IO多路复用来实现的;
  • 单线程不涉及加锁和解锁等复杂的操作,这块也能节约一定的性能;

1.3 Redis 不仅仅是单线程

我们所说的Redis单线程,指的是"其网络IO和键值对读写是由一个线程完成的",Redis是基于Reactor模式(底层是IO多路复用,可以理解为事件分发)开发的网络事件处理器,这个处理器被称为文件事件处理器。它的组成结构为4部分:多个套接字、IO多路复用程序、文件事件分派器、事件处理器。因为文件事件分派器队列的消费是单线程的,所以Redis才叫单线程模型。但是,Redis实例还有其它线程,Redis 4.0 开始就有多线程的概念了,比如 Redis 通过多线程方式在后台删除对象、以及通过 Redis 模块实现的阻塞命令等。
同时,Redis6.0开始针对数据包的接收和处理使用了多线程,核心的redis操作命令依然保持单线程。

1.4 Redis的性能瓶颈

  • 机器内存大小,内存大小关系到Redis存储的数据量;
  • 网络带宽,Redis客户端执行一条命令分为四个过程:发送命令、命令排队、命令执行、返回结果,而其中发送命令+返回结果这一过程被称为Round Trip Time(RTT,往返时间),这个在redis基础和使用(二)–pipeline中也有介绍;
  • 如果带宽和内存足够,瓶颈还有可能在针对大value的同步IO读写,拷贝数据导致的消耗,这也是redis不建议使用查看所有keys的命令,因为可能导致长时间阻塞,具体的同步IO读写消耗在于两部分:
1.从socket中读取请求数据,会从内核态将数据拷贝到用户态 (read调用)
2.将数据回写到socket,会将数据从用户态拷贝到内核态 (write调用)
这部分数据读写会占用大量的cpu时间,也直接导致了瓶颈。 如果能有多个线程来分担这部分消耗,那redis的吞吐量还能更上一层楼,这也是redis引入多线程IO的目的

2 IO多路复用

2.1 文件描述符和文件句柄

文件描述符(File descriptor):在Linux系统中一切皆可以看成是文件,文件又可分为:普通文件、目录文件、链接文件和设备文件。文件描述符是内核为了高效管理已被打开的文件所创建的索引,其是一个非负整数(通常是小整数),用于指代被打开的文件,所有执行I/O操作的系统调用都通过文件描述符。如下图所示,每个进程会维护一份自己的文件描述符的数组,每次都会使用未被占用的最小的整数。

文件句柄(file handles):也有叫文件流,文件指针,windows的叫法,C语言中使用文件指针做为I/O的句柄。文件指针指向进程用户区中的一个被称为FILE结构的数据结构。FILE结构包括一个缓冲区和一个文件描述符。而文件描述符是文件描述符表的一个索引,因此从某种意义上说文件指针就是句柄的句柄(在Windows系统上,文件描述符被称作文件句柄)。
最后做个总结,其实文件句柄和文件描述符都对应的文件操作,或者IO操作相关的指针信息对应的对象封装,针对网络请求,磁盘操作,文件操作,都是通过文件句柄或者说文件描述符进行操作的。

2.2 什么是IO多路复用?

  • IO 多路复用是一种同步IO模型,实现一个线程可以监视多个文件句柄;
  • 一旦某个文件句柄就绪,就能够通知应用程序进行相应的读写操作;
  • 没有文件句柄就绪就会阻塞应用程序,交出CPU。

    其实逻辑就是,服务器端采用单线程通过 select/poll/epoll 等系统调用获取 fd 列表,遍历有事件的 fd 进行 accept/recv/send ,使其能支持更多的并发连接请求。

// 伪代码描述
while(true) {
  // 通过内核获取有读写事件发生的fd,只要有一个则返回,无则阻塞
  // 整个过程只在调用select、poll、epoll这些调用的时候才会阻塞,accept/recv是不会阻塞
  for (fd in select(fds)) {
    if (fd == listen_fd) {
        client_fd = accept(listen_fd)
        fds.append(client_fd)
    } elseif (len = recv(fd) && len != -1) { 
      // logic
    }
  }  

2.3 select/poll/epoll比较

  • select它仅仅知道,有I/O事件发生了,却并不知道是哪那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流 ,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。同时,单个进程所打开的FD是有限制的,通过 FD_SETSIZE 设置,默认1024 ;
  • poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的/font>;
  • epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是事件驱动(每个事件关联上fd)的 ,此时我们对这些流的操作都是有意义的。(复杂度降低到了O(1))

2.4 Redis的IO多路复用

在Redis 中,每当一个套接字准备好执行连接应答、写入、读取、关闭等操作时,就会产生一个文件事件。Redis的IO多路复用程序的所有功能都是通过包装操作系统的IO多路复用函数库来实现的。每个IO多路复用函数库在Redis源码中都有对应的一个单独的文件。

redis基础和使用(三)--单线程与IO多路复用_第1张图片
具体的单线程Redis底层调度可以参考下面这张图片:
redis基础和使用(三)--单线程与IO多路复用_第2张图片

3 多线程IO多路复用

3.1 产生背景

单线程IO的处理流程以及要解决的问题都很明确,他的瓶颈上面结合Redis也有说到,读写会占用大量的cpu时间, 单线程会阻塞在这里,如果能有多个线程来分担这部分消耗,那Redis的吞吐量还能更上一层楼,这也是Redis6.0引入多线程IO的目的。

3.2 Redis多线程实现思路

Redis 6.0 只有在网络请求的接收和解析,以及请求后的数据通过网络返回给时,使用了多线程。而数据读写操作还是由单线程来完成的 ,这样就不会出现并发问题了。然后也能一定程度解决大数据量的读写导致线程的阻塞。

3.3 Redis多线程具体实现

  • 用一组单独的线程专门进行 read/write socket读写调用 (同步IO)
  • 读回调函数中不再读数据,而是将对应的连接追加到可读clients_pending_read的链表
  • 主线程在beforeSleep中将IO读任务分给IO线程组
  • 主线程自己也处理一个IO读任务,并自旋式等IO线程组处理完,再继续往下
  • 主线程在beforeSleep中将IO写任务分给IO线程组
  • 主线程自己也处理一个IO写任务,并自旋式等IO线程组处理完,再继续往下
  • IO线程组要么同时在读,要么同时在写
  • 命令的执行由主线程串行执行(保持单线程)
流程图可参考下面:

你可能感兴趣的:(温习,redis,单线程,IO多路复用,Redis6.0,epoll)