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 优化重点:

  1. GPU 所在 PCIe 总线对应的 CPU 节点决定了基础内存分配
  2. CUDA_VISIBLE_DEVICES=7 与 numactl --membind=0 需要配合使用
  3. 使用 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)

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部
0.361459s