Redis 多核CPU与NUMA架构优化

多核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 号所代表的进程 / 线程绑在对应的逻辑核上。
  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值