一、记录零碎的知识

1.KV数据库应该包含的模块:
1.连接模块;2.操作模块;3索引模块;4储存模块;5高可用模块;6集群扩展模块
2.Redis的高效数据结构与底层存储对应的多种数据结构:
1.String:动态简单字符串
2.List:双向链表、压缩数组
3.Hash:压缩数组、hash表
4.Sorted Set: 压缩数组、跳表
5.Set:数组
使用数组和压缩数组的原因是:1.内存使用率的考虑,减少内存碎片;2.CPU高速缓存行的阈值之内使用连续的空间可以直接放在缓存行中,此时的数据访问效率将超过内存,但是大小超过阈值后缓存行失效,这也是Redis设计成数据量超过一定之后将压缩数组换成双向链表或其他数据结构的原因。
3.IO模型与多路复用

4.redis调优——cpu的NUMA架构与Redis性能的关系
4.1 CPU的多级缓存架构
一个 CPU 处理器中一般有多个运行核心,我们把一个运行核心称为一个物理核,每个物理核都可以运行应用程序。每个物理核都拥有私有的一级缓存(Level 1 cache,简称 L1 cache),包括一级指令缓存和一级数据缓存,以及私有的二级缓存(Level 2 cache,简称 L2 cache)
因为 L1 和 L2 缓存是每个物理核私有的,所以,当数据或指令保存在 L1、L2 缓存时,物理核访问它们的延迟不超过 10 纳秒,速度非常快。那么,如果 Redis 把要运行的指令或存取的数据保存在 L1 和 L2 缓存的话,就能高速地访问这些指令和数据。但是,这些 L1 和 L2 缓存的大小受限于处理器的制造技术,一般只有 KB 级别,存不下太多的数据。如果 L1、L2 缓存中没有所需的数据,应用程序就需要访问内存来获取数据。而应用程序的访存延迟一般在百纳秒级别,是访问 L1、L2 缓存的延迟的近 10 倍,不可避免地会对性能造成影响。所以,不同的物理核还会共享一个共同的三级缓存(Level 3 cache,简称为 L3 cache)。L3 缓存能够使用的存储资源比较多,所以一般比较大,能达到几 MB 到几十 MB,这就能让应用程序缓存更多的数据。当 L1、L2 缓存中没有数据缓存时,可以访问 L3,尽可能避免访问内存。
另外,现在主流的 CPU 处理器中,每个物理核通常都会运行两个超线程,也叫作逻辑核。同一个物理核的逻辑核会共享使用 L1、L2 缓存。
image.png

4.2 多CPU Socket
在主流的服务器上,一个 CPU 处理器会有 10 到 20 多个物理核。同时,为了提升服务器的处理能力,服务器上通常还会有多个 CPU 处理器(也称为多 CPU Socket),每个处理器有自己的物理核(包括 L1、L2 缓存),L3 缓存,以及连接的内存,同时,不同处理器间通过总线连接。下图显示的就是多 CPU Socket 的架构,图中有两个 Socket,每个 Socket 有两个物理核。
在多 CPU 架构上,应用程序可以在不同的处理器上运行。在刚才的图中,Redis 可以先在 Socket 1 上运行一段时间,然后再被调度到 Socket 2 上运行。但是,有个地方需要你注意一下:如果应用程序先在一个 Socket 上运行,并且把数据保存到了内存,然后被调度到另一个 Socket 上运行,此时,应用程序再进行内存访问时,就需要访问之前 Socket 上连接的内存,这种访问属于远端内存访问。和访问 Socket 直接连接的内存相比,远端内存访问会增加应用程序的延迟。在多 CPU 架构下,一个应用程序访问所在 Socket 的本地内存和访问远端内存的延迟并不一致,所以,我们也把这个架构称为非统一内存访问架构(Non-Uniform Memory Access,NUMA 架构)。
image.png
image.png
如果网络中断处理程序和 Redis 实例各自所绑的 CPU 核不在同一个 CPU Socket 上,那么,Redis 实例读取网络数据时,就需要跨 CPU Socket 访问内存,这个过程会花费较多时间
所以,我们要避免 Redis 总是在不同 CPU 核上来回调度执行。于是,我们尝试着把 Redis 实例和 CPU 核绑定了,让一个 Redis 实例固定运行在一个 CPU 核上。我们可以使用 taskset 命令把一个程序绑定在一个核上运行。比如说,我们执行下面的命令,就把 Redis 实例绑在了 0 号核上,其中,“-c”选项用于设置要绑定的核编号。

  1. taskset -c 0 ./redis-server

