关于后端:高性能IO模型为什么单线程Redis能那么快

1次阅读

共计 4405 个字符,预计需要花费 12 分钟才能阅读完成。

你好,我是蒋德钧。

明天,咱们来探讨一个很多人都很关怀的问题:“为什么单线程的 Redis 能那么快?”

首先,我要和你厘清一个事实,咱们通常说,Redis 是单线程,次要是指Redis 的网络 IO 和键值对读写是由一个线程来实现的,这也是 Redis 对外提供键值存储服务的次要流程。但 Redis 的其余性能,比方长久化、异步删除、集群数据同步等,其实是由额定的线程执行的。

所以,严格来说,Redis 并不是单线程,然而咱们个别把 Redis 称为单线程高性能,这样显得“酷”些。接下来,我也会把 Redis 称为单线程模式。而且,这也会促使你紧接着发问:“为什么用单线程?为什么单线程能这么快?”

要弄明确这个问题,咱们就要深刻地学习下 Redis 的单线程设计机制以及多路复用机制。之后你在调优 Redis 性能时,也能更有针对性地防止会导致 Redis 单线程阻塞的操作,例如执行复杂度高的命令。

好了,话不多说,接下来,咱们就先来学习下 Redis 采纳单线程的起因。

Redis 为什么用单线程?

要更好地了解 Redis 为什么用单线程,咱们就要先理解多线程的开销。

多线程的开销

日常写程序时,咱们常常会听到一种说法:“应用多线程,能够减少零碎吞吐率,或是能够减少零碎扩展性。”确实,对于一个多线程的零碎来说,在有正当的资源分配的状况下,能够减少零碎中解决申请操作的资源实体,进而晋升零碎可能同时解决的申请数,即吞吐率。上面的左图是咱们采纳多线程时所期待的后果。

然而,请你留神,通常状况下,在咱们采纳多线程后,如果没有良好的零碎设计,理论失去的后果,其实是右图所展现的那样。咱们刚开始减少线程数时,零碎吞吐率会减少,然而,再进一步减少线程时,零碎吞吐率就增长缓慢了,有时甚至还会呈现降落的状况。

为什么会呈现这种状况呢?一个要害的瓶颈在于,零碎中通常会存在被多线程同时拜访的共享资源,比方一个共享的数据结构。当有多个线程要批改这个共享资源时,为了保障共享资源的正确性,就须要有额定的机制进行保障,而这个额定的机制,就会带来额定的开销。

拿 Redis 来说,在上节课中,我提到过,Redis 有 List 的数据类型,并提供出队(LPOP)和入队(LPUSH)操作。假如 Redis 采纳多线程设计,如下图所示,当初有两个线程 A 和 B,线程 A 对一个 List 做 LPUSH 操作,并对队列长度加 1。同时,线程 B 对该 List 执行 LPOP 操作,并对队列长度减 1。为了保障队列长度的正确性,Redis 须要让线程 A 和 B 的 LPUSH 和 LPOP 串行执行,这样一来,Redis 能够无误地记录它们对 List 长度的批改。否则,咱们可能就会失去谬误的长度后果。这就是 多线程编程模式面临的共享资源的并发访问控制问题

并发访问控制始终是多线程开发中的一个难点问题,如果没有精密的设计,比如说,只是简略地采纳一个粗粒度互斥锁,就会呈现不现实的后果:即便减少了线程,大部分线程也在期待获取访问共享资源的互斥锁,并行变串行,零碎吞吐率并没有随着线程的减少而减少。

而且,采纳多线程开发个别会引入同步原语来爱护共享资源的并发拜访,这也会升高零碎代码的易调试性和可维护性。为了防止这些问题,Redis 间接采纳了单线程模式。

讲到这里,你应该曾经明确了“Redis 为什么用单线程”,那么,接下来,咱们就来看看,为什么单线程 Redis 能取得高性能。

单线程 Redis 为什么那么快?

