多核CPU架构
当今的CPU一般会有多颗核心(我们称为物理核心),每颗核心都有自己的一级缓存(简称L1 Cache)与二级缓存(简称L2 Cache),这两集缓存都比较小,一般都是KB级别,CPU核心访问它们一般只有几纳秒,非常快。一级缓存又可以进一步分为指令缓存与数据缓存。但是一级缓存与二级缓存都比较小,可以保存的指令与数据比较少,如果指令或者数据没有在L1与L2 Cache命中,那么就要访问内存,内存的访问速度就比较慢了,一般是100纳秒左右,为此引入了共所有CPU核心共享的三级缓存,它的大小从几MB到几十MB不等。
当前主流的CPU,在一个物理核心上一般可以运行2个超线程,也叫逻辑核,它们共享L1与L2 Cache。如下图所示是一个2个核心的典型CPU架构:
由于L1缓存比L2、L3缓存离CPU核心更近,CPU物理核心访问它们的速度依次是L1>L2>L3,如果指令或者数据没有再L1缓存命中,那么会从L2、L3或者内存加载到L3、L2或者L1缓存上。我们可以通过提高程序再缓存的命中率,提升程序的性能。
多核心架构优化
由于程序运行时,需要再CPU缓存中保持一些诸如栈指针等的运行时信息,程序访问频繁的指令与数据还会保存在L1与L2缓存上。如果程序从CPU物理核心1切换到物理核心2上面,那么缓存中保持的程序运行时信息与L1、L2缓存上保存的指令与数据都要重新从L3缓存甚至时内存中重新加载,从而影响程序的性能。程序从物理核心1切换到物理核心2的过程,我们称之为进程的上下文切换。我们可以通过如下的linux命令查看进程的上下文切换
pidstat -w
如果进程有大量的上下文切换,那么我们就需要考虑绑定CPU核心了,我们可以通过如下的命令绑定核心
taskset -c 1
可以通过如下的命令查看CPU编号等信息
lscpu
NUMA架构
现在很多服务器都有多颗CPU,每颗CPU在不同的socket上面,不同的CPU之间通过总线连接,每颗CPU都有自己的内存与L3缓存,每颗物理核又都有自己的L1与L2缓存。如下图所示极为NUMA架构的CPU
程序可以在任意的CPU1的任意物理核运行。如果程序先在CPU1的核心上运行,然后切换到CPU2上运行,由于程序先在CPU1上运行,故它的运行时信息、指令与数据都保存在CPU1的缓存与内存中,为此当切换到CPU2上运行时,需要访问CPU1的内存上的指令与数据。这种访问非本CPU的内存的指令与数据,我们称之为远端内存访问。由于远端内存离CPU相较本地内存远,因此其访问延迟比本地内存长。
在多CPU架构下,程序访问本地内存与远端的内存的延迟并不一致,我们把这种不一致叫做非统一内存访问(Non Uniform Memory Access,简称NUMA)。这也是CPU NUMA架构的由来。
为了提升网络处理性能,我们一般会把网络中断处理程序绑定到某个CPU物理核心上。我们知道网络的收发流程如下:
那么如果网络中断处理程序绑定到了CPU1物理核心1上,那么套接字缓冲区就在CPU1的本地内存中,如果程序绑定到了CPU2的核心上,那么CPU2收发网络数据就需要访问远端内存,从而导致程序延时变长,影响性能。因此这种情况下,我们需要将应用程序与网络中断处理程序绑定到同一颗CPU的不同核心上。
redis绑核后的问题
我们知道redis的AOF重写与RDB快照生成是通过fork子进程实现的,而绑核操作会继承给子进程,从而导致子进程与主线程争抢同一个核心,影响redis性能。而且redis还会创建一些线程,用于执行异步删除,过期数据删除与缓存淘汰等,他们也会争抢同一个核心的资源。
我们可以通过以下两种方案解决:
1、将redis绑定到一个物理核心上,这样主线程,后台线程与子进程可以共享一个CPU核心的两个逻辑核,也可以在一定程度上缓解争抢CPU资源的问题。需要注意的是CPU的一个物理核的两个逻辑核的编码并不是连续的,Linux会先一次编码物理核心的第一个逻辑核,然后再依次编码第二个逻辑核。对于NUMA架构,会先依次编码不同CPU的物理核的第一个逻辑核,然后再编码第二个物理核。如下所示就是一个NUMA架构的CPU编码
lscpu
Architecture: x86_64
...
NUMA node0 CPU(s): 0-5,12-17
NUMA node1 CPU(s): 6-11,18-23
...
2、修改代码,使得redis的主线程,后台线程与子进程分别绑定到不同的逻辑核上。Linux提供了如下的函数,用于绑定核心
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 号所代表的进程 / 线程绑在对应的逻辑核上。