mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1404 字
4 分钟
NUMA 架构
2026-02-05

在单插槽系统上,所有 CPU 核心访问内存的延迟相同——这叫 UMA(Uniform Memory Access)。但在多插槽系统上,每个 CPU 插槽有自己的本地内存,访问本地内存快,访问远端内存慢——这就是 NUMA(Non-Uniform Memory Access)。

NUMA 不是”问题”,而是多插槽系统的必然结果。理解 NUMA,才能在多插槽服务器上获得预期的性能。

一、从 UMA 到 NUMA#

1.1 为什么需要 NUMA?#

当 CPU 核心数增加时,前端总线(FSB)成为瓶颈——所有核心竞争同一条总线访问内存。NUMA 的解决方案:给每个 CPU 插槽分配本地内存和本地内存控制器,插槽之间通过互联总线(Interconnect)连接。

graph TB subgraph UMA["UMA:前端总线架构"] C1["CPU 0"] --> FSB["前端总线<br/>瓶颈!"] C2["CPU 1"] --> FSB C3["CPU 2"] --> FSB C4["CPU 3"] --> FSB FSB --> MEM["共享内存控制器"] MEM --> DRAM["DRAM"] end subgraph NUMA["NUMA:分布式内存"] N0["CPU 0<br/>内存控制器"] --- N1["CPU 1<br/>内存控制器"] N0 --- DRAM0["本地 DRAM 0"] N1 --- DRAM1["本地 DRAM 1"] N0 -.->|"跨插槽访问<br/>延迟 1.5-2x"| DRAM1 N1 -.->|"跨插槽访问<br/>延迟 1.5-2x"| DRAM0 end style UMA fill:#ffcdd2,stroke:#c62828 style NUMA fill:#e8f5e9,stroke:#2e7d32

1.2 NUMA 的延迟差异#

访问类型典型延迟相对延迟
本地 L1~1 ns1x
本地 L2~4 ns4x
本地 L3~12 ns12x
本地 DRAM~80 ns80x
远端 DRAM(1 跳)~140 ns140x
远端 DRAM(2 跳)~200 ns200x
远端 DRAM(3 跳)~260 ns260x
Note

“1 跳”表示数据经过 1 段互联总线。4 插槽系统中,最远的两个插槽之间可能需要 2-3 跳。Intel 的 UPI(Ultra Path Interconnect)和 AMD 的 Infinity Fabric 是不同的互联技术,但延迟特征类似。

二、NUMA 拓扑#

2.1 常见拓扑结构#

graph TB subgraph 双插槽["双插槽(1 跳)"] S0["Socket 0<br/>Node 0"] --- S1["Socket 1<br/>Node 1"] end subgraph 四插槽环["四插槽环形(1-2 跳)"] Q0["Node 0"] --- Q1["Node 1"] Q1 --- Q2["Node 2"] Q2 --- Q3["Node 3"] Q3 --- Q0 end subgraph 八插槽["八插槽(1-3 跳)"] E0["Node 0"] --- E1["Node 1"] E1 --- E2["Node 2"] E2 --- E3["Node 3"] E3 --- E4["Node 4"] E4 --- E5["Node 5"] E5 --- E6["Node 6"] E6 --- E7["Node 7"] E7 --- E0 end style 双插槽 fill:#e8f5e9,stroke:#2e7d32 style 四插槽环 fill:#fff9c4,stroke:#f9a825 style 八插槽 fill:#ffcdd2,stroke:#c62828

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 numa

2.3 NUMA 距离矩阵#

Node 0Node 1Node 2Node 3
Node 010213121
Node 121102131
Node 231211021
Node 321312110

距离值 10 = 本地,21 = 1 跳,31 = 2 跳。延迟与距离成正比。

三、NUMA 对性能的影响#

3.1 内存带宽#

访问类型带宽相对带宽
本地 DRAM~50 GB/s1.0x
远端 DRAM(1 跳)~35 GB/s0.7x
远端 DRAM(2 跳)~25 GB/s0.5x

3.2 典型场景的性能影响#

场景NUMA 感知NUMA 无感知性能差异
数据库(OLTP)本地内存随机分布20-40%
内存数据库本地内存跨节点30-50%
大数据处理本地分配默认分配10-30%
多线程计算线程绑定不绑定15-35%

3.3 跨 NUMA 的缓存一致性#

跨 NUMA 节点的缓存一致性消息需要经过互联总线,延迟更高:

sequenceDiagram participant Core0 as Node 0 核心 participant Ctrl0 as Node 0 控制器 participant Link as 互联总线 participant Ctrl1 as Node 1 控制器 participant Core1 as Node 1 核心 Core0->>Ctrl0: 写入共享变量(Invalidate) Ctrl0->>Link: Invalidate 消息 Link->>Ctrl1: 传递 Invalidate Ctrl1->>Core1: 执行 Invalidate Core1->>Ctrl1: Ack Ctrl1->>Link: Invalidate Ack Link->>Ctrl0: 传递 Ack Ctrl0->>Core0: 写入完成 Note over Link: 互联总线延迟 40-80 ns

四、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 --show

4.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=3306
numactl --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 感知内存分配策略详解#

不同策略对延迟和带宽的影响差异巨大,选错策略可能让延迟翻倍:

flowchart TD APP["应用类型"] --> LAT["延迟敏感型<br/>数据库、KV 存储"] APP --> BW["带宽敏感型<br/>大数据、图计算"] APP --> MIX["混合型<br/>Web 服务器"] LAT --> BIND["策略: bind<br/>线程+内存绑定同一节点<br/>最小延迟"] BW --> INTER["策略: interleave<br/>交错分配到所有节点<br/>最大聚合带宽"] MIX --> PREF["策略: preferred<br/>优先本地,不足时远端<br/>平衡延迟和带宽"] style BIND fill:#e8f5e9,stroke:#2e7d32 style INTER fill:#fff9c4,stroke:#f9a825 style PREF fill:#e3f2fd,stroke:#1565c0
策略命令延迟带宽适用场景
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 ms1.75x
随机访问 256MB~500 ms~900 ms1.8x
原子操作~50 ns/op~120 ns/op2.4x
Warning

NUMA 感知编程的一个常见错误是”过度绑定”——把所有线程和内存都绑到 Node 0,让其他节点空闲。正确的做法是均匀分配:每个节点承载 1/N 的线程和内存,让每个线程主要访问本地内存。

九、小结#

上一章从全景视角介绍了TLB 与页表结构。

概念要点对软件的影响
NUMA 拓扑每个插槽有本地内存远端访问延迟 1.5-2x
距离矩阵量化节点间延迟调度决策的依据
内存分配策略bind/interleave/preferred不同场景选择不同策略
线程绑定CPU 亲和性避免线程迁移
数据库 NUMABuffer Pool 分配交错分配或分实例
虚拟化 NUMAvCPU 绑定虚拟机 NUMA 感知
Warning

NUMA 问题在双插槽系统上可能只带来 20-30% 的性能差异,但在 4 插槽和 8 插槽系统上可能达到 50-100%。随着核心数增长,NUMA 感知变得越来越重要。


下一步硬件预取与软件预取——CPU 如何预判你要访问的数据?stream 和 stride 预取器如何工作?软件预取何时有效?

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

NUMA 架构
https://blog.souloss.com/posts/cpu-architecture/numa-architecture/
作者
Souloss
发布于
2026-02-05
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时