这是多插槽服务器(如双路 Xeon、AMD EPYC、ARM 服务器)上最容易被忽视的性能杀手。你在单颗桌面 CPU 上跑得飞快的程序,搬到多插槽服务器上可能性能直接腰斩——十有八九是 NUMA 在作祟。
1. 什么是 NUMA?(物理拓扑视角)
在传统的 UMA(Uniform Memory Access,统一内存访问) 架构中,所有 CPU 核心通过同一条总线(或同一个内存控制器)访问所有物理内存,延迟一致。
在 NUMA 架构中,每个 CPU 插槽(Socket)拥有自己独立的内存控制器和本地内存。CPU 插槽之间通过高速互联(如 Intel UPI、AMD Infinity Fabric)连接。
+-----------------------------------------------------------+
| NUMA 双插槽服务器 |
+-----------------------------------------------------------+
| Socket 0 (CPU 0) | Socket 1 (CPU 1) |
| +-------------------+ | +-------------------+ |
| | 核心 0-7 | | | 核心 8-15 | |
| | L1/L2/L3 缓存 | | | L1/L2/L3 缓存 | |
| +-------------------+ | +-------------------+ |
| | 内存控制器 (MC0) | | | 内存控制器 (MC1) | |
| +--------+----------+ | +--------+----------+ |
| | | | |
| +--------v----------+ | +--------v----------+ |
| | 本地内存 (Node 0) | | | 本地内存 (Node 1) | |
| | 延迟: ~80ns | | | 延迟: ~80ns | |
| +-------------------+ | +-------------------+ |
| | | | |
| +----[UPI 互联]---+-----------+ |
| 跨插槽访问延迟: ~160ns (2倍!) |
+-----------------------------------------------------------+
核心事实:
- 访问本地内存(同插槽):~80ns(与普通 UMA 系统一致)。
- 访问远端内存(另一插槽):~160ns(2 倍延迟!)。
- 带宽:本地内存 ~100GB/s,远端内存可能降到 ~40GB/s(取决于互联带宽)。
2. NUMA 如何影响并发程序?(三个典型场景)
场景 1:MPSC 队列跨越 NUMA 节点
[生产者线程] → (MPSC 队列) → [消费者线程]
运行在 Socket 0 运行在 Socket 1
(数据在 Node 0 内存) (跨节点读取 Node 0 内存)
│
▼
每次 dequeue 都访问远端内存
延迟 ~160ns (vs 80ns)
吞吐量下降 30%~50%
场景 2:多线程共享数据结构
[线程A] 在 Socket 0 修改共享变量
│
▼
[缓存行状态变为 M (Modified)] 在 Socket 0 的 L3 缓存中
│
▼
[线程B] 在 Socket 1 读取同一变量
│
▼
UPI 互联传递缓存行 (~100ns 额外延迟)
Socket 1 的 TLB/缓存行失效
场景 3:内存分配器(jemalloc)的 arena 分配
jemalloc默认会把内存分配到调用线程所在的 NUMA 节点。- 但如果消费者线程在另一个节点上消费,就会变成跨节点访问。
3. 如何诊断 NUMA 问题?
命令 1:查看系统的 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: 28000 MB
node 1 cpus: 8 9 10 11 12 13 14 15
node 1 size: 32768 MB
node 1 free: 27500 MB
node distances:
node 0 1
0: 10 20 ← 10 = 本地延迟,20 = 远端延迟(2倍)
1: 20 10
命令 2:查看进程的 NUMA 内存分布
### 查看进程 (PID=1234) 在各个 NUMA 节点上的内存分配
numastat -p 1234
### 输出示例:
Per-node process memory usage for PID 1234 (your_program)
Node 0: 256 MB
Node 1: 10 MB ← 大部分内存在 Node 0,但程序可能在两个节点都有线程!
命令 3:perf 检测跨节点访问
### 统计远端内存访问次数
perf stat -e node-loads,node-load-misses ./your_program
4. NUMA 优化策略(四层解决方案)
策略 1:绑核(CPU Affinity)—— 最常用
把线程绑定到特定 CPU 核心,并分配该核心所在节点的内存。
#include <pthread.h>
#include <numa.h>
// 绑定线程到 NUMA 节点 0 的 CPU 核心
void bind_to_node(int node_id) {
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
// 把该节点上的所有核心加入集合
struct bitmask* mask = numa_allocate_cpumask();
numa_node_to_cpus(node_id, mask);
for (int i = 0; i < mask->size; ++i) {
if (numa_bitmask_isbitset(mask, i)) {
CPU_SET(i, &cpuset);
}
}
pthread_setaffinity_np(pthread_self(), sizeof(cpu_set_t), &cpuset);
numa_free_cpumask(mask);
}
使用方式:
void producer_thread() {
bind_to_node(0); // 生产者在 Node 0 运行,内存在 Node 0 分配
// ...
}
void consumer_thread() {
bind_to_node(0); // 消费者也绑到 Node 0,避免跨节点
// ...
}
策略 2:内存绑定(Memory Policy)—— 让内存在指定节点分配
#include <numa.h>
void* allocate_on_node(size_t size, int node_id) {
// 强制在指定节点分配内存
return numa_alloc_onnode(size, node_id);
}
void free_on_node(void* ptr, size_t size) {
numa_free(ptr, size);
}
与 SPSC 队列配合:把整个环形缓冲区分配在消费者所在的 NUMA 节点上。
// 在 Node 0 分配 SPSC 队列的缓冲区
auto* buffer = static_cast<Frame*>(allocate_on_node(
sizeof(Frame) * QUEUE_SIZE, 0));
策略 3:使用 libnuma 的 API 控制内存分配策略
#include <numa.h>
// 绑定当前线程的内存分配到本地节点(默认)
numa_set_localalloc();
// 绑定到特定节点
numa_set_preferred(0); // 优先在 Node 0 分配
// 内存交错分配(Round-Robin,均衡但牺牲局部性)
numa_set_interleave_mask(numa_all_nodes_ptr);
策略 4:MPSC 队列的 NUMA 感知设计
在 NUMA 系统中,理想设计是生产者和消费者在同一节点。
[Node 0] [Node 1]
+---------------------------+ +---------------------------+
| 生产者线程 (Core 0-3) | | 生产者线程 (Core 8-11) |
| 消费者线程 (Core 4-7) | | 消费者线程 (Core 12-15) |
| 队列内存 (分配在 Node 0) | | 队列内存 (分配在 Node 1) |
+---------------------------+ +---------------------------+
UPF 互联
↑
仅在需要时跨节点通信
实现:为每个 NUMA 节点创建独立的 MPSC 队列,线程只操作本地队列,全局协调只在必要时发生。
5. 硬件层面的 NUMA 优化:QM(Quality of Service)与缓存分配
Intel 和 AMD 提供了缓存分配技术(Intel RDT / AMD QoS),可以给特定的核心分配更多的 L3 缓存,减少远端内存访问。
### Intel RDT:为特定核心预留 L3 缓存
### 需要内核支持 CONFIG_RESCTRL
6. 笔记存档:NUMA 核心概念速查
| 概念 | 解释 |
|---|---|
| NUMA | 多 CPU 插槽架构,每个插槽有独立内存控制器,访问本地内存快,远端内存慢。 |
| 本地内存 | 同一插槽上的内存,延迟 ~80ns。 |
| 远端内存 | 另一插槽上的内存,延迟 ~160ns(2倍)。 |
| 互联总线 | Intel UPI / AMD Infinity Fabric,连接多插槽,带宽 ~40GB/s。 |
| 绑核 | 用 pthread_setaffinity_np 把线程绑定到特定核心。 |
| 内存绑定 | 用 numa_alloc_onnode 在指定节点分配内存。 |
| 适用场景 | 多插槽服务器(双路/四路 Xeon、EPYC、鲲鹏)。 |
| 不适用场景 | 单路桌面 CPU(所有内存访问延迟一致)。 |
本节关键词:NUMA、本地内存、远端内存、绑定、numa_alloc_onnode、UPI、Infinity Fabric。 与前期知识的关联:
- SPSC/MPSC 队列 → 在 NUMA 上,队列内存应分配在消费者所在节点。
jemalloc→ 默认优先分配在调用线程所在节点(可用MALLOC_CONF=retain:true调整)。mmap→ 可用mbind系统调用控制映射的物理页在哪个 NUMA 节点。