NUMA:内存为什么有远近
上一篇讲了 swap 如何为匿名页提供外部 backing store。讨论中一直隐含一个假设:物理内存是一块均匀的资源,任何 CPU 访问任何物理页的代价相同。在 NUMA 架构下这个假设不成立。
这篇要回答的核心矛盾:
NUMA 让"物理内存"不再是均匀资源;分配位置、CPU 位置和迁移策略共同决定访问成本。
问题从哪里来
在 UMA(Uniform Memory Access)系统中,所有 CPU 共享一条总线访问同一组内存控制器。每个 CPU 访问任何物理地址的延迟相同。早期单路和低端双路机器大多是这种架构。
当 CPU 数量增加,单一总线成为瓶颈。NUMA(Non-Uniform Memory Access)把系统拆成多个节点(node),每个节点包含若干 CPU 核心和一组本地内存。节点内部通过本地总线通信(快),节点之间通过互联(QPI、UPI、Infinity Fabric 等)通信(慢)。
1 | |
CPU 访问本节点内存延迟约 80-100ns(典型值),跨节点访问可能增加 40-80% 的延迟,具体取决于互联拓扑和跳数。带宽差异同样存在。
这对内核内存管理的影响是根本性的:分配一页物理内存不再只关心"有没有空闲页",还要关心"离谁近"。
node、zone 和 CPU 的关系
Linux 内核用三层结构组织物理内存:
| 层级 | 含义 | 数据结构 |
|---|---|---|
| node | 一个 NUMA 节点,对应一组本地内存 | struct pglist_data(pg_data_t) |
| zone | 节点内按地址范围或属性分区 | struct zone |
| page | 单个物理页帧 | struct page / struct folio |
每个 node 有自己的一组 zone(DMA、DMA32、Normal、Movable)。每个 node 有自己的 lruvec、自己的 kswapd、自己的空闲页统计。
numactl --hardware 的输出直接展示这个结构:
1 | |
node distances 表是关键:对角线上的 10 表示本地访问(基准距离),非对角线的 21 表示远程访问代价是本地的 2.1 倍。在更大系统(4 路、8 路)中,这个矩阵不对称且有多级距离。
默认分配策略:本地优先
内核的默认内存分配策略是本地分配(local allocation):在当前 CPU 所属的 node 上分配物理页。这是最简单也最常见的情况——进程在哪个 CPU 上触发 page fault,物理页就从哪个 node 分配。
如果本地 node 空闲页不足,分配回退到其他 node(按距离排序,近的优先)。这个回退列表叫 zonelist,在系统启动时根据 NUMA 拓扑构建。
本地分配对大多数工作负载是合理的:进程在某个 CPU 上运行并访问自己分配的内存,数据局部性自然满足。问题出在两种场景:
第一,进程被调度器迁移到另一个 node 的 CPU 上运行。原来的本地内存变成了远程内存。
第二,分配时的 CPU 位置与后续主要访问的 CPU 位置不同。例如一个线程池在初始化阶段集中分配内存,但后续由不同 node 上的 worker 线程分别使用。
内存策略
Linux 提供 set_mempolicy 和 mbind 系统调用,允许进程显式控制内存分配位置。
| 策略 | 行为 |
|---|---|
MPOL_DEFAULT |
回退到系统默认(本地分配) |
MPOL_BIND |
只从指定的 node 集合分配 |
MPOL_PREFERRED |
优先从指定 node 分配,失败时回退 |
MPOL_INTERLEAVE |
按页粒度在指定 node 集合间轮转分配 |
MPOL_PREFERRED_MANY |
优先从指定集合分配,全部压力下回退到全局 |
MPOL_WEIGHTED_INTERLEAVE |
按权重在 node 间分配(如 node0:5, node1:2) |
策略按优先级从低到高分层:系统默认 < 进程策略(set_mempolicy)< VMA 策略(mbind)< 共享策略。cpuset 的限制优先于所有策略——策略指定的 node 如果不在 cpuset 允许的集合中,取交集。
numactl 命令是用户空间设置策略的常用工具,内部通过 set_mempolicy + fork + exec 实现:
1 | |
模式提炼:资源标识包含位置
1 | |
| 维度 | UMA 视角 | NUMA 视角 |
|---|---|---|
| 物理页 | 只有 PFN | PFN + node + zone |
| 分配决策 | 有没有空闲页 | 哪个 node 有空闲页 + 离 CPU 多远 |
| 回收 | 全局平衡 | per-node 水位线 + per-node kswapd |
| 性能模型 | 访问延迟固定 | 延迟取决于 CPU 与内存的拓扑关系 |
这种"资源带位置属性"的思路在分布式系统中无处不在:CDN 的边缘节点 vs 源站、数据库读副本的区域感知路由、Kubernetes 的 topology-aware scheduling。核心思想是:访问延迟不只取决于资源本身,还取决于请求者与资源之间的距离。
NUMA balancing
即使分配时做了本地优先,后续调度器可能把进程迁移到其他 node。此时内存访问变成远程的。NUMA balancing 机制试图检测这种情况并迁移页面到当前 CPU 所在 node。
原理:内核周期性地把某些 PTE 标记为"无效"(清除 present 位,设置特殊标记)。进程访问这些页面时触发 NUMA hint fault(一种特殊缺页)。内核统计每个页面被哪个 node 上的 CPU 访问,如果页面被远程 node 频繁访问,将其迁移到该 node。
1 | |
NUMA balancing 的配置入口:
1 | |
NUMA balancing 不是免费的。每次 NUMA hint fault 都有开销(类似一次软件 TLB miss);迁移页面需要分配新页、复制数据、更新反向映射中的所有 PTE。对于频繁被多个 node 共享访问的页面,迁移可能导致"乒乓"——页面在 node 之间来回搬,反而更慢。
一个最小实验:观察 NUMA 拓扑与访问延迟
下面这个实验测量本地访问 vs 远程访问的延迟差异。
1 | |
编译与运行:
1 | |
如果机器是 UMA(只有一个 node),所有测试结果会接近相同。这本身是一个观察:“NUMA 问题只在多 node 系统上存在”。
完成实验后清理:
1 | |
读取实验结果
在双路或多路 NUMA 机器上预期看到的差异:
本地访问(CPU 在 node 0,内存绑定 node 0):每页访问延迟作为基准。典型值在 80-120ns(取决于具体硬件和访问模式)。
远程访问(CPU 在 node 0,内存绑定 node 1):每页访问延迟比本地高 30-80%。在两跳 NUMA 拓扑中差异更大。
交织分配:延迟介于本地和远程之间,接近两者的加权平均。交织的好处不在于单页延迟,而在于带宽分摊——大块顺序访问时两个 node 的内存控制器同时服务请求。
关键观察:
- 延迟差异来自物理互联的传输开销,软件无法消除。软件能做的只是让热数据尽量在本地。
numastat命令可以查看每个 node 的分配命中/未命中统计。numa_miss高意味着大量分配落到了非本地 node。/proc/<pid>/numa_maps显示进程每个 VMA 的页面分布在哪些 node 上。
模式提炼:拓扑感知分配降低尾延迟
1 | |
在生产系统中,NUMA 不感知的分配往往不影响平均延迟,但会显著影响 P99/P999 尾延迟。因为偶尔的远程访问叠加到某些请求的关键路径上时,该请求的延迟突然增高。这和分布式系统中"跨 AZ 调用偶尔导致尾延迟抖动"是同一个现象。
per-node 回收与水位线
在 NUMA 系统中,每个 node 独立维护水位线和 kswapd。node 0 的内存紧张不影响 node 1(除非全局都紧张)。
这意味着:一个进程如果只绑定到 node 0(MPOL_BIND),即使 node 1 有大量空闲内存,该进程仍可能触发 direct reclaim 甚至 OOM。从全局看内存充裕,但从该进程的策略约束看已经耗尽。
/proc/zoneinfo 中每个 node 的每个 zone 都有独立的 min/low/high 水位线。numastat -m 可以观察 per-node 的内存使用细节。
核心结构:mempolicy
struct mempolicy 控制进程/VMA 级别的 NUMA 分配策略(include/linux/mempolicy.h,精简):
1 | |
分配路径通过 alloc_pages_mpol() 读取当前生效的 mempolicy,决定从哪个 node 的 free list 取页面。mode 不同行为差异很大:BIND 失败不回退,PREFERRED 失败可回退。
源码锚点
| 入口 | 读它的目的 |
|---|---|
mm/mempolicy.c |
set_mempolicy()、mbind()、策略解析与应用 |
mm/page_alloc.c |
zonelist 构建、fallback 顺序、NUMA 感知分配 |
mm/migrate.c |
migrate_pages():NUMA balancing 使用的页迁移核心 |
kernel/sched/fair.c |
NUMA balancing 与调度器的交互 |
include/linux/mmzone.h |
struct pglist_data、node 与 zone 定义 |
Documentation/admin-guide/mm/numa_memory_policy.rst |
策略语义的权威描述 |
读 mm/mempolicy.c 中的 alloc_pages_mpol() 时可以带着一个问题:当策略指定的 node 集合内所有 node 都达到水位线以下时,fallback 路径是什么?
研究生迁移表
| Linux 概念 | 一般系统设计 |
|---|---|
| NUMA node | 数据中心的可用区(AZ) |
| 本地 vs 远程访问延迟 | 同 AZ vs 跨 AZ 延迟 |
MPOL_BIND |
严格区域约束(pod anti-affinity with zone) |
MPOL_INTERLEAVE |
负载均衡跨区域分散 |
| NUMA balancing(页迁移) | 数据重新分布(re-sharding、replica placement) |
| NUMA hint fault | 采样探测访问模式 |
| per-node kswapd | per-AZ 资源管理独立性 |
| zonelist fallback | 跨区域降级路由 |
NUMA 架构的核心教训——“资源的物理位置影响访问性能”——在任何非均匀系统中都成立。网络拓扑中的 hop count、存储系统中的 tier 距离、甚至 CPU cache 的 L1/L2/L3 距离,都是"位置决定延迟"的实例。
常见误解
第一个误解是认为 NUMA 只存在于多路物理服务器。云主机也可能暴露 NUMA 拓扑(取决于虚拟化配置)。CXL 内存和异构内存池也会引入类似的非均匀访问特征。
第二个误解是认为 NUMA balancing 总是有益的。对于被多个 node 共享读取的数据(如只读配置表),NUMA balancing 可能导致无效迁移。此时用 MPOL_INTERLEAVE 分散分配比依赖自动迁移更好。
第三个误解是把"远程访问慢"理解为"必须避免所有远程访问"。大多数应用的工作集中在少量热页面上。只要热页面在本地,偶尔的远程访问不会构成性能问题。优化应该集中在高频访问路径上。
第四个误解是认为 MPOL_BIND 只是"偏好"。MPOL_BIND 是硬约束——如果指定的 node 集合没有空闲内存,分配会触发 direct reclaim 甚至 OOM,不会回退到其他 node。MPOL_PREFERRED 才是"尽量但可以回退"。
第五个误解是认为进程绑定 CPU 就自动获得了 NUMA 感知。CPU 绑定影响进程在哪执行,但不影响已分配页面的物理位置。如果在绑定 CPU 之前分配了大量内存,这些页面可能仍然在其他 node 上。需要配合 mbind 或内存迁移才能把数据搬到本地。
第六个误解是认为 NUMA 距离在硬件确定后就固定不变。CXL(Compute Express Link)2.0/3.0 允许动态挂载和移除内存设备,内核需要在运行时更新 NUMA 拓扑。未来的分层内存(tiered memory)架构中,同一 node 内部也可能有不同延迟的介质层级,NUMA 抽象正在从"离散 node"向"连续延迟梯度"演化。
练习
第一,在一台 NUMA 机器上(numactl --hardware 显示多个 node),按文中实验分别测量本地访问和远程访问的延迟。计算远程/本地比值,与 node distances 表中的值比较。
第二,写一个分配 1GB 内存的程序,分别用 --membind=0、--membind=1、--interleave=0,1 运行。用 numastat -p <pid> 观察页面在各 node 的分布。
第三,在一个 NUMA 机器上启用 NUMA balancing(echo 1 > /proc/sys/kernel/numa_balancing),用 --membind=1 --cpunodebind=0 运行一个反复访问自己内存的程序。观察 /proc/vmstat 中 numa_hint_faults 和 numa_pages_migrated 是否增长。
第四,阅读 mm/mempolicy.c 中 do_mbind() 的实现。画出策略安装到 VMA 上的路径,标出如何处理"策略 node 集合与 cpuset 不相交"的情况。
第五,查看一个 Java 或 Go 服务进程的 /proc/<pid>/numa_maps,识别哪些内存段分布在多个 node 上。思考:对于这个应用,哪种策略(bind、interleave、preferred)最合适?
系列导航
- 上一篇:Swap:换出去的页怎样回来
- 本文:NUMA:内存为什么有远近
- 下一篇:Huge Page:TLB 压力和页表开销