4.3网络中断程序与Redis
网络中断处理程序从网卡硬件中读取数据,并把数据写入到操作系统内核维护的一块内存缓冲区。内核会通过 epoll 机制触发事件,通知 Redis 实例,Redis 实例再把数据从内核的内存缓冲区拷贝到自己的内存空间,如下图所示:
image.png
那么,在 CPU 的 NUMA 架构下,当网络中断处理程序、Redis 实例分别和 CPU 核绑定后,就会有一个潜在的风险:如果网络中断处理程序和 Redis 实例各自所绑的 CPU 核不在同一个 CPU Socket 上,那么,Redis 实例读取网络数据时,就需要跨 CPU Socket 访问内存,这个过程会花费较多时间。
可以看到,图中的网络中断处理程序被绑在了 CPU Socket 1 的某个核上,而 Redis 实例则被绑在了 CPU Socket 2 上。此时,网络中断处理程序读取到的网络数据,被保存在 CPU Socket 1 的本地内存中,当 Redis 实例要访问网络数据时,就需要 Socket 2 通过总线把内存访问命令发送到 Socket 1 上,进行远程访问,时间开销比较大。
image.png
所以,为了避免 Redis 跨 CPU Socket 访问网络数据,我们最好把网络中断程序和 Redis 实例绑在同一个 CPU Socket 上,这样一来,Redis 实例就可以直接从本地内存读取网络数据了,如下图所示:
image.png

lscpu
Architecture: x86_64
...
NUMA node0 CPU(s): 0-5,12-17
NUMA node1 CPU(s): 6-11,18-23...

可以看到,NUMA node0 的 CPU 核编号是 0 到 5、12 到 17。其中,0 到 5 是 node0 上的 6 个物理核中的第一个逻辑核的编号,12 到 17 是相应物理核中的第二个逻辑核编号。NUMA node1 的 CPU 核编号规则和 node0 一样。所以,在绑核时,我们一定要注意,不能想当然地认为第一个 Socket 上的 12 个逻辑核的编号就是 0 到 11。否则,网络中断程序和 Redis 实例就可能绑在了不同的 CPU Socket 上。比如说,如果我们把网络中断程序和 Redis 实例分别绑到编号为 1 和 7 的 CPU 核上,此时,它们仍然是在 2 个 CPU Socket 上,Redis 实例仍然需要跨 Socket 读取网络数据。
在 CPU 多核的场景下,用 taskset 命令把 Redis 实例和一个核绑定,可以减少 Redis 实例在不同核上被来回调度执行的开销,避免较高的尾延迟;在多 CPU 的 NUMA 架构下,如果你对网络中断程序做了绑核操作,建议你同时把 Redis 实例和网络中断程序绑在同一个 CPU Socket 的不同核上,这样可以避免 Redis 跨 Socket 访问内存中的网络数据的时间开销。
4.1绑核的风险和解决方案
方案一:一个 Redis 实例对应绑一个物理核,即绑定两个逻辑核
当我们把 Redis 实例绑到一个 CPU 逻辑核上时,就会导致子进程、后台线程和 Redis 主线程竞争 CPU 资源,一旦子进程或后台线程占用 CPU 时,主线程就会被阻塞,导致 Redis 请求延迟增加。因此同时给Redis实例绑定两个逻辑核可以一定程度上减少CPU竞争,以刚才的NUMA架构为例

taskset -c 0,12 ./redis-server

方案二:优化 Redis 源码
这个方案就是通过修改 Redis 源码,把子进程和后台线程绑到不同的 CPU 核上。
通用的做法,然后,再具体说说可以把这个做法对应到 Redis 的哪部分源码中。通过编程实现绑核时,要用到操作系统提供的 1 个数据结构 cpu_set_t 和 3 个函数 CPU_ZERO、CPU_SET 和 sched_setaffinity。
cpu_set_t 数据结构:是一个位图,每一位用来表示服务器上的一个 CPU 逻辑核。
CPU_ZERO 函数:以 cpu_set_t 结构的位图为输入参数,把位图中所有的位设置为 0。
CPU_SET 函数:以 CPU 逻辑核编号和 cpu_set_t 位图为参数,把位图中和输入的逻辑核编号对应的位设置为 1。
sched_setaffinity 函数:以进程 / 线程 ID 号和 cpu_set_t 为参数,检查 cpu_set_t 中哪一位为 1,就把输入的 ID 号所代表的进程 / 线程绑在对应的逻辑核上。
流程:
第一步:创建一个 cpu_set_t 结构的位图变量;
第二步:使用 CPU_ZERO 函数,把 cpu_set_t 结构的位图所有的位都设置为 0;
第三步:根据要绑定的逻辑核编号,使用 CPU_SET 函数,把 cpu_set_t 结构的位图相应位设置为 1;
第四步:使用 sched_setaffinity 函数,把程序绑定在 cpu_set_t 结构位图中为 1 的逻辑核上。
对于 Redis 来说,生成 RDB 和 AOF 日志重写的子进程分别是下面两个文件的函数中实现的。rdb.c 文件:rdbSaveBackground 函数;aof.c 文件:rewriteAppendOnlyFileBackground 函数。
Redis 6.0 出来后,可以支持 CPU 核绑定的配置操作了,我将在第 38 讲中向你介绍 Redis 6.0 的最新特性.
集合统计的各种数据类型对比
Redis实战——学后总结 - 图7