引言: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~120ns1.5x
AMD EPYC (IF)~90ns~150ns1.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 开启。核心流程:

  1. 扫描阶段:内核周期性扫描进程的虚拟地址空间(通过 task_numa_work 工作项),将访问的页面标记。扫描速度受 numa_balancing_scan_size_mb(默认 256MB/扫描周期)和 numa_balancing_scan_period_min_ms(默认 1000ms)控制
  2. 故障统计:扫描期间内内核通过清除页面的 Present 位(或 Young 位),当页面下次被访问时触发 Page Fault,记录该 Page Fault 发生的 CPU(Node)
  3. 决策迁移:如果某进程在 Node A 上运行但其大部分页面位于 Node B 上(页面故障数据统计),内核会将该进程迁移到 Node B
  4. 页面迁移:同时将页面从 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_balancing0 (HVM 环境常开)总开关
numa_balancing_scan_delay_ms1000 (ms)新进程启动后延迟多久开始扫描
numa_balancing_scan_period_min_ms1000 (ms)最小扫描周期
numa_balancing_scan_period_max_ms60000 (ms)最大扫描周期
numa_balancing_scan_size_mb256 (MB)每次扫描的虚拟地址范围
numaBalancingPromotionRate10 (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 时代写出真正高性能的系统和应用。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部