﻿# NUMA 架构详解：从原理到 Linux 实战调优

> 本文由 [tools/numa/paper.md](/concepts/numa/paper.md) 重新梳理而成——规范化 Markdown 结构、补齐 ASCII 示意图（原文微信配图有防盗链无法内嵌）、统一代码块与表格。

> 原作者：Linux 教程 ｜ 原文链接：<https://mp.weixin.qq.com/s/eugWx_QOTdN8_DJ4FvmtvA>


> 简明版概念/工具速查见同目录 [numa.md](/concepts/numa/numa.md) / [numactl.md](/tools/numa/numactl.md) / [numastat.md](/tools/numa/numastat.md)；缓存一致性(MESI)见 [../cache/mesi.md](/concepts/cache/mesi.md)。本文是系统性的长篇整理。

NUMA（Non-Uniform Memory Access，非一致性内存访问）是现代多处理器系统优化内存访问性能的关键技术。系统被划分为多个**节点（Node）**，每个节点含一个或多个 CPU 核心 + 与之紧耦合的**本地内存**。CPU 访问本地内存的延迟远低于访问其他节点内存（远程内存）——这种访问时间的不一致性，正是 NUMA 的核心特征。

Linux 内核识别 NUMA 时，解析 ACPI 规范里的 **SRAT**（System Resource Affinity Table）和 **SLIT**（System Locality Information Table）获取硬件拓扑。本地内存由节点内的内存控制器直接管理；跨节点访问需经高速互联总线（Intel **QPI/UPI**、AMD **Infinity Fabric**）传输，引入额外延迟和带宽开销。

---

## 一、NUMA 与 UMA、SMP 架构对比

要理解 NUMA 的优势，先对比传统 UMA/SMP。UMA 是 SMP 的典型内存模式：所有 CPU 共享一个内存池和单一系统总线，任何 CPU 访问内存延迟都一致。早期够用，但 CPU 一多，**总线就成了瓶颈**，访问冲突加剧。

NUMA 打破"统一访问"，采用分布式内存布局，每个节点有独立内存，从根源解决 SMP 的扩展瓶颈。32 核以上的大型服务器上，SMP 会因总线争用导致延迟飙升，NUMA 则让节点内 CPU 访问本地内存不受其他节点影响。代价是引入新挑战：**如何优化跨节点访问、避免高延迟**。

| 缩写 | 全称 | 含义 |
|------|------|------|
| SMP | Symmetric MultiProcessing / Shared-Memory MultiProcessors | 对称多处理机 / 共享存储型多处理机 |
| UMA | Uniform-Memory-Access | 均匀存储器存取（SMP 的典型内存模式）|
| NUMA | Non-Uniform-Memory-Access | 非均匀存储器存取 |

```bash
   UMA / SMP（统一访问，总线易成瓶颈）          NUMA（分布式内存，就近访问）
 ┌──────┬──────┬──────┬──────┐             ┌───── Node0 ─────┐   ┌───── Node1 ─────┐
 │CPU0  │CPU1  │CPU2  │CPU3  │             │ CPU0..3         │   │ CPU4..7         │
 └───┬──┴───┬──┴───┬──┴───┬──┘             │   ↕ 本地(快)    │◄─►│   ↕ 本地(快)    │
     └──────┴──┬───┴──────┘                │ Memory0(直连)   │QPI│ Memory1(直连)   │
          单一系统总线 ← 争用瓶颈            └─────────────────┘UPI└─────────────────┘
        ┌──────┴──────┐                       CPU0 访问 Memory0 快；访问 Memory1 需过互联(慢)
        │  共享内存池  │
        └─────────────┘
```

---

## 二、NUMA 系统架构详解

### 2.1 定义与关键特性

NUMA 将系统划分为多个相对独立的节点，每个节点是一个**紧密耦合的计算单元**（一组 CPU 核心 + 本地内存 + 内存控制器）。核心目标：最大化利用本地内存的高效性，减少共享总线的冲突和延迟。

两个核心特点：

1. **本地内存优先访问**：CPU 就近读写本地内存，低延迟、高带宽，大幅提升多线程与大数据业务（如数据库）性能。
2. **cc-NUMA 缓存一致性**：依托 MESI 及其扩展（MESIF、MOESI）同步多节点缓存状态，保障跨节点数据一致与系统稳定（详见 [../cache/mesi.md](/concepts/cache/mesi.md)）。

### 2.2 系统组成