通常来说,单线程的解决能力要比多线程差很多,然而 Redis 却能应用单线程模型达到每秒数十万级别的解决能力,这是为什么呢?其实,这是 Redis 多方面设计抉择的一个综合后果。

一方面,Redis 的大部分操作在内存上实现,再加上它采纳了高效的数据结构,例如哈希表和跳表,这是它实现高性能的一个重要起因。另一方面,就是 Redis 采纳了 多路复用机制,使其在网络 IO 操作中能并发解决大量的客户端申请,实现高吞吐率。接下来,咱们就重点学习下多路复用机制。

首先,咱们要弄明确网络操作的根本 IO 模型和潜在的阻塞点。毕竟,Redis 采纳单线程进行 IO,如果线程被阻塞了,就无奈进行多路复用了。

根本 IO 模型与阻塞点

你还记得我在第一节课介绍的具备网络框架的 SimpleKV 吗?

以 Get 申请为例,SimpleKV 为了解决一个 Get 申请,须要监听客户端申请(bind/listen),和客户端建设连贯(accept),从 socket 中读取申请(recv),解析客户端发送申请(parse),依据申请类型读取键值数据(get),最初给客户端返回后果,即向 socket 中写回数据(send)。

下图显示了这一过程,其中,bind/listen、accept、recv、parse 和 send 属于网络 IO 解决,而 get 属于键值数据操作。既然 Redis 是单线程,那么,最根本的一种实现是在一个线程中顺次执行下面说的这些操作。

然而,在这里的网络 IO 操作中,有潜在的阻塞点,别离是 accept()和 recv()。当 Redis 监听到一个客户端有连贯申请,但始终未能胜利建设起连贯时,会阻塞在 accept()函数这里,导致其余客户端无奈和 Redis 建设连贯。相似的,当 Redis 通过 recv()从一个客户端读取数据时,如果数据始终没有达到,Redis 也会始终阻塞在 recv()。

这就导致 Redis 整个线程阻塞,无奈解决其余客户端申请,效率很低。不过,侥幸的是,socket 网络模型自身反对非阻塞模式。

非阻塞模式

Socket 网络模型的非阻塞模式设置,次要体现在三个要害的函数调用上,如果想要应用 socket 非阻塞模式,就必须要理解这三个函数的调用返回类型和设置模式。接下来,咱们就重点学习下它们。

在 socket 模型中,不同操作调用后会返回不同的套接字类型。socket()办法会返回被动套接字,而后调用 listen()办法,将被动套接字转化为监听套接字,此时,能够监听来自客户端的连贯申请。最初,调用 accept()办法接管达到的客户端连贯,并返回已连贯套接字。

针对监听套接字,咱们能够设置非阻塞模式:当 Redis 调用 accept()但始终未有连贯申请达到时,Redis 线程能够返回解决其余操作,而不必始终期待。然而,你要留神的是,调用 accept()时,曾经存在监听套接字了。

尽管 Redis 线程能够不必持续期待,然而总得有机制持续在监听套接字上期待后续连贯申请,并在有申请时告诉 Redis。

相似的,咱们也能够针对已连贯套接字设置非阻塞模式:Redis 调用 recv()后,如果已连贯套接字上始终没有数据达到,Redis 线程同样能够返回解决其余操作。咱们也须要有机制持续监听该已连贯套接字,并在有数据达到时告诉 Redis。

这样能力保障 Redis 线程,既不会像根本 IO 模型中始终在阻塞点期待,也不会导致 Redis 无奈解决理论达到的连贯申请或数据。

到此,Linux 中的 IO 多路复用机制就要退场了。

基于多路复用的高性能 I / O 模型

Linux 中的 IO 多路复用机制是指一个线程解决多个 IO 流,就是咱们常常听到的 select/epoll 机制。简略来说,在 Redis 只运行单线程的状况下,该机制容许内核中,同时存在多个监听套接字和已连贯套接字。内核会始终监听这些套接字上的连贯申请或数据申请。一旦有申请达到,就会交给 Redis 线程解决,这就实现了一个 Redis 线程解决多个 IO 流的成果。

