3. 硬件拓扑与NUMA架构:理解CPU与内存的物理连接、NUMA节点概念、跨NUMA访问的代价

大家好,我是你们的系统性能讲师。今天我们来聊聊一个非常核心的话题——硬件拓扑与NUMA架构。

很多朋友在排查内存带宽不足时,第一反应就是看内存频率、看通道数。嗯,这些当然重要。但说实话,我见过太多案例,明明硬件配置很高,内存带宽却死活上不去。问题出在哪?往往就出在CPU和内存的物理连接方式上。

你想想看,现代服务器动辄几十个核心,内存条插了好几根。CPU怎么访问这些内存?是随便访问都一个速度吗?当然不是。这里面的门道,就是我们今天要讲的NUMA。

3.1 从单CPU到多CPU:物理连接的演变

我们先从最简单的模型说起。早年间的单CPU系统,CPU和内存之间通过一条总线连接。CPU要访问内存,走总线就行,延迟固定,带宽共享。那时候排查内存问题,确实简单。

但到了多核、多CPU时代,事情就复杂了。每个CPU都有自己的内存控制器,直接管理一部分物理内存。CPU之间通过高速互联通道(比如Intel的QPI、UPI,AMD的Infinity Fabric)连接。

这就引出了NUMA的核心概念。

3.2 NUMA节点是什么?

NUMA,全称Non-Uniform Memory Access,非统一内存访问。说白了,就是CPU访问不同位置的内存,速度不一样。

一个NUMA节点,通常包含:

  • 一组物理CPU核心(比如一个Socket上的所有核心)
  • 一个本地内存控制器
  • 一部分物理内存条(直接插在这个CPU的内存通道上)
  • 连接到其他节点的互联接口

我习惯把NUMA节点想象成一个「小王国」。每个王国都有自己的仓库(本地内存),国王(CPU核心)拿自己仓库的东西,又快又方便。但如果要去隔壁王国拿东西,就得走一段路(跨节点互联),自然就慢了。

核心要点:在NUMA架构下,CPU访问本地内存(本节点内的内存)的速度,远快于访问远程内存(其他节点的内存)。这个速度差异,就是性能瓶颈的根源。

3.3 跨NUMA访问的代价

代价有多大?我给大家一个直观的数据。在我之前调优过的一台双路服务器上:

访问类型 延迟(纳秒) 相对倍数
本地内存访问(Node 0) ~80 ns 1x
远程内存访问(Node 0 -> Node 1) ~140 ns 1.75x
跨Socket访问(更远节点) ~200 ns 2.5x

看到了吗?跨NUMA访问的延迟,可能是本地访问的2倍甚至更高。带宽呢?同样会大打折扣。因为跨节点互联的带宽是有限的,所有核心共享那几条通道。

为什么会这样?我给大家画个图就明白了。

NUMA Node 0 CPU 0 核心 内存控制器 本地内存 NUMA Node 1 CPU 1 核心 内存控制器 本地内存 跨节点互联 (UPI/IF) 远程访问路径(慢) 本地访问(快)

这张图很直观。CPU 0访问自己的本地内存,走的是绿色路径,一步到位。但如果CPU 0要访问Node 1的内存,就得走红色路径——先经过跨节点互联,再到对方的内存控制器,最后才拿到数据。多绕了一大圈。

我的经验:我曾经帮一个团队排查数据库性能问题。他们的服务器是4路,内存插满了。但业务高峰期,延迟总是飙升。我用numastat一看,好家伙,超过60%的内存访问都是跨节点的。后来调整了内存分配策略和进程绑定,延迟直接降了40%。

3.4 如何查看NUMA拓扑

在Linux系统上,查看NUMA拓扑非常简单。我常用的命令:

# 查看NUMA节点信息
numactl --hardware

# 输出示例:
available: 2 nodes (0-1)
node 0 cpus: 0 1 2 3 4 5 6 7
node 0 size: 32768 MB
node 0 free: 16384 MB
node 1 cpus: 8 9 10 11 12 13 14 15
node 1 size: 32768 MB
node 1 free: 16384 MB
node distances:
node   0   1
  0:  10  20
  1:  20  10

注意看那个node distances表格。10表示本地访问的「相对代价」,20表示跨节点访问的代价。这个数值越大,跨节点访问越慢。

另一个有用的工具是lstopo,它可以画出整个系统的拓扑图:

lstopo --of png topology.png

我个人习惯在排查内存带宽问题时,第一件事就是跑numactl --hardware。先搞清楚系统有几个NUMA节点,每个节点有哪些CPU核心和内存。这就像打仗前先看地图,心里有数。

3.5 跨NUMA访问的典型场景

什么时候会触发跨NUMA访问?我总结了几种常见情况:

  • 进程/线程未绑定CPU:操作系统调度器可能把进程从一个节点迁移到另一个节点,但进程使用的内存还在原节点。
  • 内存分配策略不当:默认的内存分配策略是「本地分配优先」,但如果本地内存不足,就会去远程节点分配。
  • 大页内存配置错误:大页内存如果只在某个节点上预留,其他节点上的进程访问它就会跨节点。
  • 中断亲和性未设置:网卡中断如果落在某个节点,但处理中断的进程在另一个节点,也会产生跨节点访问。

避坑指南:我曾经遇到过一个问题,一台服务器上跑着多个虚拟机,每个虚拟机都申请了巨量内存。结果宿主机内存不够,虚拟机被迫使用远程节点的内存。所有虚拟机性能集体下降。后来我限制了每个虚拟机的内存上限,并强制绑定到固定NUMA节点,问题才解决。

3.6 跨NUMA访问的代价到底有多大?

我再用一个简单的测试来说明。假设我们写一个程序,分别测试本地内存和远程内存的访问速度:

// 伪代码示意
void test_latency(void *addr, int node) {
    // 绑定到指定节点
    bind_to_node(node);
    
    // 访问内存地址
    for (int i = 0; i < 1000000; i++) {
        volatile int tmp = *(volatile int *)addr;
    }
}

实际测试结果:

  • 本地访问:约80纳秒/次
  • 远程访问:约150纳秒/次
  • 带宽差异:本地约40GB/s,远程约25GB/s

你看,带宽直接打了六折。如果你的应用对内存带宽敏感,比如数据库、HPC、大数据处理,这个差距就是致命的。

3.7 小结

好了,今天的内容就到这里。总结一下:

  • NUMA架构下,CPU访问本地内存最快,访问远程内存慢很多。
  • 跨NUMA访问的代价体现在延迟增加和带宽下降。
  • 排查内存带宽问题时,一定要先搞清楚系统的NUMA拓扑。
  • 通过numactl、lstopo等工具可以查看NUMA信息。

记住一句话:让数据靠近计算,让计算使用本地内存。这是NUMA调优的核心思想。

想要更多资料,联系可可