| 组成 | 说明 |
|------|------|
| **节点(Node)** | 基本单元，通常对应一个物理 CPU 插槽，含多个核心，共享节点内 L3 缓存。如一颗 16 核 CPU + 其内存 = 一个节点 |
| **本地内存** | 直连节点内的内存控制器，与 CPU 紧耦合；其容量和速度直接决定节点的处理能力 |
| **互联模块** | 节点间的"桥梁"，负责数据传输：Intel QPI/UPI、AMD Infinity Fabric，提供高带宽低延迟通道，并保障缓存一致性 |

> `numactl --hardware` 即可查看节点数量、CPU/内存配置及节点间距离。

### 2.3 工作原理：本地访问 vs 远程访问

```bash
  本地访问(快)                              远程访问(慢)
  CPU ──内部总线──► 本地内存                CPU ─► 互联模块 ─QPI/UPI─► 目标节点内存控制器 ─► 数据
  几乎无额外开销                            多环节，每环都引入延迟；带宽也更低
```

- **本地访问**：CPU 访问所在节点的本地内存，路径最短、延迟最低、带宽最高。
- **远程访问**：CPU 访问其他节点内存，需经互联模块中转，延迟高、带宽低。分布式计算中若频繁互访对方内存，会因远程延迟拖慢整体。

为保证多节点数据一致，cc-NUMA 用缓存一致性协议。以 **MESI** 为例，缓存行有四态：Modified / Exclusive / Shared / Invalid。某 CPU 改了缓存数据 → 状态变 Modified → 通过互联模块通知其他节点把对应缓存行置为 Invalid → 其他 CPU 再访问时发现失效 → 从内存重读最新值，保证一致。

> Linux 页面分配器**优先从进程所在节点分配内存**，只有本地不足才考虑其他节点——从内核层面优化了访问性能。

### 2.4 拓扑结构：节点间距离 (distance)

拓扑的核心指标是**节点间距离（distance）**，`numactl --hardware` 的 `node distances` 可看。数值越小延迟越低：本地通常为 `10`，跨节点在 `20` 以上，直观体现本地/远程延迟差距。

```bash
$ numactl -H
available: 2 nodes (0-1)
node 0 cpus: 0 1 2 3 4 5 6 7
node 0 size: 32126 MB
node 0 free: 18568 MB
node 1 cpus: 8 9 10 11 12 13 14 15
node 1 size: 32128 MB
node 1 free: 19234 MB
node distances:
node   0   1
  0:  10  20      # 本地=10，到 node1=20 → 远程延迟约为本地的 2 倍
  1:  20  10
```

节点间带宽也差异明显：Intel UPI 单向带宽可达 25.6 GB/s，AMD Infinity Fabric 亦表现出色。分布式数据库同步等大量跨节点传输场景，高带宽能显著提效；反之低带宽成瓶颈。因此系统调优必须考虑拓扑差异，合理分配任务和内存，尽量减少跨节点访问。

---

## 三、Linux 内核中的 NUMA 实现机制

### 3.1 拓扑识别：SRAT / SLIT

内核启动时借助 ACPI 的两张表识别拓扑：

- **SRAT**（系统资源亲和性表）：记录 CPU 核与内存的对应关系（每个节点含哪些逻辑核、关联哪些内存）。
- **SLIT**（系统局部性信息表）：记录各节点之间的距离，用于判断访问延迟。

内核在启动阶段依次调用 `x86_numa_init → numa_init → x86_acpi_numa_init → acpi_numa_init` 解析 SRAT，把内存与节点的关联存进 `numa_meminfo` 结构——一本"内存地图"，每项是 `(起始地址, 结束地址, 节点编号)` 三元组。

```c
// 解析 SRAT 表，初始化 NUMA 节点内存映射
int __init acpi_numa_init(void) {
    struct acpi_table_srat *srat;
    acpi_status status;
    // 获取 SRAT 表
    status = acpi_get_table(ACPI_SIG_SRAT, 0,
                            (struct acpi_table_header **)&srat);
    if (ACPI_FAILURE(status)) {
        pr_warn("ACPI SRAT table not found, NUMA disabled\n");
        return -EINVAL;
    }
    // 解析 SRAT 表中的 CPU 与内存关联信息
    acpi_srat_parse_entries(srat);
    return 0;
}
```

### 3.2 内存管理：三种分配策略 (mempolicy)

