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到几十MBL1和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的内存访问延迟不一致这种架构被称为非统一内存访问架构NUMANon-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属于同一个物理核的两个逻辑核。这样主线程、子进程、后台线程可以共享使用两个逻辑核在一定程度上缓解竞争。方案二修改源码实现精细绑核通过编程方式把子进程和后台线程绑到不同的核上。核心APIcpu_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 switchRedis在不同核间频繁调度会导致L1/L2缓存失效表现为尾延迟升高。解决方案是使用taskset绑核。NUMA架构下的远端内存访问网络中断程序和Redis实例如果分属不同Socket网络数据读取会产生跨Socket延迟。解决方案是确保它们绑在同一个Socket上。绑核时需要注意CPU竞争问题推荐绑定物理核而非单个逻辑核或者通过源码修改实现主线程与子进程/后台线程的核隔离。在部署多实例Redis集群时建议将实例均匀分布在不同Socket上充分利用各Socket的L3缓存和本地内存资源。