Linux 内核 NUMA 架构深度工程实战:从硬件拓扑到性能优化全景
一、为什么 NUMA 是现代系统的性能命门
在单一 CPU 核心的岁月里,所有内存访问的代价是均匀的——这正是 SMP(Symmetric Multi-Processing)对称多处理架构的理论前提。然而随着核心数突破两位数,"共享总线"成为瓶颈。现代多路服务器中,跨 CPU 访问远端内存(Remote NUMA Access)的延迟可能达到本地访问的 2-3 倍,带宽则可能缩水到 1/3。对于延迟敏感的数据库、AI 训练和高性能网络应用而言,这种"NUMA 惩罚"足以吞噬硬件升级的全部收益。
本文从硬件拓扑出发,深入 Linux 内核 NUMA 子系统实现,结合工业级调优案例,构建完整的 NUMA 优化知识体系。
二、NUMA 硬件拓扑与演进
2.1 从 SMP 到 NUMA 的架构迁移
SMP (UMA) NUMA
┌─────────────┐ ┌──────┐ ┌──────┐
│ CPU0 CPU1 │ │ CPU0 │ │ CPU1 │
│ CPU2 CPU3 │ │ CPU2 │ │ CPU3 │
│ ↕ │ └──┬───┘ └──┬───┘
│ ┌────────┐ │ ┌──┴──┐ ┌──┴──┐
│ │ DRAM │ │ │MEM0 │ │MEM1 │
│ └────────┘ │ └─────┘ └─────┘
└─────────────┘ HyperTransport / UPI
传统 UMA 架构下所有 CPU 通过共享总线访问同一内存池,核心数超过 8 个后总线争用急剧恶化。NUMA(Non-Uniform Memory Access)将本地 DRAM 直接集成到每个 CPU 封装内,通过高速互连链路(AMD 的 Infinity Fabric、Intel 的超路径互连 UPI)提供远端访问通道。
2.2 现代多路服务器的 NUMA 拓扑
以 AMD EPYC 9654 或 Intel Xeon Platinum 系列为例:
# 查看 NUMA 拓扑
$ numactl --hardware
available: 4 nodes (0-3)
node 0 cpus: 0 1 2 ... 31
node 0 size: 263741 MB
node 1 cpus: 32 33 ... 63
node 1 size: 263741 MB
...
node distances:
node 0 1 2 3
0: 10 20 20 20
1: 20 10 20 20
2: 20 20 10 20
3: 20 20 20 10
距离值 10 代表本地访问基准,20 意味着跨节点访问的相对延迟翻倍。注意某些双路系统中,本地内存控制器访问对应的内存节点上可能标记为 11(略高于纯本地 10),反映同一封装内多个芯片(CCD)的微小幅外延迟差异。
三、Linux 内核 NUMA 子系统实现
3.1 pg_data_t:NUMA 节点的内核抽象
每个 NUMA 节点在内核中由 pg_data_t 结构体描述,这是内存管理的顶层控制结构:
// include/linux/mmzone.h
typedef struct pglist_data {
struct zone node_zones[MAX_NR_ZONES]; // 节点内的内存区域
struct zonelist node_zonelists[MAX_ZONELISTS]; // 备选分配区域列表
int nr_zones; // 节点拥有的 zone 数量
struct page *mem_map; // 物理页面映射表
unsigned long node_start_pfn; // 起始页帧号
unsigned long node_present_pages; // 总物理页面数
unsigned long node_spanned_pages; // 跨度页面数(含空洞)
int node_id; // 节点 ID
/* NUMA Balancing 相关 */
struct task_struct *numa_next_scan; // 下次扫描的任务
unsigned int numa_scan_period; // 扫描周期 (ms)
unsigned int numa_scan_period_max; // 最大扫描周期
...
} pg_data_t;
每个节点的 node_zones 数组包含 ZONE_DMA、ZONE_DMA32、ZONE_NORMAL、ZONE_MOVABLE 等多个区域,而 node_zonelists 定义了内存分配失败时的跨区域回退策略——优先回退同一节点的其他 zone,再尝试远端节点。
3.2 zone_reclaim_mode:内存回收策略控制
/proc/sys/vm/zone_reclaim_mode 控制 NUMA 节点的内存回收行为:
| bit | 含义 |
|---|---|
| 0 | 默认:不回退到远端节点 |
| 1 | 启用 zone reclaim:本地不足时优先回收本地 page cache |
| 2 | 本地内存不够时执行本地 swap(仅节点内回收) |
| 4 | 回收时扫描脏页 |
对于数据库和实时应用,zone_reclaim_mode=1 可以强制本地回收,避免因远端分配导致的性能抖动;但对于内存使用不均匀的通用场景,设为 0 更稳妥。
四、NUMA 内存分配策略深度剖析
4.1 First-Touch 策略
Linux 默认的 NUMA 分配策略是 First-Touch:进程在哪个 CPU 上首次触发页面缺页(page fault),该页面就分配到该 CPU 对应的本地 NUMA 节点。这意味着线程运行的位置直接决定了其内存的物理位置。
// mm/mempolicy.c 简化逻辑
page = alloc_pages_node(nid, gfp_mask, order);
// nid 由当前 CPU 所在的 NUMA 节点决定
第一接触陷阱:如果一个进程在 CPU 0 上启动并分配大量内存,之后通过 taskset 绑定到 CPU 32(节点 1),那么该进程的所有内存都在节点 0,即使 CPU 改变了,内存不会随之迁移。这就是为什么NUMA 优化中,"分配时 CPU 位置"比"运行时 CPU 位置"更重要。
4.2 Auto NUMA Balancing(默认启用)
Linux 4.1+ 引入了 Auto NUMA Balancing 机制,通过周期性扫描进程地址空间来识别跨节点访问热点,主动将页面迁移到本地节点。
工作机制:
1. 内核每隔 scan_period 毫秒扫描一个进程的部分地址空间
2. 通过清除 PTE 的 Present 位技巧,在下次访问时捕获 NUMA Hint Fault
3. 记录 fault 发生的 CPU(即访问者位置)
4. 如果一个页面被远程访问频率超过阈值,触发页面迁移至远程 CPU 所在的节点
# Auto NUMA Balancing 开关
$ cat /proc/sys/kernel/numa_balancing
1 # 默认启用
# 关键参数
$ cat /sys/kernel/debug/sched/numa_balancing
scan_period_min_ms: 100 # 最小扫描周期
scan_period_max_ms: 60000 # 最大扫描周期(频繁故障时递减)
scan_size_mb: 256 # 每次扫描的地址范围(MB)
实战建议:对于 NUMA 感知良好的应用(如DPDK、NVLink-GPUDirect),禁用 Auto NUMA Balancing(sysctl kernel.numa_balancing=0)避免无谓的页面迁移开销;对于数据库和通用应用,保持启用。
4.3 Memory Policy 与 NUMA API
libnuma 提供了五种 NUMA 内存分配策略:
| 策略 | 行为 |
|---|---|
| MPOL_DEFAULT | 使用节点默认的 First-Touch 策略 |
| MPOL_BIND | 必须在指定节点列表中分配,否则触发 OOM |
| MPOL_PREFERRED | 优先在指定节点分配,回退到其他节点 |
| MPOL_INTERLEAVED | 轮询(round-robin)在所有指定节点间交错分配 |
| MPOL_LOCAL | 在当前 CPU 所在节点分配 |
#include <numaif.h>
// 绑定到节点 0 和 1 交错分配
nodemask_t mask = {0x03}; // node 0 + 1
mbind(vptr, length, MPOL_INTERLEAVED, mask, 64, 0);
// 检查页面实际位置
int status[NR_PAGES];
move_pages(pid, NR_PAGES, pages, NULL, status, 0);
// status[i] 表示第 i 个页面所在的 NUMA 节点
五、NUMA 性能诊断工具链
5.1 numastat:全局 NUMA 访问统计
$ numastat -c mysql
Node 0 Node 1 Node 2 Node 3
------ ------ ------ -----
numa_hit 1258394 1093827 1194827 1082739
numa_miss 93847 102847 88274 103284
numa_foreign 28347 38294 29284 38192
interleave_hit 12734 12834 12842 12743
local_node 1193847 993284 1093847 992837
other_node 64547 100543 100980 89902
numa_miss 表示 First-Touch 期望的节点已满回退到远端分配;numa_foreign 表示页面被重新分配到非期望节点,两者比值超过 5% 就需要关注。
5.2 numactl:进程级 NUMA 绑定
# 指定进程要在 node 0,1 上运行,内存也来自 node 0,1
$ numactl --cpunodebind=0,1 --membind=0,1 ./mysql
# 交错分配在所有 AI 训练节点
$ numactl --interleave=all python train.py
# 查看当前进程的 NUMA 内存绑定
$ numastat -p $$
Per-node process memory (in MBs):
Node 0 Node 1 Node 2 Node 3
Total 4096 1024 512 256
5.3 perf c2c:缓存行级别的 NUMA 访问分析
# 记录缓存到缓存的访问事件
$ perf c2c record -a -- -p $PID sleep 10
$ perf c2c report --stdio # 输出 cross-socket 缓存命中率
这一工具可以精确识别哪些缓存行产生跨 socket 的 Remote HITM(Hit Modified)事件,是定位 NUMA 热点级别的利器。
5.4 内核 tracepoint 追踪
# 追踪 NUMA 页面迁移
$ echo 1 > /sys/kernel/debug/tracing/events/numa/enable
# Auto NUMA Balancing 扫描事件
$ trace-cmd record -e numa:*
六、工业级 NUMA 优化实践
6.1 数据库场景(MySQL/PostgreSQL)
MySQL InnoDB 的专用内存池(innodb_buffer_pool)若跨 NUMA 节点分配,在高并发 OLTP 场景下吞吐量可能下降 30% 以上。
# systemd service override
[Service]
ExecStart=
ExecStart=/usr/bin/numactl \
--cpunodebind=0,1 \
--membind=0,1 \
/usr/sbin/mysqld --daemonize --pid-file=/run/mysqld/mysqld.pid
# InnoDB Buffer Pool 也会跟随内存分配策略
# innodb_numa_interleave=ON 可以强制交错分配
PostgreSQL 同样受益:通过 numactl --interleave=all 启动进程可以均匀分配共享缓冲区,避免单一节点的访问热点。
6.2 DPDK / 高性能网络
DPDK 应用必须显式指定 NUMA 节点:
# DPDK EAL 参数中显式指定内存巨页在节点 0
dpdk-proc --socket-mem=4096,0 --huge-dir=/dev/hugepages/node0
# 网卡绑定到特定 NUMA 节点的 PCI 总线
# 使用 lspci 确认网卡位于哪个 NUMA 节点
lspci | grep Mellanox
lspci -vvv -s 04:00.0 | numa_node
# → numa_node: 0
关键原则:网卡、内存、处理线程三者必须在同一 NUMA 节点,这是达到线速(line rate)处理的前提。
6.3 AI 训练与 GPU Direct
NVIDIA GPU Direct RDMA 将网卡和 GPU 直接连接,绕过 CPU 和内存复制。此时 NUMA 优化重点:
- GPU 所在 PCIe 总线对应的 CPU 节点决定了基础内存分配
CUDA_VISIBLE_DEVICES=7与numactl --membind=0需要配合使用- 使用
nvidia-smi topo -m查看 GPU-CPU-NIC 拓扑矩阵
$ nvidia-smi topo -m
GPU0 CPU Affinity NUMA Affinity
GPU0 X 0-31 0
CPU N/A X N/A 0
# PyTorch 训练脚本中显式绑定
import torch
import os
os.environ["CUDA_VISIBLE_DEVICES"] = "0"
os.environ["NUMA_MEM_BIND"] = "0"
6.4 Kubernetes NUMA-Aware 调度
Kubelet 的 Topology Manager 通过 --topology-manager-policy 控制:
kubeletConfig:
topologyManagerPolicy: best-effort # best-effort | restricted | single-numa-node
cpuManagerPolicy: static
single-numa-node 策略保证 Pod 的所有 CPU 和内存设备在同一 NUMA 节点内,是延迟敏感型工作负载的首选。Operator 层面通过 NodeResourceTopology CRD 暴露硬件拓扑,让调度器做出 NUMA-Aware 决策:
# NodeResourceTopology CRD 示例
apiVersion: topology.node.k8s.io/v1alpha2
kind: NodeResourceTopology
metadata:
name: worker-node-0
policies:
- Name: SingleNUMANodePodLevel
Zones:
- Name: node-0
Type: Node
Parent: ""
- Name: NIC-0
Type: Device
Parent: node-0
- Name: GPU-0
Type: Device
Parent: node-0
七、未来:CXL 与 NUMA 演进
Compute Express Link(CXL)正在重新定义 NUMA 的边界。CXL 3.0 支持内存池化和多节点级联,使得"NUMA 节点"不再与物理 CPU 封装一一对应:
- CXL 内存:可作为独立 NUMA 节点暴露给操作系统,可能具有更高的访问延迟(典型值 200-400ns vs DRAM 80ns)
- 内存分层:DRAM(node 0)→ CXL.mem(node 4)→ NVMe SSD(node 5),形成三级 NUMA 结构
- Linux 内核适配:5.15+ 版本已支持将 CXL 设备作为 NUMA 节点注册,zone_reclaim_mode 机制可扩展至新层级
对于 CXL 场景,Auto NUMA Balancing 的扫描策略可能需调整:对 Hot Data 留在 DRAM、Cold Data 下沉至 CXL.mem 的分层策略,比盲目本地迁移更加务实。
八、NUMA 优化最佳实践速查表
┌────────────────────────────────────────────────────────────────────┐
│ NUMA 优化工程速查表 │
├───────────────┬──────────────────────────────────────────────────┤
│ 场景 │ 推荐配置 │
├───────────────┼──────────────────────────────────────────────────┤
│ MySQL/OltP │ numactl --cpunodebind --membind + innodb_numa=1 │
│ Redis/缓存 │ numactl + activedefrag + THP=madvise │
│ DPDK/网络 │ 网卡+CPU+内存同节点 + kernel.numa_balancing=0 │
│ AI训练 │ CUDA_VISIBLE_DEVICES + 显式 node bind │
│ K8s Pod │ TopologyManager=single-numa-node │
│ CXL 内存 │ 分层策略 + AutoNUMA 调低扫描频率 │
├───────────────┴──────────────────────────────────────────────────┤
│ 诊断命令栈: │
│ numactl -H → numastat -c <app> → perf c2c record → trace-numa │
│ 关键阈值: │
│ miss率 >5% 需介入; remote_hitm/s > 1e6 需perf c2c定位 │
└────────────────────────────────────────────────────────────────────┘
结语
NUMA 并非硬件设计的补丁,而是现代多核系统性能优化的基石。从 First-Touch 策略的隐性影响,到 Auto NUMA Balancing 的主动干预,Linux 内核提供了完整的工具链。然而,工具只是手段——真正解决 NUMA 问题需要理解应用的内存访问模式、线程调度拓扑和硬件布局。当 CXL 将 NUMA 从物理封装扩展到内存池时,这套知识非但不会过时,反而成为驾驭下一代硬件的关键。
nuactl 安装:apt-get install numactl numad;CXL 工具包:cxl-cli (>= v79)

发表评论 取消回复