昨天的内容中,我们说了Redis变慢有可能存在的2个方面原因:
1、Redis内部的阻塞式操作或者命令
2、CPU核心以及NUMA架构
对于第一部分,主要是Redis自身的实现原理导致的,我们暂时不去做过多讨论。第二部分是在多核心CPU场景下,多核心之间的频繁上下文调度会导致Redis变慢,今天我们更近一步的分析一下多核心CPU场景以及NUMA架构下的Redis运行机制。
多核心的服务器上,Redis实例有可能会被频繁的调用到不同的CPU物理核心上,每次这种CPU物理核心的调度,都会带来运行时指令、信息和数据加载的过程,这会大大影响Redis的性能,为了解决这个问题,我们通常采用一种"绑定CPU核心"的方法来解决这个问题。简称"绑核"
要让一个Redis总是绑定在一个CPU物理核上,我们通常使用taskset命令来实现,例如下面的方法,就可以把Redis服务绑定在核心编号为1的CPU上:
taskset -c 1 redis-server
绑定CPU物理核心,消除了Redis在CPU内核之间频繁切换带来的消耗,因而能够降低Redis的延迟,提升Redis的吞吐率,提升Redis的性能。
到这里,我们知道了,为了避免多核心之间的上下文切换,我们可以通过绑定Redis和某个CPU核心。下面来看NUMA架构对Redis的影响。
昨天我们说了,CPU的NUMA架构,可能带来跨Socket访问,这里我们贴出来昨天的图:
除此之外,其实NUMA架构下,还有一些其他的因素,对Redis性能造成影响。
我们知道Redis之所以运行的如此之快,是基于下面3个特点:
1、纯内存访问
2、单线程架构避免上下文切换和锁竞争
3、IO多路复用
其中,第3点的IO多路复用这里展开一下,通常情况下,套接字网络模型分为阻塞式与非阻塞式,Redis中采用了非阻塞式。具体表现为Redis采用了select/epoll机制,这个机制允许内核中同时存在多个监听套接字和已连接套接字,这就使得Redis不会阻塞在某一个特定的套接字上。它还提供了基于事件的回调机制,当某个监听套接字上有请求到达的时候,select/epoll会触发响应的事件,然后放入一个事件队列中,由Redis对这个事件队列不断进行处理。
而上述过程,离不开Linux操作系统本身的网络中断处理程序,它的存在,才使得一个完整的请求能够被划分为一个一个事件。而这个网络中断处理程序是需要跟Redis进行网络数据交互的,通常情况下,二者应该被绑定在同一个CPU处理器上。如下:
而如果绑核的时候,我们误将Redis实例和中断程序绑定在不同的Socket中,就有可能导致Redis和中断程序的跨Socket交互,这对Redis的性能也是一个很大的影响。
那么既然绑核对Redis这么重要,那么我们来看看Redis中多核心的一些基本知识,这对于我们正确的绑核有很大作用,来看一个线上服务器的输出:
[root ~]# lscpu Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Byte Order: Little Endian CPU(s): 24 On-line CPU(s) list: 0-23 Thread(s) per core: 2 Core(s) per socket: 6 Socket(s): 2 NUMA node(s): 2 Vendor ID: GenuineIntel CPU family: 6 Model: 62 Model name: Intel(R) Xeon(R) CPU E5-2620 v2 @ 2.10GHz Stepping: 4 CPU MHz: 2399.926 CPU max MHz: 2600.0000 CPU min MHz: 1200.0000 BogoMIPS: 4190.02 Virtualization: VT-x L1d cache: 32K L1i cache: 32K L2 cache: 256K L3 cache: 15360K NUMA node0 CPU(s): 0-5,12-17 NUMA node1 CPU(s): 6-11,18-23
使用lscpu查看当前服务器的CPU情况,红色部分可以看出:1、当前服务器有2个CPU处理器,Sockets=2
2、每个socket上有6个物理核心,Cores per socket=6
3、每个物理核心有2个逻辑线程,Threads per core=2
4、累计2*6*2=24个逻辑核心,其中
NUMA node0的CPU核编号是0~5,12~17
NUMA node1的CPU核编号是6~11,18~23
5、其实,上述NUMA编号中,0和12、1和13、...5和17,分别为一个物理核心上的2个逻辑线程。
画图更容易理解:
有了上述结果,我们就能发现,在绑定核心的时候,需要按照编号的分布来进行绑定,而不能按照编号顺序来绑定。而且,本质上,绑定的是逻辑核,而不是物理核。
如果我们把redis绑定在编号为5的核心上,将中断程序绑定在编号为6的核心上,那么二者交互的时候,就会跨Socket访问,从而影响Redis的性能。
既然Redis绑核能够带来这么多收益,那么绑核是不是没有任何的影响呢,自然也不是。
风险点
我们知道Redis在做bgsave或者bgrewriteaof的时候,会产生一个后台的子进程,一旦我们采用Redis绑核,这就会导致后台子进程,后台线程和Redis的主线程之间出现CPU资源竞争,当后台子进程或者后台线程占用了CPU的资源时候,Redis的主线程响应就会被阻塞,从而变慢。
那么这个风险如何解决呢?
在上面的内容中我们提到,Redis绑定核心本质绑定的是逻辑核心,那么如果我们的一个物理核心上有多个逻辑核心,我们将Redis和这个物理核心进行绑定,那么就可以让Redis子进程、后台线程、追线程同时使用多个逻辑核心,而绑定物理核心的命令是:
taskset -c 0,12 redis-server
这里的0和12这两个编号,是CPU物理核心1的2个逻辑核心编号。
这样可以在同一个物理核心上的多个逻辑核心上进行切换,后台线程和子进程对Redis主线程的影响就会减小。