Redis单线程处理IO请求性能瓶颈

Redis单线程处理IO请求性能瓶颈主要包括2个方面:

1、任意一个请求在server中一旦发生耗时,都会影响整个server的性能,也就是说后面的请求都要等前面这个耗时请求处理完成,自己才能被处理到。耗时的操作包括以下几种:
a、操作bigkey:写入一个bigkey在分配内存时需要消耗更多的时间,同样,删除bigkey释放内存同样会产生耗时;
b、使用复杂度过高的命令:例如SORT/SUNION/ZUNIONSTORE,或者O(N)命令,但是N很大,例如lrange key 0 -1一次查询全量数据;
c、大量key集中过期:Redis的过期机制也是在主线程中执行的,大量key集中过期会导致处理一个请求时,耗时都在删除过期key,耗时变长;
d、淘汰策略:淘汰策略也是在主线程执行的,当内存超过Redis内存上限后,每次写入都需要淘汰一些key,也会造成耗时变长;
e、AOF刷盘开启always机制:每次写入都需要把这个操作刷到磁盘,写磁盘的速度远比写内存慢,会拖慢Redis的性能;
f、主从全量同步生成RDB:虽然采用fork子进程生成数据快照,但fork这一瞬间也是会阻塞整个线程的,实例越大,阻塞时间越久;
2、并发量非常大时,单线程读写客户端IO数据存在性能瓶颈,虽然采用IO多路复用机制,但是读写客户端数据依旧是同步IO,只能单线程依次读取客户端的数据,无法利用到CPU多核。

针对问题1,一方面需要业务人员去规避,一方面Redis在4.0推出了lazy-free机制,把bigkey释放内存的耗时操作放在了异步线程中执行,降低对主线程的影响。

针对问题2,Redis在6.0推出了多线程,可以在高并发场景下利用CPU多核多线程读写客户端数据,进一步提升server性能,当然,只是针对客户端的读写是并行的,每个命令的真正操作依旧是单线程的。
Redis单线程处理IO请求性能瓶颈_第1张图片
aeMain() 内部是一个死循环,会在 epoll_wait 处短暂休眠
epoll_wait 返回的是当前可读、可写的 socket 列表
beforeSleep 是进入休眠前执行的逻辑,核心是回写数据到 socket
核心逻辑都是由 IO 事件触发,要么可读,要么可写,否则执行 timer 定时任务
第一次的 IO 可读事件,是监听 socket(如监听 6379 的 socket),当有握手请求时,会执行 accept 调用,得到一个连接 socket,注册可读回调 createClient,往后客户端和 redis 的数据都通过这个 socket 进行
一个完整的命令,可能会通过多次 readQueryFromClient 才能从 socket 读完,这意味这多次可读 IO 事件
命令执行的结果会写,也是这样,大概率会通过多次可写回调才能写完
当命令被执行完后,对应的连接会被追加到 clients_pending_write,beforeSleep 会尝试回写到 socket,写不完会注册可写事件,下次继续写
整个过程 IO 全部都是同步非阻塞,没有浪费等待时间
注册事件的函数叫 aeCreateFileEvent

多线程 IO

上面已经梳理了单线程 IO 的处理流程,以及多线程 IO 要解决的问题,接下来将目光放到: 如何用多线程分担 IO 的负荷。其做法用简单的话来说就是:

用一组单独的线程专门进行 read/write socket 读写调用(同步 IO)
读回调函数中不再读数据,而是将对应的连接追加到可读 clients_pending_read 的链表
主线程在 beforeSleep 中将 IO 读任务分给 IO 线程组
主线程自己也处理一个 IO 读任务,并自旋式等 IO 线程组处理完,再继续往下
主线程在 beforeSleep 中将 IO 写任务分给 IO 线程组
主线程自己也处理一个 IO 写任务,并自旋式等 IO 线程组处理完,再继续往下
IO 线程组要么同时在读,要么同时在写
命令的执行由主线程串行执行 (保持单线程)
IO 线程数量可配置
Redis单线程处理IO请求性能瓶颈_第2张图片
beforesleep 中,先让 IO 线程读数据,然后再让 IO 线程写数据。 读写时,多线程能并发执行,利用多核。

将读任务均匀分发到各个 IO 线程的任务链表 io_threads_list[i],将 io_threads_pending[i] 设置为对应的任务数,此时 IO 线程将从死循环中被激活,开始执行任务,执行完毕后,会将 io_threads_pending[i] 清零。 函数名为: handleClientsWithPendingReadsUsingThreads

将写任务均匀分发到各个 IO 线程的任务链表 io_threads_list[i],将 io_threads_pending[i] 设置为对应的任务数,此时 IO 线程将从死循环中被激活,开始执行任务,执行完毕后,会将 io_threads_pending[i] 清零。 函数名为: handleClientsWithPendingWritesUsingThreads

beforeSleep 中主线程也会执行其中一个任务 (图中忽略了),执行完后自旋等待 IO 线程处理完。

读任务要么在 beforeSleep 中被执行,要么在 IO 线程被执行,不会再在读回调中执行

写任务会分散到 beforeSleep、IO 线程、写回调中执行

主线程和 IO 线程交互是无锁的,通过标志位设置进行,不会同时写任务链表

性能据测试提升了一倍以上 (4 个 IO 线程)。
部分内容转载自 https://ruby-china.org/topics/39925

你可能感兴趣的:(数据库,redis,epoll)