内核通过 `mempolicy` 子系统实现三种核心 NUMA 分配策略：

| 策略 | 行为 | 适用场景 | 风险 |
|------|------|----------|------|
| `MPOL_BIND` | **强制**绑定到指定节点 | 内存局部性要求极高（如实时数据库核心）| 指定节点内存不足 → 触发 **OOM** |
| `MPOL_INTERLEAVE` | 跨节点**轮询**分配 | 大内存、对延迟不敏感、对**带宽**要求高（如批量数据中间结果）| 局部性差 |
| `MPOL_PREFERRED` | **优先**首选节点，不足则降级到其他节点 | 大多数普通应用 | 兼顾局部性与灵活性 |

> 内核**默认本地分配**：进程启动后优先从所在节点分配，对数据库等内存密集型应用尤为重要。

### 3.3 NUMA API 与工具（numactl、libnuma）

- **numactl**：管理核心工具。`numactl --hardware` 查拓扑；`numactl --cpunodebind=0 --membind=0` 把进程绑到指定节点的 CPU 和内存。生效前提：BIOS 开启 SRAT/SLIT，内核未禁用 NUMA（无 `numa=off`）。**仅对新进程生效**，无法改运行中进程的策略。
- **libnuma**：细粒度编程接口。`numa_alloc_local()` 直接申请本地内存，比 `malloc` 更高效（编译需 `-lnuma`）。

### 3.4 进程调度中的 NUMA 亲和性

线程绑定能避免线程在 CPU 间频繁切换、提升缓存命中率：

- `taskset -p 0x1 1234`：把 PID 1234 绑到 CPU0。
- 多线程用 `pthread_setaffinity_np()` 在代码层做线程级绑定。
- **关键**：CPU 绑定必须配合内存绑定，否则会"线程在节点1、内存在节点0"的跨节点访问，反而更慢。绑定后可 `cat /proc/PID/status` 查 `Cpus_allowed` / `Mems_allowed` 验证。

内核调度器自带 **numa_balancing**：实时统计进程的本地内存命中率，若远程访问过多，触发**页面迁移或进程迁移**，实现"计算靠近数据"。单节点系统无跨节点问题，该机制反而增加开销，可 `kernel.numa_balancing=0` 禁用。

### 3.5 系统调用与性能监控

- **numastat**：核心指标 `numa_hit`（本地命中）、`numa_miss`（远程访问）。`numa_hit` 越高、`numa_miss` 占比越低越好；**`numa_miss` 占比超 5% 就说明存在跨节点瓶颈**。
- **perf**：`perf record -e mem:trace_mm_page_alloc -g` 记录内存分配事件，`perf report` 分析，定位内存分配/页面迁移等耗时操作。
- `/proc/PID/numa_maps`：查单进程访问统计；`MPOL_PREFERRED` 下 `numa_miss` 不为 0 需排查内存碎片或 THP 干扰。

---

## 四、NUMA 感知的应用程序优化

### 4.1 内存分配优化

- **本地分配 API**：`numa_alloc_local()` / `numa_alloc_onnode()` 从本地/指定节点分配（编译 `-lnuma`；未启用 NUMA 时退化为普通分配）。
- **CPU 与内存强绑定**：`numactl --membind=0 ./app` 强制内存分配限定在节点 0。
- **关闭透明大页（THP）**：THP 把小页合并成 2MB/1GB 大页减少页表开销，但在 NUMA 下可能因节点内存不足触发跨节点分配。建议关闭：

  ```bash

  echo never > /sys/kernel/mm/transparent_hugepage/enabled

  ```

- **内存预留**：为关键进程提前预留本地内存，避免系统紧张时被迫跨节点。

### 4.2 线程绑定与 CPU 亲和性

- `taskset -c 0-3 ./app`：绑到 CPU 0–3，减少核间迁移的上下文切换、提升缓存命中。
- 多线程用 `pthread_setaffinity_np()` 做细粒度绑定（如把网络 I/O 线程固定到特定核）。
- 遵循 **"CPU 节点与内存节点一致"** 原则，绑定后用 `taskset -p` + `numastat` 验证。

#### 深入：绑定了核心，线程是不是"只会在这个 CPU 上运行"？

这是亲和性最核心、也最容易误解的一点。**答案分两半**：

> **是——线程只会在你允许的那些核上运行，绝不会跑到集合之外的核（这是硬约束）。**

> **不是——"只在这个 CPU 运行"不等于"这个 CPU 只给它运行"。**

