引言:NUMA 时代的性能陷阱
在摩尔定律放缓的时代,多核与众核架构成为性能提升的主流路径。几乎所有现代服务器都采用 NUMA(Non-Uniform Memory Access,非统一内存访问)架构,其核心特征是:每个 CPU 节点拥有本地内存,访问本地内存远快于访问远端(其他节点的)内存。然而,大多数应用程序和开发者对 NUMA 无感——Linux 内核的 NUMA 调度器与内存策略系统在幕后做出了大量自动决策,这些决策可能让性能相差 2-3 倍甚至更多。本文从 NUMA 硬件拓扑出发,深入剖析 Linux 内核的 NUMA 调度域层级、Automatic NUMA Balancing 机制、内存分配策略(First-Touch、Interleave、MPOL_BIND)、巨页与 NUMA 的交互,最后到容器场景下的 NUMA 感知实践。
一、NUMA 架构硬件拓扑
1.1 从 SMP 到 NUMA 的演进
传统 SMP(Symmetric Multiprocessing)架构中,所有 CPU 通过共享总线访问同一内存控制器,内存访问延迟一致但随着核心数增加总线成为瓶颈。NUMA 架构将处理器和内存划分为多个节点(Node),每个节点内的 CPU 通过本地内存控制器访问本地内存,节点间通过高速互连(Intel UPI、AMD Infinity Fabric)通信。Intel Xeon Scalable(Skylake 以后)使用 Mesh 或 UPI 互连,AMD EPYC 使用 Infinity Fabric,ARM 服务器(Ampere Altra/Graviton)同样支持多 socket 的 NUMA 拓扑。
1.2 NUMA 距离矩阵
ACPI SLIT(System Locality Information Table)定义了 NUMA 距离矩阵:对角线为 10(本地访问),不同节点间为 16/20/32 等(跨节点距离)。Linux 内核通过 /sys/devices/system/node/nodeX/distance 暴露该信息。距离值决定了调度器和内存分配策略的 cost 估算依据。典型双路 EPYC 系统:Node0-Distance=[10,16,22,22],Node1-Distance=[16,10,22,22]。
1.3 关键性能数据
| 架构 | 本地内存延迟 | 远端内存延迟 | 性能差异 |
|---|---|---|---|
| Intel Xeon (UPI) | ~80ns | ~120ns | 1.5x |
| AMD EPYC (IF) | ~90ns | ~150ns | 1.7x |
| 双路服务器(多跳) | ~80ns | ~200ns+ | 2.5x |
实际差异还受内存带宽竞争和互连带宽瓶颈影响。在内存带宽敏感场景(HPC、AI 训练),跨 Node 访问可能损失 40%-60% 的有效带宽。
1.4 查看 NUMA 拓扑信息
# lscpu | grep NUMA
NUMA node(s): 4
NUMA node0 CPU(s): 0-15
NUMA node1 CPU(s): 16-31
NUMA node2 CPU(s): 32-47
NUMA node3 CPU(s): 48-63
# numactl --hardware
available: 4 nodes (0-3)
node 0 cpus: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
node 0 size: 65488 MB
node 0 free: 32145 MB
node 1 size: 65536 MB
...
# numastat -m # 每个节点的内存使用统计
二、Linux 内核 NUMA 调度域层次
2.1调度域(sched_domain)层次结构
Linux CFS 调度器通过 sched_domain 层次结构组织 CPU,从底层到顶层依次为:MC(Multi-Core) → PKG(Package/Die) → NUMA Node → Full System。每个层级有自己的负载均衡策略和参数。在 AMD EPYC 等处理器上,可能还有额外的 LLC(Last-Level Cache) 域。sched_domain 通过 sched_domain_topology_level 静态数组定义,在系统启动时通过 build_sched_domains() 构建。
2.2 同一 NUMA 节点内的负载均衡
MC 层级通过s_load_balance 和 s_balance 在核间进行任务迁移,是触发最频繁的层级(tick-driven 或 idle-balance)。PKG 层级在多个 Die 之间均衡工作负载。这两层关注 CPU 缓存亲和性(任务保持在同一 LLC 域内以避免缓存失效)。
2.3 跨 NUMA 节点的负载均衡
NUMA 层级(Node 级)的负载均衡频率远低于 MC层级:
- Idle Balance:当某个 CPU 进入 idle 时,它会尝试从其他 NUMA 节点拉取工作负载。这是开销最低的均衡方式(无需中断运行中的 CPU)
- Periodic Balance:由 scheduler tick 周期性触发(系统默认每 1ms 至几百 ms不等,取决于 NUMA imbalance 阈值)。
/proc/sys/kernel/numa_balancing控制 - NUMA Hint Faulting:见下节 Automatic NUMA Balancing
关键参数:sched_numa_balancing_cost_ratio(控制 NUMA 均衡的力度)、sched/numa_balancing_scan_delay_ms(进程启动后延迟多久开始扫描)
2.4 task_numa_placement 与代价估算
当内核需要跨节点迁移任务时,通过 task_numa_placement() 计算每个节点的代价:node_load(节点当前负载)、node_capacity(节点计算能力)、task_faults_on_node(任务在该节点上的页面数量)。如果目标节点的综合代价低于当前节点超过阈值,触发任务迁移。此机制确保"任务靠近其大多数数据所在节点"。
三、Automatic NUMA Balancing(ANB)机制
3.1 工作原理
Automatic NUMA Balancing 是 Linux 内核自动 NUMA 均衡机制,通过 /proc/sys/kernel/numa_balancing 开启。核心流程:
- 扫描阶段:内核周期性扫描进程的虚拟地址空间(通过
task_numa_work工作项),将访问的页面标记。扫描速度受numa_balancing_scan_size_mb(默认 256MB/扫描周期)和numa_balancing_scan_period_min_ms(默认 1000ms)控制 - 故障统计:扫描期间内内核通过清除页面的 Present 位(或 Young 位),当页面下次被访问时触发 Page Fault,记录该 Page Fault 发生的 CPU(Node)
- 决策迁移:如果某进程在 Node A 上运行但其大部分页面位于 Node B 上(页面故障数据统计),内核会将该进程迁移到 Node B
- 页面迁移:同时将页面从 Node A 迁移到 Node B(通过
migrate_misplaced_pages()),迁移后释放原节点的物理页
3.2 内核源码关键路径
kernel/sched/fair.c:
task_tick_numa() → 周期性触发 NUMA 平衡检查
task_numa_work() → 执行 NUMA 扫描工作项
task_numa_fault() → 处理 NUMA 页面故障(Page Fault 时调用)
task_numa_placement() → 评估是否需要迁移任务
migrate_task_to() → 将任务迁移到目标 CPU
mm/migrate.c:
migrate_misplaced_page() → 处理被迁移页面的页表更新
migrate_misplaced_pages() → 批量迁移
3.3 相关结构体
struct numa_group:NUMA 协调组,将多个共享地址空间的线程归为一组进行联合均衡决策。struct numa_stats:统计每个节点的故障率。task_numa *numa:进程描述符的 NUMA 状态字段,记录了 numa_faults、numa_faults_locality、numa_pages_migrated 等。
3.4 调优参数
| 参数 | 默认值 | 说明 |
|---|---|---|
| /proc/sys/kernel/numa_balancing | 0 (HVM 环境常开) | 总开关 |
| numa_balancing_scan_delay_ms | 1000 (ms) | 新进程启动后延迟多久开始扫描 |
| numa_balancing_scan_period_min_ms | 1000 (ms) | 最小扫描周期 |
| numa_balancing_scan_period_max_ms | 60000 (ms) | 最大扫描周期 |
| numa_balancing_scan_size_mb | 256 (MB) | 每次扫描的虚拟地址范围 |
| numaBalancingPromotionRate | 10 (MB/s) | 最大页面迁移速率(避免 I/O 拥塞) |
四、NUMA 内存分配策略(mempolicy)
4.1 策略类型
Linux 通过 mempolicy 控制内存分配行为:
- MPOL_DEFAULT(默认策略):分配页面时使用 First-Touch 策略——页面首次被写入时分配给执行该写入操作的 CPU 所在的 NUMA 节点。这是最常用也是效果最不可控的策略(取决于初始化线程的运行节点)
- MPOL_BIND(绑定策略):强制从指定节点分配内存,如果指定节点内存不足则触发 OOM(而非去其他节点分配)。使用
numactl --cpunodebind=N --membind=N - MPOL_PREFERRED(优先策略):优先从指定节点分配,节点不足时回退到 Numactl 允许的其他节点。比 MPOL_BIND 更灵活
- MPOL_INTERLEAVE(交错策略):在指定节点间交替分配页面,类似 RAID 的条带化,最大化跨节点带宽利用率。使用
numactl --interleave=all
4.2 First-Touch 策略的陷阱
First-Touch 是最常见的 NUMA 性能陷阱来源:多线程程序在主线程初始化时分配大块内存(此时主线程可能在 Node 0),之后多个 Node 上的工作线程并发访问这些数据,导致所有非 Node 0 的线程都在做远程访问。解决方案:
- 使用
numactl --interleave=all ./program - 使用
numactl --cpunodebind=N --membind=N ./program - 在所有 CPU 上初始化内存(通过
first-touch-init技术:启动时在绑定每个 CPU 的情况下都写入一遍内存页面) - 使用 libnuma 库的
numa_set_interleave_mask()API - 设置
/proc/[pid]/numa_maps查看进程的 NUMA 内存分布
4.3 mbind() 与 set_mempolicy() 系统调用
// 设置进程默认策略
set_mempolicy(MPOL_INTERLEAVE, nodemask, maxnode);
// 设置特定地址范围的策略
mbind(addr, len, MPOL_PREFERRED, nodemask, maxnode, MF_MOVE);
// 查询内存分布
move_pages(pid, num_pages, pages, nodes, status, 0);
// 返回每个页面当前所在的 NUMA 节点
4.4 /proc/[pid]/numa_maps 诊断
numa_maps 是 NUMA 性能诊断的利器,暴露了每个 VMA(虚拟内存区域)的页面在各节点的分布:
7f9b0c000000 default file=/anonMapper/hugepage mapping=15200 N0=3800 N1=3800
↑VMA起始 ↑策略名 ↑文件/映射名 ↑总页数 ↑N0页数 ↑N1页数
N0=2048 kernelpagesize:2097152 N0=1
(1个2MB巨页在Node 0)
五、巨页(Hugepages)与 NUMA
5.1 巨页的 NUMA 影响
2MB/1GB 巨页大幅减少 TLB Miss(每次 TLB Miss 开销可能高达数百个周期),但巨页的 NUMA 错误代价更高:一个 2MB 巨页跨节点意味着 2MB 数据全部远程访问。因此大型数据库(PostgreSQL、Oracle、SAP HANA)和 Java 堆在使用巨页时对 NUMA 配置非常敏感。
5.2 巨页 NUMA 参数
/proc/sys/vm/nr_hugepages # 系统级巨页总数
/proc/sys/vm/nr_hugepages_mempolicy # NUMA 巨页池(每个节点动态分配)
/proc/sys/vm/nr_overcommit_hugepages # 可超额分配的巨页
# 查询每个节点的巨页数量
cat /sys/devices/system/node/node0/hugepages/hugepages-2048kB/nr_hugepages
cat /sys/devices/system/node/node1/hugepages/hugepages-2048kB/free_hugepages
# 设置节点级巨页池
echo 1024 > /sys/devices/system/node/node0/hugepages/hugepages-2048kB/nr_hugepages
5.3 libhugetlbfs 与透明巨页(THP)
透明巨页(Transparent Hugepages,THP)通过 khugepaged 后台线程运行时将普通 4KB 页面合并为 2MB 巨页。THP 的 NUMA 行为:默认 khugepaged 尽可能在本地节点分配巨页,在 NUMA 节点内存不均衡时可能导致本地节点碎片化。建议配置:/sys/kernel/mm/transparent_hugepage/enabled = madvise(仅对显式请求的 VMA 启用 THP,减少全局碎片化影响)。对于数据库等明确需要巨页的应用,使用 hugetlbfs 静态巨页池(启动时预留,避免碎片化)而非 THP。
六、容器与 NUMA 感知
6.1 cgroup v2 与 NUMA 感知
cgroup v2 的 memory 和 cpuset 控制器与 NUMA 紧密配合:cpuset.cpus 和 cpuset.mems 分别限制容器可使用的 CPU 节点和内存节点。Kubernetes 的 Topology Manager 通过协调 Pod 的 CPU、内存、设备(如GPU、NIC)在 NUMA 节点上的对齐关系,确保跨 NUMA 访问最小化。Topology Manager 有三种策略:
- none:不实施 NUMA 拓扑约束
- best-effort:尽可能但不强制 NUMA 对齐
- restricted:强制 NUMA 对齐,无法对齐则拒绝 Pod
- single-numa-node:严格要求所有资源在同一 NUMA 节点(延迟最低)
6.2 Kubelet 配置示例
# /var/lib/kubelet/config.yaml
cpuManagerPolicy: static
topologyManagerPolicy: single-numa-node # 或 restricted
reservedSystemCPUs: "0-3" # 系统保留核
# Pod 请求 (Guaranteed QoS)
resources:
requests:
cpu: "8"
memory: "16Gi"
hugepages-2Mi: "512Mi"
limits:
cpu: "8"
memory: "16Gi"
hugepages-2Mi: "512Mi"
6.3 libvirt/KVM 虚拟机的 NUMA 配置
虚拟化场景下,vCPU 和内存同样需要遵循 NUMA 拓扑:
<domain>
<cputune>
<vcpupin vcpu='0' cpuset='0'/>
<vcpupin vcpu='1' cpuset='1'/>
<emulatorpin cpuset='0-1'/>
</cputune>
<numatune>
<memory mode='strict' nodeset='0'/>
</numatune>
<cpu>
<numa>
<cell id='0' cpus='0-1' memory='8' unit='GiB'/>
<cell id='1' cpus='2-3' memory='8' unit='GiB'/>
</numa>
</cpu>
</domain>
七、性能诊断与分析工具
7.1 numastat
numastat 是 NUMA 内存使用的直接工具:
$ numastat -m # 系统级内存统计
Node 0 Node 1
MemTotal 65488.00 65536.00
MemFree 32145.00 52341.00
Numa_Hit 892342101 721348291 # 本地访问次数
Numa_Miss 8234001 12234001 # 远端访问页面数
Numa_Foreign 5612001 2345001 # 来自其他节点的远端访问
$ numastat -p $(pgrep java) # 进程级NUMA统计
健康指标:Numa_Hit/(Numa_Hit + Numa_Miss) > 90% 为良好,< 70% 需要优化。
7.2 perf c2c(Cache-to-Cache)
现代 Intel CPU 支持 PEBS(Precise Event-Based Sampling)的 mem_load_retired.l3_miss 事件,可以精确追踪跨 NUMA 的缓存行迁移:
perf c2c record -a -- sleep 30
perf c2c report -c pid,tid,iaddr
# 输出显示跨 Node 的缓存行命中/未命中统计
7.3 Intel PT/Xeon Uncore PMU
Intel Xeon 的 Uncore(片上系统总线控制器)PMU 提供直接的跨 Node 带宽统计:
# imc (Integrated Memory Controller) 带宽
perf stat -e uncore_imc_0/cas_count_read/,uncore_imc_0/cas_count_write/ sleep 5
# UPI (Ultra Path Interconnect) 带宽
perf stat -e upi_0/rx_flits/,upi_0/tx_flits/ sleep 5
7.4 BPF/eBPF NUMA 追踪
使用 eBPF 可以自定义 NUMA 级 PMU 追踪脚本(通过 bpftrace):
#!/usr/bin/bpftrace
// 追踪 NUMA 任务迁移事件
tracepoint:sched:sched_migrate_task {
printf("task %s migrated from cpu %d (node %d) to cpu %d (node %d)\n",
args->comm, args->orig_cpu,
cpu_to_node(args->orig_cpu), args->dest_cpu,
cpu_to_node(args->dest_cpu));
}
// 追踪页面迁移(NUMA Balancing)
kprobe:migrate_misplaced_page {
printf("page migrated for pid %d to node %d, size %d KB\n",
pid, arg2, arg3 / 1024);
}
八、生产级 NUMA 优化案例
8.1 案例一:Redis NUMA 优化
Redis 在多 NUMA 节点服务器上的典型优化——默认 Redis 将所有内存分配在 Node 0(First-Touch 陷阱),而多 worker 线程分布在多个 Node。优化方案:
# 使用 interleave 策略
numactl --interleave=all redis-server /etc/redis.conf
# 或者显式绑定(if Redis 主要工作线程在同一节点)
numactl --cpunodebind=0 --membind=0 redis-server /etc/redis.conf
# 关闭 NUMA balancing (避免 Redis 频繁触发 Page Fault)
echo 0 > /proc/sys/kernel/numa_balancing
# 关闭透明巨页(避免内存碎片和 khugepaged 开销)
echo never > /sys/kernel/mm/transparent_hugepage/enabled
8.2 案例二:NUMA 敏感的数据库 OLTP
PostgreSQL 在多 NUMA 系统中:
- 使用
numa_tables/将表空间分布到不同节点(每个节点一个表空间) - 开启
huge_pages = on+ nr_hugepages_per_node - 配置
effective_io_concurrency(NUMA 间存储带宽控制) - 使用
numactl --cpunodebind将 PG 绑定到拥有本地存储控制器的节点 - 监控指标:
numastat的other_node计数
8.3 案例三:AI 训练(PyTorch/Distributed Training)
分布式训练场景下,NUMA 优化围绕 GPU 本地性展开:
# 查询 GPU-CPU 本地性
nvidia-smi topo -m
# 将训练进程绑定到 GPU 本地 CPU
numactl --cpunodebind=$(nvidia-smi -q -x | grep -o 'numa="[^"]*"' | ...) python train.py
# NCCL 配置(跨节点多 GPU 训练的 NUMA 敏感参数)
export NCCL_SOCKET_IFNAME=eth0
export NCCL_TOPO_FILE=/etc/nccl/topology.xml
export NCCL_P2P_LEVEL=NVL # 优先使用 NVLink(Node 间)
九、总结
NUMA 是理解现代多核服务器性能的关键维度。从内核调度域的层次结构到 Automatic NUMA Balancing 的自动均衡,从 First-Touch 策略的陷阱到交错分配的弹性,Linux 内核提供了完整但隐式的 NUMA 管理。在生产系统中,推荐做法是:
- 诊断先行:使用
numastat、perf c2c、/proc/[pid]/numa_maps建立 NUMA 性能基线 - 策略控制:对 NUMA 敏感的系统服务(数据库、缓存、AI 训练)使用
numactl显式控制策略 - 容器对齐:配置 Kubernetes Topology Manager + cpuManagerPolicy=static 保证 NUMA 对齐
- 虚拟化拓扑:libvirt/QEMU 配置中明确 NUMA cell + vCPU 绑核
- 巨页管理:明确使用静态 hugetlbfs 而非 THP 应对数据库场景的 NUMA 需求
NUMA 本身不是问题,对 NUMA 的无知才是。掌握这些底层机制,才能在 NUMA 时代写出真正高性能的系统和应用。

发表评论 取消回复