Redis 6.0 多线程IO 学习笔记

一、异步处理IO

Redis的核心工作负荷是一个单线程在处理,但为什么还那么快?(10万qps)

  • 纯内存操作
  • IO数据的处理是异步的,每个命令从接收到处理,再到返回,会经历多个“不连续”的工序。

这里异步处理IO不是“同步/异步 的IO”,而是IO处理过程是异步的。

假设客户端给redis发送了get aaa指令,redis要处理指令,redis必须完整地接收客户端的请求,并对指令解析,然后读取返回结果,再通过网络回写给客户端。这整个过程分以下几部分:

  • 接收指令:通过TCP接收到指令,可能会经历多次TCP包、ack、网络IO操作。
  • 解析指令:将命令解析出来
  • 执行指令:到对应的地方将value读取出来
  • 返回结果:将value通过TCP返回给客户端,如果value较大,则IO负荷会很重。

其中解析指令和执行指令,是纯CPU、内存操作,而接收指令和返回结果是IO操作。

以接收为例,redis要完整接收客户端指令,有两种策略:
第一种策略:同步串行
在接收到客户端命令时一直等,知道接收到完整的命令,然后执行,再将结果返回,直到客户端收到完整的结果,然后才处理下一条命令。这种同步的策略,过程中有很多等待的时间,例如有个客户端网络不好,那等他完整的命令就会很耗时。

第二种策略:异步并行
客户端的TCP包来一个才处理一个,将数据追加到缓冲区,处理完了就去立即找其他事做,不等待,下一个TCP包来了再继续处理。这种异步的方式,过程中没有额外的空闲等待时间。

很显然,异步的方式效率更高,要实现高并发必须要异步,同步的话就有太多时间浪费在IO等待上了,遇到网络不好时客户端可能直接被拖垮。

异步的策略过程总结如下:

  • 网络包有数据了,就去读一下放入缓冲区,读完立马切换到其他事情上,不等下一个包
  • 解析下缓冲区是否完整,如完整则执行命令,不完整则切到其他事情上
  • 数据完整了,则立即执行命令,将执行结果放到缓冲区
  • 将数据返回给客户端,如果一次给不完,就等下次能给时再给,而不做等待,直到全部给完。

二、事件驱动

异步过程没有额外的瞪大,但redis不一直阻塞等命令来,咋知道“网络包有新数据了”,“下次能给时”这两个时机呢?如果一直去轮询问,肯定效率低,所以需要有个高效机制,来通知redis这两个时刻,从而触发动作,这就是事件驱动。

一个新TCP包来了、客户端再次给客户端发送数据 这两个时机都是事件,与之对应的就是redis和客户端之间socket的可读、可写事件。Linux中通过epoll来做的,redis基于epoll机制抽象除了一套事件驱动框架,由事件驱动,有了事件就处理。

三、单线程IO处理过程

redis启动后会进入一个死循环,在这个循环里一直等待事件发生,事件分为IO事件和timer事件,timer事件是一些定时任务,如expire key等,这里只说IO事件。

epoll处理的是socket的可读、可写事件,当事件发生后,提供一种高效的通知方式,当想要一部监听某个socket的读写事件时,需要去事件驱动框架中注册要监听事件的socket, 以及对应事件的回调function。然后死循环中可以通过epoll_wait不断地去拿可读写事件的socket, 依次处理。

在这里插入图片描述

  • aeMain() 内部是一个死循环,会在epoll_wait里短暂休眠
  • epoll_wait返回的是当前可读、可写的socket列表
  • 核心逻辑都是由IO事件触发,要么可读,要么可写,否则执行timer定时任务
  • 第一次的IO可读事件,是监听socket(如监听6379的socket),当有握手请求时,会执行accept调用,得到一个socket,注册可读回调createClient,往后客户端和redis的数据都通过这个socket进行。
  • 一个完整的命令,可能会通过多次readQueryFromClient才能从socket中读完,这意味着有多次可读IO事件
  • 命令执行的结果回写,大概率会通过多次写回调才能完成
  • 当命令被执行完后,对应的连接会被追加到client_pending_write,beforeSleep会尝试回写到socket,写不完会注册可写事件,下次继续写
  • 整个过程IO全部都是同步非阻塞的,没有浪费等待时间
  • 注册事件的函数叫aeCreateFileEvent

四、单线程IO的瓶颈

从上文可知,单线程IO的处理过程,IO都是非阻塞的,所以虽然是单线程,但qps能到达10万。

但这个模型有几个缺陷:

  • 只能用一个CPU核(忽略后台线程)
  • 如果value比较大,redis的qps会下降的很厉害,有时一个大key就可以拖垮
  • qps很难更上一层楼了

redis主线程的时间注意消耗在这两个方面:

  • 逻辑计算
  • 同步IO的读写,copy数据

当value比较大时,瓶颈会先出现在同步IO上(假设内存和带宽足够),这部分消耗在于两部分:

  • 从socket中读取请求数据,会从内核态将数据copy到用户态(read调用)
  • 将数据会写到socket, 会将数据从用户态copy到内核态(write调用)

这部分会占用大量的CPU时间,从而直接导致了瓶颈。所以,如果有多个线程来分担这部分消耗,那么redis的吞吐量还能更上一层楼,这也是redis引入多线程的目的。

五、多线程IO

上面提到redis要用多线程来分担IO的负荷,具体如下:

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

完整的流程:
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/shijinghan1126/article/details/108986029