分开说清楚：

**1) 亲和性是"允许集合(cpumask)"，是对线程的单向约束**

`sched_setaffinity`/`taskset` 给线程设的是一个**允许运行的 CPU 掩码**。调度器保证：这个线程**永远只在掩码内的核上被调度**，一步都不会踏出去。

- 绑到**单核**（`taskset -c 2`）→ 该线程确实只可能在 CPU2 上跑，永远不会迁到别的核。
- 绑到**多核**（`taskset -c 0-3`）→ 线程可在 0/1/2/3 之间，由调度器**动态挑一个空闲的**跑，仍可能在这 4 个核之间迁移（只是不出这个范围）。所以"绑定"不必然等于"钉死在一个核"，取决于你给的集合多大。

**2) 但这只是"线程 → 核"的单向限制，反过来不成立**

绑定**不会**把那个核**变成该线程独占**。默认情况下：

- CPU2 上除了你绑的线程，**照样会调度系统里其他没设亲和性的进程/线程、内核线程、中断下半部等**——它们的默认亲和性是"所有核"，包括 CPU2。
- 于是你以为"独占 CPU2"，实际 CPU2 还在被别人抢时间片，你的线程照样会被抢占、发生上下文切换。

**想要"这个核只给我这个线程用"，需要额外手段**（亲和性本身给不了）：

| 手段 | 作用 |
|------|------|
| `isolcpus=2,3`（内核启动参数）| 把这些核从**调度器默认负载均衡**里摘出去，普通任务不会被派到上面，只有显式绑上来的才跑 |
| `nohz_full=` + `rcu_nocbs=`| 进一步减少时钟中断/RCU 回调对隔离核的打扰（低延迟场景）|
| `irqaffinity` / 把中断绑到别的核 | 让中断处理不落在隔离核上 |
| cgroup `cpuset` | 用 cpuset 把一组核划给特定进程组专用 |

`isolcpus` + 亲和性绑定配合，才能做到接近"独占一个核"——高频交易、DPDK、实时任务常这么干。

**3) 亲和性 vs NUMA 内存：绑了核 ≠ 绑了内存**

这是本文反复强调的坑：`taskset` **只管线程在哪个核跑，完全不管它的内存分配在哪个节点**。绑了核但没绑内存，仍可能"线程在 node1 的核上跑、内存却在 node0"——远程访问照旧。所以：

```bash
taskset -c 8-15 ./app            # 只绑核（node1 的核），内存可能还在 node0 → 跨节点！
numactl --cpunodebind=1 --membind=1 ./app   # 核 + 内存都绑 node1，才真正本地化
```

> 结论：**要 NUMA 本地性，用 `numactl` 同时绑核和内存；单用 `taskset` 只解决核、不解决内存。**

**4) 代码层：`pthread_setaffinity_np` 精确到线程**

进程级 `taskset` 对所有线程一视同仁；要"IO 线程绑 A 核、计算线程绑 B 核"这种，得在代码里逐线程设：

```c
#define _GNU_SOURCE
#include <pthread.h>
cpu_set_t set;
CPU_ZERO(&set);
CPU_SET(2, &set);                 // 只允许 CPU2
pthread_setaffinity_np(pthread_self(), sizeof(set), &set);
// 之后本线程只会在 CPU2 上运行；CPU_SET 多个核则在这些核间浮动
```

**5) 验证**

```bash
taskset -cp <PID>                 # 看进程/线程当前的允许核集合
taskset -cp <TID>                 # 线程级（TID）
cat /proc/<PID>/status | grep Cpus_allowed_list   # 允许运行的核
# 实际正跑在哪个核：top 按 f 开 P 列，或 ps -o psr= -p <PID>
```

**实例：`cat /proc/<pid>/status` 逐字段解读**

下面是 `cpu_demo`（PID 1052）在一台 4 核机器上的真实 `status` 节选，正好把上面几点全印证了：

