在单插槽系统上,所有 CPU 核心访问内存的延迟相同——这叫 UMA(Uniform Memory Access)。但在多插槽系统上,每个 CPU 插槽有自己的本地内存,访问本地内存快,访问远端内存慢——这就是 NUMA(Non-Uniform Memory Access)。
NUMA 不是”问题”,而是多插槽系统的必然结果。理解 NUMA,才能在多插槽服务器上获得预期的性能。
一、从 UMA 到 NUMA
1.1 为什么需要 NUMA?
当 CPU 核心数增加时,前端总线(FSB)成为瓶颈——所有核心竞争同一条总线访问内存。NUMA 的解决方案:给每个 CPU 插槽分配本地内存和本地内存控制器,插槽之间通过互联总线(Interconnect)连接。
1.2 NUMA 的延迟差异
| 访问类型 | 典型延迟 | 相对延迟 |
|---|---|---|
| 本地 L1 | ~1 ns | 1x |
| 本地 L2 | ~4 ns | 4x |
| 本地 L3 | ~12 ns | 12x |
| 本地 DRAM | ~80 ns | 80x |
| 远端 DRAM(1 跳) | ~140 ns | 140x |
| 远端 DRAM(2 跳) | ~200 ns | 200x |
| 远端 DRAM(3 跳) | ~260 ns | 260x |
“1 跳”表示数据经过 1 段互联总线。4 插槽系统中,最远的两个插槽之间可能需要 2-3 跳。Intel 的 UPI(Ultra Path Interconnect)和 AMD 的 Infinity Fabric 是不同的互联技术,但延迟特征类似。
二、NUMA 拓扑
2.1 常见拓扑结构
2.2 查看 NUMA 拓扑
# 查看 NUMA 节点信息numactl --hardware# available: 2 nodes (0-1)# node 0 size: 64217 MB# node 1 size: 64509 MB# node 0 free: 58123 MB# node 1 free: 58456 MB# node distances:# node 0 1# 0: 10 21# 1: 21 10
# 距离矩阵:10 = 本地,21 = 远端(2.1x 延迟)
# 查看进程的 NUMA 内存分布numastat -p $(pidof your_program)
# 查看每个 NUMA 节点的 CPU 列表lscpu | grep -i numa2.3 NUMA 距离矩阵
| Node 0 | Node 1 | Node 2 | Node 3 | |
|---|---|---|---|---|
| Node 0 | 10 | 21 | 31 | 21 |
| Node 1 | 21 | 10 | 21 | 31 |
| Node 2 | 31 | 21 | 10 | 21 |
| Node 3 | 21 | 31 | 21 | 10 |
距离值 10 = 本地,21 = 1 跳,31 = 2 跳。延迟与距离成正比。
三、NUMA 对性能的影响
3.1 内存带宽
| 访问类型 | 带宽 | 相对带宽 |
|---|---|---|
| 本地 DRAM | ~50 GB/s | 1.0x |
| 远端 DRAM(1 跳) | ~35 GB/s | 0.7x |
| 远端 DRAM(2 跳) | ~25 GB/s | 0.5x |
3.2 典型场景的性能影响
| 场景 | NUMA 感知 | NUMA 无感知 | 性能差异 |
|---|---|---|---|
| 数据库(OLTP) | 本地内存 | 随机分布 | 20-40% |
| 内存数据库 | 本地内存 | 跨节点 | 30-50% |
| 大数据处理 | 本地分配 | 默认分配 | 10-30% |
| 多线程计算 | 线程绑定 | 不绑定 | 15-35% |
3.3 跨 NUMA 的缓存一致性
跨 NUMA 节点的缓存一致性消息需要经过互联总线,延迟更高:
四、NUMA 感知编程
4.1 内存分配策略
| 策略 | 行为 | 适用场景 |
|---|---|---|
| default | 在当前线程所在的 NUMA 节点分配 | 通用 |
| bind | 绑定到指定 NUMA 节点 | 需要确定性延迟 |
| interleave | 交错分配到所有节点 | 带宽密集型 |
| preferred | 优先在指定节点分配,不足时用其他 | 灵活 |
4.2 numactl 工具
# 绑定到 Node 0 分配内存numactl --membind=0 ./your_program
# 交错分配内存(适合带宽密集型)numactl --interleave=all ./your_program
# 绑定 CPU 和内存到同一节点numactl --cpunodebind=0 --membind=0 ./your_program
# 查看当前 NUMA 策略numactl --show4.3 代码中的 NUMA 感知
#include <numa.h>#include <numaif.h>#include <stdio.h>
void numa_aware_allocation() { if (numa_available() < 0) { printf("NUMA 不可用\n"); return; }
int num_nodes = numa_num_configured_nodes(); printf("NUMA 节点数: %d\n", num_nodes);
// 在当前线程所在的 NUMA 节点分配内存 int preferred_node = numa_node_of_cpu(sched_getcpu()); void *local_mem = numa_alloc_onnode(1024 * 1024, preferred_node);
// 交错分配 void *interleaved_mem = numa_alloc_interleaved(1024 * 1024 * 4);
// 绑定到指定节点 void *bound_mem = numa_alloc_onnode(1024 * 1024, 0);
numa_free(local_mem, 1024 * 1024); numa_free(interleaved_mem, 1024 * 1024 * 4); numa_free(bound_mem, 1024 * 1024);}4.4 线程绑定(CPU Affinity)
#define _GNU_SOURCE#include <sched.h>#include <pthread.h>
void bind_to_node(int node) { cpu_set_t cpuset; CPU_ZERO(&cpuset);
// 获取该节点的 CPU 列表 struct bitmask *cpus = numa_allocate_cpumask(); numa_node_to_cpus(node, cpus); for (int i = 0; i < numa_num_configured_cpus(); i++) { if (numa_bitmask_isbitset(cpus, i)) { CPU_SET(i, &cpuset); } } numa_free_cpumask(cpus);
pthread_setaffinity_np(pthread_self(), sizeof(cpuset), &cpuset);}五、NUMA 与数据库
5.1 PostgreSQL 的 NUMA 优化
# postgresql.conf# 绑定到指定 NUMA 节点# 使用 numactl 启动numactl --cpunodebind=0 --membind=0 pg_ctl start
# 共享缓冲区大小应考虑 NUMA 节点内存shared_buffers = 32GB # 不超过单个 NUMA 节点的内存5.2 MySQL 的 NUMA 问题
MySQL 的 Buffer Pool 是一个大内存区域,默认分配在启动线程所在的 NUMA 节点上。后续其他节点的线程访问 Buffer Pool 时都是远端访问。
解决方案:
# 方案 1:交错分配numactl --interleave=all mysqld
# 方案 2:每个 NUMA 节点一个 MySQL 实例numactl --cpunodebind=0 --membind=0 mysqld --port=3306numactl --cpunodebind=1 --membind=1 mysqld --port=3307六、NUMA 与虚拟化
6.1 虚拟机的 NUMA 拓扑
KVM 支持将虚拟机的 vCPU 和内存绑定到物理 NUMA 节点:
<!-- libvirt XML 配置 --><numatune> <memory mode='strict' nodeset='0'/></numatune><vcpu placement='static' cpuset='0-15'>16</vcpu><cputune> <vcpupin vcpu='0' cpuset='0'/> <vcpupin vcpu='1' cpuset='1'/> <!-- ... --></cputune>6.2 容器的 NUMA 感知
# Docker 绑定到 NUMA 节点docker run --cpuset-cpus=0-15 --cpuset-mems=0 your_image
# Kubernetes NUMA 感知调度# 需要启用 CPU Manager 和 Device Manager# kubelet 配置:# --cpu-manager-policy=static# --topology-manager-policy=best-effort七、动手实验
7.1 实验 1:测量 NUMA 延迟差异
#include <stdio.h>#include <stdlib.h>#include <time.h>#include <numa.h>#include <numaif.h>
#define SIZE (64 * 1024 * 1024) // 64M int = 256MB
int main() { if (numa_available() < 0) { printf("NUMA 不可用\n"); return 1; }
int num_nodes = numa_num_configured_nodes(); printf("NUMA 节点数: %d\n", num_nodes);
for (int node = 0; node < num_nodes; node++) { int *arr = numa_alloc_onnode(SIZE * sizeof(int), node); for (int i = 0; i < SIZE; i++) arr[i] = i;
clock_t start = clock(); long long sum = 0; for (int i = 0; i < SIZE; i++) sum += arr[i]; clock_t end = clock();
printf("Node %d 分配: %.3f 秒\n", node, (double)(end - start) / CLOCKS_PER_SEC); numa_free(arr, SIZE * sizeof(int)); } return 0;}7.2 实验 2:numactl 的效果
# 本地访问numactl --cpunodebind=0 --membind=0 ./memory_benchmark
# 远端访问numactl --cpunodebind=0 --membind=1 ./memory_benchmark
# 交错分配numactl --interleave=all ./memory_benchmark
# 对比三种方式的延迟7.3 实验 3:观察 NUMA 统计
# 查看 NUMA 内存分配统计numastat
# 查看进程的 NUMA 内存分布numastat -p $(pidof your_program)
# 查看 NUMA 命中/未命中perf stat -e numa_hit,numa_miss,numa_foreign ./your_program八、NUMA 感知编程深入
8.1 NUMA 感知内存分配策略详解
不同策略对延迟和带宽的影响差异巨大,选错策略可能让延迟翻倍:
| 策略 | 命令 | 延迟 | 带宽 | 适用场景 |
|---|---|---|---|---|
| bind | --membind=0 | 最低(本地) | 单节点带宽 | 延迟敏感 |
| interleave | --interleave=all | 中等(平均) | 最大聚合带宽 | 带宽敏感 |
| preferred | --preferred=0 | 低(优先本地) | 中等 | 通用 |
| default | 默认 | 取决于首次访问 | 取决于分配 | 不推荐 |
8.2 线程放置策略
// NUMA 感知的线程池#define _GNU_SOURCE#include <sched.h>#include <pthread.h>#include <numa.h>
struct thread_config { int numa_node; int cpu_within_node; void* (*func)(void*); void* arg;};
void* numa_thread_entry(void* arg) { struct thread_config *cfg = (struct thread_config*)arg;
// 1. 绑定到指定 NUMA 节点的 CPU cpu_set_t cpuset; CPU_ZERO(&cpuset); struct bitmask *cpus = numa_allocate_cpumask(); numa_node_to_cpus(cfg->numa_node, cpus); int cpu_list[64], n = 0; for (int i = 0; i < numa_num_configured_cpus(); i++) { if (numa_bitmask_isbitset(cpus, i)) cpu_list[n++] = i; } CPU_SET(cpu_list[cfg->cpu_within_node % n], &cpuset); pthread_setaffinity_np(pthread_self(), sizeof(cpuset), &cpuset); numa_free_cpumask(cpus);
// 2. 设置内存分配策略为本地节点 struct bitmask *nodemask = numa_allocate_nodemask(); numa_bitmask_setbit(nodemask, cfg->numa_node); set_mempolicy(MPOL_BIND, nodemask->maskp, nodemask->size + 1); numa_free_nodemask(nodemask);
// 3. 执行线程函数 return cfg->func(cfg->arg);}8.3 NUMA 感知的数据结构
分区哈希表:每个 NUMA 节点一个分区,线程只访问本地分区:
// NUMA 分区哈希表struct numa_hash_table { int num_nodes; struct hash_bucket **partitions; // 每个节点一个分区};
// 创建:每个分区在对应 NUMA 节点分配struct numa_hash_table* numa_ht_create(int capacity) { struct numa_hash_table *ht = malloc(sizeof(*ht)); ht->num_nodes = numa_num_configured_nodes(); ht->partitions = malloc(ht->num_nodes * sizeof(void*));
for (int i = 0; i < ht->num_nodes; i++) { // 在 NUMA 节点 i 上分配分区 ht->partitions[i] = numa_alloc_onnode( capacity / ht->num_nodes * sizeof(struct hash_bucket), i); } return ht;}
// 查找:只访问本地分区(零跨节点访问)struct hash_entry* numa_ht_lookup(struct numa_hash_table *ht, int key) { int node = numa_node_of_cpu(sched_getcpu()); // 当前线程所在节点 int local_key = key / ht->num_nodes; // 映射到本地分区 return hash_lookup(ht->partitions[node], local_key);}8.4 性能影响实测
// NUMA 延迟基准测试#include <stdio.h>#include <stdlib.h>#include <time.h>#include <numa.h>
#define SIZE (64 * 1024 * 1024) // 256MB
int main() { if (numa_available() < 0) { printf("NUMA 不可用\n"); return 1; }
int nn = numa_num_configured_nodes(); int cpu_node = numa_node_of_cpu(sched_getcpu());
printf("当前线程在 Node %d\n\n", cpu_node); printf("%-15s %-15s %-10s\n", "分配节点", "访问延迟(ms)", "相对延迟");
for (int mem_node = 0; mem_node < nn; mem_node++) { int *arr = numa_alloc_onnode(SIZE * sizeof(int), mem_node); for (int i = 0; i < SIZE; i++) arr[i] = i;
clock_t start = clock(); volatile long long sum = 0; for (int i = 0; i < SIZE; i++) sum += arr[i]; clock_t end = clock();
double ms = (double)(end - start) / CLOCKS_PER_SEC * 1000; printf("Node %-8d %-12.1f %.1fx\n", mem_node, ms, mem_node == cpu_node ? 1.0 : ms / 100.0); numa_free(arr, SIZE * sizeof(int)); } return 0;}| 场景 | 本地访问 | 远端访问 | 差异 |
|---|---|---|---|
| 顺序遍历 256MB | ~80 ms | ~140 ms | 1.75x |
| 随机访问 256MB | ~500 ms | ~900 ms | 1.8x |
| 原子操作 | ~50 ns/op | ~120 ns/op | 2.4x |
NUMA 感知编程的一个常见错误是”过度绑定”——把所有线程和内存都绑到 Node 0,让其他节点空闲。正确的做法是均匀分配:每个节点承载 1/N 的线程和内存,让每个线程主要访问本地内存。
九、小结
上一章从全景视角介绍了TLB 与页表结构。
| 概念 | 要点 | 对软件的影响 |
|---|---|---|
| NUMA 拓扑 | 每个插槽有本地内存 | 远端访问延迟 1.5-2x |
| 距离矩阵 | 量化节点间延迟 | 调度决策的依据 |
| 内存分配策略 | bind/interleave/preferred | 不同场景选择不同策略 |
| 线程绑定 | CPU 亲和性 | 避免线程迁移 |
| 数据库 NUMA | Buffer Pool 分配 | 交错分配或分实例 |
| 虚拟化 NUMA | vCPU 绑定 | 虚拟机 NUMA 感知 |
NUMA 问题在双插槽系统上可能只带来 20-30% 的性能差异,但在 4 插槽和 8 插槽系统上可能达到 50-100%。随着核心数增长,NUMA 感知变得越来越重要。
下一步:硬件预取与软件预取——CPU 如何预判你要访问的数据?stream 和 stride 预取器如何工作?软件预取何时有效?
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






