Redis - CPU架构对Redis性能的影响
文章目录
- 引言
- 主流CPU架构概览
- 物理核与缓存层次
- 超线程与逻辑核
- 多Socket与NUMA架构
- CPU多核对Redis性能的影响
- 上下文切换的代价
- 实际案例:绑核降低尾延迟
- NUMA架构对Redis性能的影响
- 网络中断与Redis的跨Socket问题
- NUMA下的CPU编号陷阱
- 绑核的风险与解决方案
- 风险:子进程和后台线程的CPU竞争
- 方案一:绑定物理核而非逻辑核
- 方案二:修改源码实现精细绑核
- 总结
引言
很多开发者对Redis和CPU的关系理解比较简单:Redis线程跑在CPU上,CPU越快Redis就越快。这种认知是片面的。现代服务器普遍采用多核CPU和NUMA架构,这些硬件特性会从底层深刻影响Redis的延迟表现和吞吐能力。如果在性能调优时忽略了CPU层面的因素,就可能错过一些关键的优化手段。
本文将从CPU多核架构和NUMA架构两个维度,分析它们对Redis性能的影响机制,并给出实际可操作的绑核优化方案。
主流CPU架构概览
物理核与缓存层次
一个CPU处理器包含多个物理核(Physical Core),每个物理核拥有:
- 私有L1缓存:包括L1指令缓存和L1数据缓存,访问延迟不超过10纳秒
- 私有L2缓存:同样为物理核私有,访问延迟在10纳秒级别
- 共享L3缓存:同一个CPU Socket上的所有物理核共享,容量可达几MB到几十MB
L1和L2缓存虽然速度极快,但容量只有KB级别。当缓存未命中时,程序需要访问内存,延迟会跳到百纳秒级别——接近L1/L2访问延迟的10倍。L3缓存的存在就是为了在L1/L2未命中时提供一个中间层,尽量避免直接访问内存。
超线程与逻辑核
现代CPU的每个物理核通常运行两个超线程(Hyper-Threading),也叫逻辑核。同一物理核的两个逻辑核共享L1和L2缓存。主流服务器上,一个CPU处理器有10到20多个物理核。
多Socket与NUMA架构
为了提升处理能力,服务器通常配备多个CPU处理器(多CPU Socket)。每个Socket有自己的物理核、L1/L2/L3缓存,以及直连的本地内存。不同Socket之间通过总线互联。
这种架构下,应用程序访问本Socket直连的内存(本地内存访问)速度快,而访问其他Socket连接的内存(远端内存访问)则需要跨总线,延迟明显增加。由于不同Socket的内存访问延迟不一致,这种架构被称为非统一内存访问架构(NUMA,Non-Uniform Memory Access)。
CPU多核对Redis性能的影响
上下文切换的代价
Redis主线程在某个CPU核上运行时,会把频繁访问的指令和数据缓存到该核的L1/L2中。一旦操作系统把Redis调度到另一个核上运行,就会发生context switch:
- 运行时信息(栈指针、寄存器值等)需要重新加载到新核
- 新核的L1/L2缓存中没有Redis之前的热点数据,需要从L3甚至内存重新加载
- Redis必须等待加载完成才能继续处理请求
如果Redis被频繁调度到不同核上,每次调度都会有一批请求受到缓存重加载的影响,导致这些请求的延迟明显高于正常水平。
实际案例:绑核降低尾延迟
一个真实的优化案例:在24核服务器上运行Redis实例,使用O(1)复杂度的String类型操作,关闭了RDB和AOF,没有bigkey——排除了所有常见的慢查询因素。但测试结果显示:
- GET 99%尾延迟:504微秒(目标<300微秒)
- PUT 99%尾延迟:1175微秒(目标<500微秒)
排查发现CPU的context switch次数异常偏高。使用taskset命令将Redis实例绑定到固定CPU核后:
taskset-c0./redis-server绑核后的测试结果:
- GET 99%尾延迟:260微秒
- PUT 99%尾延迟:482微秒
尾延迟直接降低了50%以上,达到了预期目标。绑核的本质是让Redis持续复用同一个核的L1/L2缓存,避免了缓存失效带来的性能抖动。
NUMA架构对Redis性能的影响
网络中断与Redis的跨Socket问题
在实际部署中,为了提升网络性能,运维人员经常会把网络中断处理程序绑定到特定CPU核上。网络中断程序从网卡读取数据后,写入内核维护的内存缓冲区,Redis再通过epoll机制从该缓冲区拷贝数据到自己的内存空间。
问题来了:如果网络中断程序绑在Socket 1的某个核上,而Redis实例绑在Socket 2上,那么网络数据存放在Socket 1的本地内存中。Redis读取网络数据时,就需要跨Socket访问——Socket 2通过总线向Socket 1发送内存访问命令,这属于远端内存访问。
实测数据表明,跨Socket的内存访问延迟比本地访问增加了约18%。
NUMA下的CPU编号陷阱
NUMA架构下CPU核的编号规则容易让人踩坑。编号并不是先把一个Socket的所有逻辑核编完再编下一个Socket,而是:
- 先给每个Socket中每个物理核的第一个逻辑核依次编号
- 再给每个Socket中每个物理核的第二个逻辑核依次编号
例如,2个Socket、每个Socket 6个物理核、每个物理核2个逻辑核(共24个逻辑核)的情况下,用lscpu查看:
NUMA node0 CPU(s): 0-5, 12-17 NUMA node1 CPU(s): 6-11, 18-23如果想当然地认为0-11都属于同一个Socket,把网络中断绑到核1、Redis绑到核7,实际上它们分属两个不同的Socket,仍然会产生跨Socket访问。
正确做法:把Redis实例和网络中断处理程序绑在同一个Socket的不同核上。
绑核的风险与解决方案
风险:子进程和后台线程的CPU竞争
Redis除了主线程,还有:
- RDB生成和AOF重写的子进程
- 4.0版本后的惰性删除后台线程
如果把Redis实例绑到单个逻辑核上,这些子进程和后台线程会与主线程竞争同一个核的CPU资源,反而导致请求延迟增加。
方案一:绑定物理核而非逻辑核
不要把Redis绑到单个逻辑核,而是绑到一个完整的物理核(即该物理核的两个逻辑核)。例如:
taskset-c0,12./redis-server这里核0和核12属于同一个物理核的两个逻辑核。这样主线程、子进程、后台线程可以共享使用两个逻辑核,在一定程度上缓解竞争。
方案二:修改源码实现精细绑核
通过编程方式,把子进程和后台线程绑到不同的核上。核心API:
cpu_set_tcpuset;// 位图,表示可用的逻辑核CPU_ZERO(&cpuset);// 清零位图CPU_SET(bindCpu,&cpuset);// 设置目标核sched_setaffinity(0,sizeof(cpuset),&cpuset);// 绑定当前进程/线程对于Redis源码,具体的修改点:
- 后台线程:在
bio.c的bioProcessBackgroundJobs函数中加入绑核操作 - RDB子进程:在
rdb.c的rdbSaveBackground函数的fork后子进程代码中加入绑核操作 - AOF重写子进程:在
aof.c的rewriteAppendOnlyFileBackground函数的fork后子进程代码中加入绑核操作
这种方案可以让主线程独占一个核,子进程和后台线程使用其他核,彻底避免CPU竞争。
Redis 6.0已经原生支持了CPU核绑定的配置操作,可以通过配置文件直接指定主线程、后台线程、子进程分别使用哪些核。
总结
CPU架构对Redis性能的影响主要体现在两个层面:
多核场景下的context switch:Redis在不同核间频繁调度会导致L1/L2缓存失效,表现为尾延迟升高。解决方案是使用taskset绑核。
NUMA架构下的远端内存访问:网络中断程序和Redis实例如果分属不同Socket,网络数据读取会产生跨Socket延迟。解决方案是确保它们绑在同一个Socket上。
绑核时需要注意CPU竞争问题,推荐绑定物理核而非单个逻辑核,或者通过源码修改实现主线程与子进程/后台线程的核隔离。在部署多实例Redis集群时,建议将实例均匀分布在不同Socket上,充分利用各Socket的L3缓存和本地内存资源。