```bash
Name:       cpu_demo
State:      S (sleeping)         # 睡眠态：此刻没跑满 CPU（多半停在菜单等输入，或选了场景3 idle）
Threads:    2                    # 主线程 + detach 的 work_thread，和 main.cpp 结构对上
VmSize:     88600 kB             # 虚拟内存（映射了 libstdc++ 等一堆库）
VmRSS:       1372 kB             # 真正占的物理内存 —— 极小；VmSize ≫ VmRSS = 映射多、触碰少
RssAnon:      196 kB             #   其中匿名(堆/栈)
RssFile:     1176 kB             #   其中文件映射(代码/库页)，196+1176≈VmRSS
VmSwap:         0 kB             # 没被换出，内存充足
Cpus_allowed:        3           # 掩码 0x3 = 二进制 11
Cpus_allowed_list:   0-1         # → 只允许在 CPU0、CPU1 跑（机器有 4 核，说明被约束过）
Mems_allowed_list:   0           # 只允许从 NUMA 节点0 分配 → 单节点(或被限定 node0)
voluntary_ctxt_switches:    66   # 主动让出 66 次：等输入/IO 时挂起，sleeping 的特征
nonvoluntary_ctxt_switches:  1   # 几乎没被抢占 → 没在抢 CPU，和 State: S 一致
```

读这份数据的三个关键结论：

| 观察 | 说明 |
|------|------|
| `Cpus_allowed_list: 0-1`（4 核机上）| 该进程**被限制**只能用 CPU0/1（taskset / cgroup cpuset / 容器 / 云配额），绝不会跑到 CPU2/3——**"允许集合"的真实样本**：它在 0、1 间浮动，但出不了这个圈 |
| `Mems_allowed_list: 0` | 内存只能从节点0 分配——单 NUMA 节点（云主机常见），此时 NUMA 优化无从谈起 |
| `VmSize 88600 ≫ VmRSS 1372` | 虚拟内存远大于物理内存：映射了大量库但没真正触碰那么多页——印证"看真实占用要看 RSS，不是 VIRT/VmSize" |

> `Cpus_allowed`(掩码) 与 `Cpus_allowed_list`(人类可读) 是同一信息两种写法：`3` = `0x3` = `0-1`。绑定生效后，这两行就是你 4.2 节所有操作的"验收单"。

> 一句话总结：**绑定 = 给线程画了个"只能在这些核里跑"的圈（硬约束、单向）；圈多大由你定（单核=钉死，多核=范围内浮动）；但圈里的核并不因此归你独占，想独占得再加 `isolcpus`/cpuset；而且绑核不等于绑内存，NUMA 本地性要用 `numactl` 连内存一起绑。**

### 4.3 数据局部性设计

- **数据库分表**：把高频热点数据放在对应节点本地内存（如电商库把热门商品放节点0）。
- **并行计算按节点拆分数据**：如图像识别按区域分片，每个节点只处理本地分片，减少跨节点传输。

### 4.4 性能调优方法论

**定位瓶颈**：

```bash
numastat                                    # 看 numa_hit / numa_miss 占比，miss>5% 即有问题
perf record -e mem:trace_mm_page_alloc -g   # 记录内存分配事件
perf report                                 # 分析热点：_alloc_pages_nodemask / migrate_pages 等
cat /sys/kernel/debug/numa/               # 排查内核 NUMA 初始化异常
```

**动态调优与验证**：遵循"测试-优化-验证"循环。数据库多用 `MPOL_BIND`，批处理多用 `MPOL_INTERLEAVE`。内核不支持动态改进程内存策略，需动态调整时用 `move_pages` 系统调用做页面热迁移。每轮调优后用 `sysbench` / `iperf` 对比吞吐、延迟，验证效果。

### 4.5 什么时候该用 libnuma（而不是 numactl）

`numactl` 和 `libnuma` 是同一套能力的两个层次：**`numactl` 是命令行工具，对整个进程施加统一策略；`libnuma`（`#include <numa.h>`，链接 `-lnuma`）是 C 库，让程序在代码里做"每块内存、每个线程"级别的精细控制**。二者背后都是 `set_mempolicy`/`mbind`/`sched_setaffinity` 等系统调用。

#### 先判断：你到底需不需要 libnuma

大多数情况**不需要**——能用 `numactl` 在启动时把进程绑到一个节点就够了。**只有当"整个进程绑到单节点"这个粗粒度手段解决不了问题时，才上 libnuma**。典型分水岭：

| 场景 | 够用的手段 |
|------|-----------|
| 进程内存能塞进单个节点，且不需要跨节点扩展 | **`numactl --cpunodebind + --membind`**，不必写代码 |
| 进程要用满**多个节点**的 CPU 和内存（单节点装不下），又想让每个线程访问本地内存 | **必须用 libnuma** 在代码里按节点分配、按节点绑线程 |

#### 该用 libnuma 的四类场景

