先交代一下背景,我最近在帮客户排查一台数据库服务器的性能抖动问题,现象很典型:同样是24核的物理机,跑同样的压测脚本,吞吐量曲线却像心电图一样忽高忽低。排查到最后,问题没出在SQL上,也没出在磁盘IO上,而是出在操作系统的NUMA调度策略上——内存被分配到远端节点,跨节点访问的代价被完全低估了。这台机器是支持NUMA架构的双路服务器,物理核和超线程核的拓扑关系直接影响了应用性能。
这篇文章把NUMA节点、物理核、超线程核这三件事串起来聊透,包括它们之间的映射关系、如何用工具把硬件拓扑读出来、怎么在应用层做CPU和内存的亲和性绑定,以及我实际踩过的坑和排查经验。适合运维、DBA、后端开发,以及做性能调优的朋友,尤其是手上有高并发、低延迟业务的,这篇文章值得看完再动手。
1. 从SMP到NUMA:为什么现在的服务器都长这样
1.1 NUMA出现的底层逻辑
先说为什么会有NUMA这个东西。早年CPU主频不高,单颗CPU就能满足大部分计算需求,服务器普遍采用SMP(对称多处理)架构:所有CPU共享一条内存总线,访问内存的延迟和带宽完全一致。但是CPU核数越来越多之后,SMP架构的瓶颈就出现了——多颗CPU抢同一条总线,内存带宽成为系统的最后一道瓶颈。
NUMA(Non-Uniform Memory Access,非一致内存访问)就是为了解决这个瓶颈提出的设计思路。它把CPU和内存划分成多个节点(Node),每个节点内的CPU访问自己的本地内存时速度最快,访问其他节点的远端内存时就要经过互联总线(比如Intel的UPI、AMD的Infinity Fabric),延迟增加,带宽也会打折。换句话说,在这套架构下,内存访问的时间不再统一了,“距离”变成了一个真实存在的成本因子。
这就是“非一致”的含义。一个进程跑在哪个CPU上,它的内存分配在哪个节点上,对性能的影响被放大到了几乎可以决定业务成败的程度。我见过太多生产事故,根因都是NUMA策略默认值导致的跨节点访问,这个问题后面会专门讲。
1.2 NUMA节点与物理核的映射关系
在Intel和AMD的主流服务器平台上,一颗物理CPU通常对应一个NUMA节点。当然也有例外,比如某些大核心数的处理器会把一颗物理CPU划分为多个NUMA节点,AMD的EPYC就是一个典型——一颗CPU内部通过CCD划分出多个NUMA域,每个域内挂着对应的内存控制器。
了解这一点,就能理解为什么“CPU核数”和“NUMA节点数”是两个维度的事情。一颗32核的物理CPU,如果被划分为4个NUMA节点,那么每个节点就包含8个物理核;如果是双路服务器,每颗CPU各占一个节点,那么整机的NUMA节点数就是2,每个节点包含所属CPU的全部物理核和本地内存。
这个拓扑关系,直接影响后续的调度决策。系统默认的调度策略会把进程尽可能分配到与内存同节点的CPU上,但这只是“尽力而为”,一旦发生CPU资源竞争、进程迁移、中断频繁触发,内存落在远端的概率就会明显上升。
1.3 本地访问与远端访问的性能差距
很多人的项目之所以没有感知到NUMA的存在,是因为业务量不够大,跨节点访问的延迟损耗被计算本身的耗时掩盖了。但一旦业务进入高并发状态,这种损耗就会暴露出来。
以Intel双路平台为例,本地内存访问延迟大约在80ns左右,远端访问延迟通常是本地的一倍以上,具体取决于UPI链路负载和距离。更显著的是带宽差异,当多个进程同时访问远端内存时,会争抢有限的UPI通道,带宽可能只有本地访问的60%甚至更低。
这张表的数字不需要精确到个位,关键是理解比例关系:
| 访问类型 | 相对延迟 | 相对带宽 | 典型场景影响 |
|---|---|---|---|
| 本地内存访问 | 1x | 1x | 理想状态,延迟最低带宽最大 |
| 同节点远端内存 | 1.3x ~ 2x | 0.5x ~ 0.8x | 同处理器多核竞争,跨不到节点边界 |
| 跨节点内存访问 | 2x ~ 3x | 0.3x ~ 0.6x | 进程调度失衡,内存分配错位 |
对延迟敏感的应用来说,跨节点访问的影响是立竿见影的。比如Redis、数据库日志写入、高频交易系统,一次内存访问的延迟翻倍,累积到整条调用链上,尾延迟会变得极其难看。
