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 中的距离值越大,表示跨节点访问越“远”。
从图中可以清楚看到两条路径。第一条是本地路径:核心访问本节点 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 强制指定。
numactl --hardware 输出为准。