**① 内存需求 > 单节点容量，但仍要保证本地性**

进程要用 100GB、而每个节点只有 64GB——绑单节点根本装不下。这时需要在代码里**把数据结构按节点切分**：node0 的线程处理的数据用 `numa_alloc_onnode(size, 0)` 分到 node0，node1 的分到 node1。`numactl` 给不了这种"一个进程内、不同数据放不同节点"的控制。

```c
#include <numa.h>
// 大数组按节点分片：第 i 片分配在第 (i % n_nodes) 个节点
int n = numa_num_configured_nodes();
for (int i = 0; i < nshards; i++)
    shard[i] = numa_alloc_onnode(shard_bytes, i % n);
// 用完必须用 numa_free（不是 free），且要传回长度
numa_free(shard[i], shard_bytes);
```

**② 线程池 / 工作线程要和它处理的数据"同节点"**

服务器程序常有一个大线程池。想做到"跑在 node0 核上的线程，只碰 node0 内存里的数据"，需要**线程级**绑定 + **该线程本地分配**：

```c
// 在工作线程启动时：先把自己绑到某节点的 CPU，再分配本地内存
numa_run_on_node(node_id);              // 线程只在该节点的核上跑
numa_set_localalloc();                  // 之后该线程的分配默认落在本地节点
char *buf = numa_alloc_local(buf_size); // 显式本地分配
```

单靠 `numactl` 只能把**整个进程**绑一个节点，做不到"进程用多节点、但每个线程各自本地化"。

**③ first-touch 陷阱：谁初始化、内存就落在谁那**

Linux 默认 first-touch——物理页由**第一个写它的线程**所在节点分配。经典 bug：主线程 `malloc` 一大块并 `memset` 初始化 → 全部落在主线程节点 → 后续多节点的工作线程访问它全是远程。libnuma 让你显式打破这个默认：要么 `numa_alloc_onnode` 直接指定，要么让"将来用这块内存的线程"去做首次触碰。

**④ 运行期需要按负载动态迁移内存**

`numactl` 只在启动时生效、且不能改运行中进程。若程序运行期数据热点会漂移，需要用 libnuma 的 `numa_move_pages()` / `move_pages(2)` 把页热迁移到当前访问它的节点。

#### libnuma 常用 API 速查

| API | 作用 |
|-----|------|
| `numa_available()` | 检测系统是否支持 NUMA（返回 -1 表示不支持，**调用其他 API 前必须先查**）|
| `numa_num_configured_nodes()` | 节点数 |
| `numa_alloc_onnode(size, node)` | 在**指定节点**分配 |
| `numa_alloc_local(size)` | 在**当前线程所在节点**分配 |
| `numa_alloc_interleaved(size)` | 跨所有节点**交错**分配（带宽型）|
| `numa_free(ptr, size)` | 释放（**必须配对，且要传长度**，不能用 `free`）|
| `numa_run_on_node(node)` | 把当前线程限制到某节点的 CPU 上跑 |
| `numa_set_localalloc()` | 设当前线程后续分配默认本地 |
| `numa_set_preferred(node)` / `numa_set_membind(mask)` | 设首选/强制绑定的内存节点 |
| `numa_move_pages(...)` | 运行期把已分配的页迁移到指定节点 |

#### 使用注意

- **编译链接**：`gcc app.c -o app -lnuma`；运行前用 `numa_available() >= 0` 判断，否则这些调用无意义（未启用 NUMA 时行为退化）。
- **分配粒度大**：`numa_alloc_*` 按**页**（通常 4KB）对齐分配，不适合频繁分配小对象（那样浪费且慢）——它面向"大块、长生命周期、访问密集"的数据（缓冲池、矩阵、分片表），小对象仍用常规分配器。
- **释放要配对**：`numa_alloc_*` 分配的必须 `numa_free(ptr, size)` 释放，且要记住长度，混用 `free()` 是 bug。
- **别过度工程**：libnuma 把 NUMA 拓扑硬编进了代码逻辑，增加复杂度和可移植性负担。**先用 `numactl` 量出确实有跨节点瓶颈（`numastat` 的 `numa_miss` 高）、且单节点装不下，再考虑改代码**。

> 一句话分工：**`numactl` = 部署/运维层，一条命令绑整个进程，占 90% 场景；libnuma = 开发层，当程序必须"横跨多节点还要保持本地性"时才写代码精细控制。** 排查手段（`numastat -p`、`perf`）两者通用。

