2. NUMA硬件拓扑与内存访问原理

要理解多核服务器上网络性能为何会“忽高忽低”,必须先看清 NUMA 的硬件拓扑。NUMA(Non-Uniform Memory Access,非统一内存访问)的核心事实是:CPU 访问不同物理地址的内存,代价并不相同。离 CPU 近的内存访问快,离得远的内存访问慢,而“远近”由硬件拓扑决定,不由操作系统随意改变。

一个 NUMA 节点(node)通常由三部分组成:一组 CPU 核心(含各自的私有缓存与共享的末级缓存 LLC)、一个集成内存控制器(IMC,Integrated Memory Controller),以及挂在该控制器下的一段本地物理内存(DIMM)。在主流 x86 服务器上,内存控制器已集成进 CPU 芯片,因此“节点”往往与一颗物理 CPU 插槽(socket)高度对应。多颗 CPU 之间通过片间互连连接,Intel 平台常见的是 UPI(Ultra Path Interconnect),AMD EPYC 平台则是 Infinity Fabric。互连是跨节点访问的必经通道,也是远程访问延迟和带宽损耗的主要来源。

关键点在于:物理地址到节点的映射由硬件在启动时确定,通常按地址区间交错(interleave)分配到各内存控制器。因此,一块内存条物理上插在哪个通道、哪个插槽,直接决定了它属于哪个节点。操作系统通过 ACPI 的 SRAT(Static Resource Affinity Table)和 SLIT(System Locality Information Table)获知节点划分与节点间距离,SLIT 中的距离值越大,表示跨节点访问越“远”。

双路 NUMA 服务器硬件拓扑与访问路径 NUMA Node 0(Socket 0) CPU 核心 0–15 + 共享 LLC 私有 L1/L2,共享 L3 缓存 集成内存控制器 IMC0 DDR5 通道 A/B/C/D 本地内存 DIMM0–3 物理地址区间 0x0000_0000 起 本地访问 NUMA Node 1(Socket 1) CPU 核心 16–31 + 共享 LLC 私有 L1/L2,共享 L3 缓存 集成内存控制器 IMC1 DDR5 通道 E/F/G/H 本地内存 DIMM4–7 物理地址区间 0x4000_0000 起 本地访问 片间互连 UPI / Infinity Fabric 跨节点访问必经通道 远程访问路径(延迟更高、带宽更低) 本地访问:延迟约 80–100 ns,带宽可达单通道峰值 远程访问:延迟约 140–200 ns,带宽受互连限制

从图中可以清楚看到两条路径。第一条是本地路径:核心访问本节点 IMC 下的内存,数据不经过片间互连,延迟最低、带宽最高。第二条是远程路径:核心 0 要访问 Node 1 的内存,请求必须经 UPI/Infinity Fabric 转发到 IMC1,再返回结果。这条路径多了一次互连跳转,延迟显著增加,同时互连带宽是共享资源,多个核心同时跨节点访问时会互相争抢。

延迟差异的量级需要建立直观认识。以典型双路服务器为例,本地内存访问延迟约 80–100 ns,远程访问约 140–200 ns,差距接近一倍。带宽方面,本地访问可跑满单通道 DDR5 的峰值,而远程访问受限于互连带宽,通常只能达到本地带宽的 50%–70%。这些数值随平台代际变化,但“本地快、远程慢”的定性结论始终成立。对网络性能而言,这意味着:如果网卡中断处理线程运行在 Node 0,而它访问的收包缓冲区、连接跟踪表、socket 结构体分配在 Node 1,每个数据包的处理都会额外付出跨节点延迟,吞吐和时延同时受损。

更隐蔽的问题来自缓存一致性。跨节点访问不仅要走互连,还要参与缓存一致性协议(如 MESIF/MOESI)。当两个节点的核心同时读写同一缓存行时,会产生缓存行在节点间来回迁移(cache line bouncing),互连上出现大量一致性流量。这种流量不产生有效数据搬运,却占用了宝贵的互连带宽,是 NUMA 系统上多线程网络程序性能退化的常见原因。

那么实际中如何观察和判断?Linux 提供了几个直接工具。用 numactl --hardware 可以查看节点数量、每节点内存大小和节点间距离:

$ numactl --hardware
available: 2 nodes (0-1)
node 0 cpus: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
node 0 size: 32768 MB
node 0 free: 28000 MB
node 1 cpus: 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
node 1 size: 32768 MB
node 1 free: 30000 MB
node distances:
node   0   1
  0:  10  21
  1:  21  10

距离矩阵中,本地为 10,远程为 21,比值约 2.1,与前面延迟差异的量级吻合。这个矩阵就是 SLIT 在用户态的呈现。另一个工具是 numastat,它按节点统计内存分配与命中情况:

$ numastat -p $(pgrep nginx | head -1)
Per-node process memory usage (in MBs)
Node 0    Node 1    Total
Heap    120.5     45.2      165.7
Stack     2.1      0.8        2.9
Private 380.0    210.0      590.0
Total   502.6    256.0      758.6

如果 Total 中 Node 1 的占比明显偏高,而进程主要运行在 Node 0,就说明存在远程内存访问。进一步可以用 perf stat -e node-loads,node-load-misses 观察跨节点加载事件,或用 numastat -m 查看系统级各节点内存使用是否均衡。

判断是否达成优化目标,可以看三个信号:一是进程的本地内存占比是否接近其运行节点,通常希望本地命中率在 90% 以上;二是 numastat 中 remote 相关计数是否持续下降;三是网络吞吐或 P99 时延是否随绑定策略改善。如果绑定后远程访问计数不降反升,往往说明内存是在绑定之前就已分配,需要重新启动进程或使用 numactl --membind 强制指定。

NUMA 距离矩阵中的数值是相对值,不是纳秒。它只用于比较节点间远近,不同平台、不同 BIOS 设置下同一对节点的距离值可能不同,判断时应以本机 numactl --hardware 输出为准。