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倍甚至更高。带宽呢?同样会大打折扣。因为跨节点互联的带宽是有限的,所有核心共享那几条通道。
为什么会这样?我给大家画个图就明白了。
这张图很直观。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调优的核心思想。
想要更多资料,联系可可