---

## 五、核心应用场景与优化实践

### 5.1 数据库（MySQL / Oracle）

**InnoDB 缓冲池本地化**：

```bash
numactl --cpunodebind=0 --membind=0 /usr/sbin/mysqld   # 进程+内存都绑节点0
echo never > /sys/kernel/mm/transparent_hugepage/enabled  # 关 THP
```

- `innodb_buffer_pool_size` 不超过节点内存容量（常设为节点内存 70%），确保缓冲池分配在本地。
- `innodb_log_group_home_dir` 指向本地磁盘，加快 redo 日志写入。

**避免跨节点数据交互**：

- 网卡中断亲和性绑到数据库所在节点（`irqbalance`），减少数据包跨节点 DMA。
- 主从架构把主/从分置不同节点，避免资源竞争。
- 定期用 `numastat` 监控 `numa_hit` / `numa_miss`。

### 5.2 虚拟化与云计算

**KVM/QEMU 的 vNUMA**：把物理 NUMA 拓扑暴露给虚拟机。libvirt XML：

```xml
<domain type='kvm'>
  <numa>
    <cell id='0' cpus='0-7'  memory='8192' unit='MiB'/>
    <cell id='1' cpus='8-15' memory='8192' unit='MiB'/>
  </numa>
  <vcpu placement='static'>16</vcpu>
  <memory unit='MiB'>16384</memory>
</domain>
```

> 注意：vCPU 数不宜超过单个物理节点的核心数，否则部分 vCPU 无法绑本地节点，增加跨节点调度概率。

**Kubernetes NUMA 亲和性调度（Volcano 插件）**：

```yaml
apiVersion: v1
kind: Pod
metadata:
  name: numa-aware-pod
  annotations:
    volcano.sh/numa-policy: "preferred"   # 优先调度到满足 CPU/内存需求的 NUMA 节点
spec:
  containers:
  - name: my-container
    image: my-image
    resources:
      requests: { cpu: "2", memory: "1Gi" }
      limits:   { cpu: "2", memory: "1Gi" }
```

> 配合 `cpuManagerPolicy: static`，可把 Pod 的 CPU 核绑到指定 NUMA 节点。

### 5.3 高性能计算（HPC）

**MPI 按节点分片绑定**：

```bash
mpirun --bind-to socket --map-by socket:pe=4 -np 16 ./mpi_app
# 16 个进程每 4 个一组，分别绑到 4 个 NUMA 节点（每节点一个插槽）
```

**OpenMP 线程亲和性**：

```bash
export KMP_AFFINITY=granularity=fine,compact,1,0   # 线程紧凑绑到同一节点的核
```

**大规模集群**：用 Slurm 收集各节点 CPU/内存/互联参数进拓扑数据库，调度器据此匹配最优节点组合，避开高延迟互联。

### 5.4 云原生与容器调度

**节点亲和性调度 Pod**：

```yaml
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: numa-node
            operator: In
            values: ["0"]      # 调度到标签 numa-node=0 的节点
```

**CPU 管理器 + 拓扑感知**：kubelet 配 `cpuManagerPolicy: static` + `topologyManagerPolicy: single-numa-node`，实现"Pod CPU 与内存同节点"。**仅对 Guaranteed QoS 的 Pod 生效**（BestEffort / Burstable 无效）。

---

## 六、Linux 实战：配置与调优指南

### 6.1 查看拓扑

```bash
lscpu | grep -i numa                # 快速看节点数与每节点的核
numactl --hardware                  # 详细：节点 CPU/内存/空闲/距离矩阵；只显示 1 节点 = NUMA 未启用(查 BIOS / numa=off)
ls  /sys/devices/system/node/       # node0 node1 ... 每个节点一个目录
cat /sys/devices/system/node/node0/meminfo   # 节点内存明细
cat /sys/devices/system/node/node0/cpulist   # 节点的 CPU 核列表
```

### 6.2 监控性能

```bash
numastat                # 系统级 numa_hit / numa_miss / numa_foreign
numastat -p <PID>       # 单进程内存在各节点的分布
watch -n 1 numastat     # 每秒刷新，看趋势
perf top -e mem:trace_mm_page_alloc -g   # 实时热点，关注 _alloc_pages_nodemask / migrate_pages
```

### 6.3 配置亲和性