下图就是基于多路复用的 Redis IO 模型。图中的多个 FD 就是方才所说的多个套接字。Redis 网络框架调用 epoll 机制,让内核监听这些套接字。此时,Redis 线程不会阻塞在某一个特定的监听或已连贯套接字上,也就是说,不会阻塞在某一个特定的客户端申请解决上。正因为此,Redis 能够同时和多个客户端连贯并解决申请,从而晋升并发性。

为了在申请达到时能告诉到 Redis 线程,select/epoll 提供了 基于事件的回调机制 ,即 针对不同事件的产生,调用相应的处理函数

那么,回调机制是怎么工作的呢?其实,select/epoll 一旦监测到 FD 上有申请达到时,就会触发相应的事件。

这些事件会被放进一个事件队列,Redis 单线程对该事件队列一直进行解决。这样一来,Redis 无需始终轮询是否有申请理论产生,这就能够防止造成 CPU 资源节约。同时,Redis 在对事件队列中的事件进行解决时,会调用相应的处理函数,这就实现了基于事件的回调。因为 Redis 始终在对事件队列进行解决,所以能及时响应客户端申请,晋升 Redis 的响应性能。

为了不便你了解,我再以连贯申请和读数据申请为例,具体解释一下。

这两个申请别离对应 Accept 事件和 Read 事件,Redis 别离对这两个事件注册 accept 和 get 回调函数。当 Linux 内核监听到有连贯申请或读数据申请时,就会触发 Accept 事件和 Read 事件,此时,内核就会回调 Redis 相应的 accept 和 get 函数进行解决。

这就像病人去医院瞧病。在医生理论诊断前,每个病人(等同于申请)都须要先分诊、测体温、注销等。如果这些工作都由医生来实现,医生的工作效率就会很低。所以,医院都设置了分诊台,分诊台会始终解决这些诊断前的工作(相似于 Linux 内核监听申请),而后再转交给医生做理论诊断。这样即便一个医生(相当于 Redis 单线程),效率也能晋升。

不过,须要留神的是,即便你的利用场景中部署了不同的操作系统,多路复用机制也是实用的。因为这个机制的实现有很多种,既有基于 Linux 零碎下的 select 和 epoll 实现,也有基于 FreeBSD 的 kqueue 实现,以及基于 Solaris 的 evport 实现,这样,你能够依据 Redis 理论运行的操作系统,抉择相应的多路复用实现。

小结

明天,咱们重点学习了 Redis 线程的三个问题:“Redis 真的只有单线程吗?”“为什么用单线程?”“单线程为什么这么快?”

当初,咱们晓得了,Redis 单线程是指它对网络 IO 和数据读写的操作采纳了一个线程,而采纳单线程的一个外围起因是防止多线程开发的并发管制问题。单线程的 Redis 也能取得高性能,跟多路复用的 IO 模型密切相关,因为这防止了 accept()和 send()/recv()潜在的网络 IO 操作阻塞点。

搞懂了这些,你就走在了很多人的后面。如果你身边还有不分明这几个问题的敌人,欢送你分享给他 / 她,解决他们的困惑。

另外,我也剧透下,可能你也留神到了,2020 年 5 月,Redis 6.0 的稳定版公布了,Redis 6.0 中提出了多线程模型。那么,这个多线程模型和这节课所说的 IO 模型有什么关联?会引入简单的并发管制问题吗?会给 Redis 6.0 带来多大晋升?对于这些问题,我会在前面的课程中和你具体介绍。

每课一问

我给你提个小问题,在“Redis 根本 IO 模型”图中,你感觉还有哪些潜在的性能瓶颈吗?欢送在留言区写下你的思考和答案,咱们一起交换探讨。

正文完
 0