```bash
numactl --cpunodebind=0 --membind=0 ./app   # 新进程：CPU+内存都绑节点0
taskset -c 0-7 <PID>                         # 运行中进程：只能调 CPU 绑定
numactl --localalloc ./app                   # 内存就近分配到运行所在节点
numactl --interleave=all ./app               # 跨节点轮询（带宽型负载）
cat /proc/<PID>/numa_maps                    # 验证内存落在哪个节点
```

> `--localalloc` / `--membind` 在节点内存不足时**无容错**，会触发 OOM，生产环境需评估节点内存余量。

### 6.4 优化案例：MySQL 调优

1. `numactl --hardware` 确认拓扑（假设双节点）。
2. `numactl --cpunodebind=0 --membind=0 /usr/sbin/mysqld` 绑到节点0。
3. `echo never > /sys/kernel/mm/transparent_hugepage/enabled` 关 THP。
4. `innodb_buffer_pool_size` 设为节点内存的 70%，确保本地分配。
5. `sysbench` 压测验证。

**效果**：`numa_miss` 占比从较高水平降到 1% 以下，查询延迟提升 20%–30%。

---

## 七、常见问题与误区

### 7.1 性能瓶颈根源

- **跨节点访问延迟**：本地访问几十纳秒，跨节点可达百纳秒以上。解法：CPU 与内存强绑定，`numactl --cpunodebind=0 --membind=0`，做到"计算与数据同节点"。
- **远程访问导致的 SWAP 陷阱**：默认策略下本地节点内存耗尽时，进程**不会** fallback 到其他节点，而是触发 SWAP（换盘）→ 性能急剧下降。解法：

  ```bash

  numactl --interleave=all ./app          # 内存跨节点交错，避免单节点耗尽

  sysctl -w vm.zone_reclaim_mode=0        # 允许节点间内存共享，本地不足时用其他节点而非 SWAP

  ```

### 7.2 虚拟化环境的 NUMA 陷阱

- **虚拟机跨 NUMA 调度**：vCPU 跨物理节点调度导致内存访问延迟翻倍。解法：启用 vNUMA、把 vCPU 与内存绑同一物理节点、限制 vCPU 数不超过单节点核心数。
- **CPU 热添加与 NUMA 不兼容**：热添加 CPU 可能破坏原有拓扑，内核无法正确关联新 CPU 与内存节点。解法：关键系统避免运行时热添加；必须时操作后重启或重配 vNUMA。

### 7.3 禁用 NUMA 的误区

- **单节点系统**：无跨节点访问，`numa_balancing` 反而增开销，可 `kernel.numa_balancing=0` 禁用。
- **多节点系统**：禁用 NUMA 会让所有访问退回统一模式、丧失本地优势、延迟增大——除非特殊需求，不应禁用。
- **过度手动绑定**：把多个进程绑到同一节点会使该节点资源耗尽、其他节点闲置。需结合 Kubernetes/Slurm 动态均衡，并用 `numastat`/`perf` 验证。

### 7.4 其他常见问题

- **内存碎片化**：频繁分配/释放产生大量小空闲块，大块分配失败会被迫跨节点。解法：启用内存规整（`compact_memory`）、关闭 THP。
- **硬件架构兼容性**：
  - **AMD Zen**：多 CCD 设计，每 CCD 对应一个 NUMA 节点，可用更细粒度的 CCD 绑定。
  - **Intel Sapphire Rapids**：支持 UPI 2.0，更高带宽，需关注带宽利用。

---

## 八、总结

NUMA 是多核 Linux 性能调优的核心：本质是用"**本地内存优先访问**"突破 SMP 的总线瓶颈。从内核的拓扑识别（SRAT/SLIT）、调度策略（numa_balancing），到应用的亲和性配置（numactl/taskset/libnuma）、数据局部性设计，再到实战工具（numastat/perf）与案例优化，环环相扣。

**一句话决策**：延迟敏感 + 有局部性 → `--cpunodebind + --membind` 绑死一个节点；带宽型 + 访问遍布内存 → `--interleave=all`；单节点机器 → 关掉 numa_balancing，其余不用折腾。

> 配套：概念速查 [numa.md](/concepts/numa/numa.md)、绑定工具 [numactl.md](/tools/numa/numactl.md)、节点内存统计 [numastat.md](/tools/numa/numastat.md)、缓存一致性 [../cache/mesi.md](/concepts/cache/mesi.md)、热点定位 [../code/perf.md](/tools/code/perf.md)。

