NUMA:拓扑、内存策略与绑定
NUMA:拓扑、内存策略与绑定
多插槽服务器上,一块内存离有的核近、离有的核远。 本文说明这种差别从哪来、代价多大、内核默认把页放在哪、怎样用命令和代码控制位置,以及怎样观测和排查。
适用范围:Linux x86-64,内核 5.x 起的行为为主线。拓扑的划分方式由 CPU 型号和 BIOS 设置决定,属于实现细节,以本机 numactl --hardware 的输出为准。
实测说明:本文代码、命令和输出样例均未实测。延迟和带宽数字只表示量级,随平台变化。
观测工具的更多用法见 performance.md 第九章;网卡收发包与 NUMA 的关系见 xdp-dpdk.md。
目录
| # | 章节 | 主题 |
|---|---|---|
| 一 | UMA 与 NUMA | 为什么从共享总线变成每个插槽自带内存 |
| 二 | 拓扑 | 节点、插槽、核;距离矩阵;单插槽多节点 |
| 三 | 远端访问的代价 | 延迟、带宽、跨插槽的缓存行传递 |
| 四 | 内存分配策略 | 首次触碰、策略一览、节点内存不足时的行为 |
| 五 | 首次触碰的陷阱 | 主线程初始化、std::vector、内存复用、页缓存 |
| 六 | 命令行控制 | numactl、taskset、cpuset、systemd |
| 七 | 程序内控制 | 线程亲和、libnuma、mbind、查询页所在节点 |
| 八 | 自动 NUMA 均衡 | 扫描、提示缺页、迁移;什么时候关 |
| 九 | 设备与中断 | 网卡、NVMe 所在节点;中断亲和;大页 |
| 十 | 典型应用的做法 | 数据库、JVM、DPDK、低延迟交易、容器、虚拟机 |
| 十一 | 观测与排查 | numastat、numa_maps、perf mem、排查流程 |
| 十二 | 面试速答卡 | 高频问题浓缩答案 |
一、UMA 与 NUMA
UMA(Uniform Memory Access) NUMA(Non-Uniform Memory Access)
CPU0 CPU1 CPU2 CPU3 ┌─────────── 节点 0 ───────────┐
└──────┴──┬───┴──────┘ │ CPU0 + 内存控制器 + 本地内存 │──┐
共享总线 └──────────────────────────────┘ │ 互联链路
内存控制器 ┌─────────── 节点 1 ───────────┐ │
内存 │ CPU1 + 内存控制器 + 本地内存 │──┘
└──────────────────────────────┘
| UMA | NUMA | |
|---|---|---|
| 内存控制器 | 所有 CPU 共用一个 | 每个插槽自带 |
| 总内存带宽 | 固定,不随 CPU 数增长 | 每加一个插槽,多一组内存通道 |
| 访问延迟 | 所有核到所有内存相同 | 本地近,远端要经过互联链路 |
| 扩展性 | CPU 越多,总线争用越严重 | 好 |
互联链路在 Intel 平台上是 UPI,在 AMD 平台上是 Infinity Fabric。
NUMA 不提供隔离:任何核都能访问任何节点的内存,差别只在快慢。
二、拓扑
三层关系
插槽(socket) 主板上的一个 CPU 插座
└─ 节点(node) 一组核 + 一块本地内存;内核按节点管理内存
└─ 核(core)
└─ 逻辑 CPU(超线程)
常见情况是一个插槽对应一个节点。BIOS 设置可以改变这一点,见本节末尾。
查看拓扑
输出样例(双插槽,每插槽 16 核 32 线程):
available: 2 nodes (0-1)
node 0 cpus: 0 1 2 ... 15 32 33 ... 47
node 0 size: 128000 MB
node 0 free: 100000 MB
node 1 cpus: 16 17 ... 31 48 49 ... 63
node 1 size: 128000 MB
node 1 free: 110000 MB
node distances:
node 0 1
0: 10 21
1: 21 10
| 字段 | 含义 |
|---|---|
node N cpus |
属于这个节点的逻辑 CPU 编号 |
node N size / free |
这个节点的内存总量和空闲量 |
node distances |
距离矩阵,来自固件提供的 ACPI SLIT 表 |
距离矩阵里的数字是相对值:本地固定为 10,21 表示远端的访问成本约为本地的 2.1 倍。它是固件声明的,不是测出来的延迟。
其他查看方式:
|
逻辑 CPU 的编号顺序由固件决定。上面的样例里 015 和 3247 同属节点 0,其中 0 和 32 是同一个物理核的两个超线程。绑核之前先查本机的编号,不能假定相邻编号在同一节点。
单插槽也可能有多个节点
| 情况 | 原因 |
|---|---|
| AMD EPYC | BIOS 选项可把一个插槽划成 1、2 或 4 个节点,还可以把每个三级缓存域呈现为一个节点 |
| Intel 至强的 Sub-NUMA Clustering | BIOS 选项把一个插槽按内存控制器划成多个节点 |
| CXL 内存扩展、持久内存 | 呈现为只有内存、没有 CPU 的节点 |
| 虚拟机 | 虚拟化层呈现的拓扑与物理机无关 |
只有一个节点的机器上,所有核到所有内存的距离相同,NUMA 绑定不产生差别。
三、远端访问的代价
延迟与带宽
| 指标 | 本地 | 远端 | 说明 |
|---|---|---|---|
| 一次访存的延迟 | 约 70~90 ns | 约 120~150 ns | 双插槽至强上的常见量级,未实测 |
| 内存带宽 | 本插槽全部内存通道 | 受互联链路限制,低于本地 | 顺序扫描大数组时差距明显 |
本机的数值用 Intel Memory Latency Checker 测量:
只有未命中缓存的访问才付这个代价
访存 → 一级缓存命中? ── 是 → 约 1 ns,与 NUMA 无关
│ 否
▼
二级、三级缓存命中? ── 是 → 几到几十 ns,与 NUMA 无关
│ 否
▼
从内存读 ── 本地约 80 ns / 远端约 140 ns
| 程序特征 | 受 NUMA 影响的程度 |
|---|---|
| 工作集放得进缓存 | 小 |
| 大数组顺序扫描 | 中:预取器能提前取数,但受远端带宽限制 |
| 大哈希表、大图、指针追逐 | 大:每次访问都是缓存未命中,延迟直接叠加 |
跨插槽的缓存行传递
两个线程读写同一个缓存行时,这一行要在两个核的缓存之间来回传递。两个核在不同插槽时,传递要经过互联链路,比同插槽内的传递慢。
| 场景 | 后果 |
|---|---|
| 锁、原子计数器被两个插槽上的线程争用 | 每次争用都是一次跨插槽传递 |
| 伪共享跨插槽 | 同上,且不容易从代码里看出来 |
| 队列的生产者和消费者在不同插槽 | 队列的头尾指针和数据都要跨插槽传递 |
互相通信频繁的线程放在同一个节点。
四、内存分配策略
首次触碰
malloc、mmap 返回时,内核只分配了虚拟地址,没有分配物理页。物理页在第一次写入时才分配,这次写入触发缺页异常,内核在当时运行这个线程的核所在的节点上取一页。这个默认行为叫首次触碰(first-touch)。
sequenceDiagram
participant T as 线程(运行在节点 1 的核上)
participant K as 内核
T->>K: mmap 1 GB
K-->>T: 返回虚拟地址,未分配物理页
T->>K: 第一次写某一页,触发缺页异常
Note over K: 查内存策略:默认是本地分配
Note over K: 在节点 1 上分配一页
K-->>T: 建立映射,写入继续
页一旦分配,位置就固定了。线程之后被调度到别的节点,页不会跟着走(自动 NUMA 均衡开启时除外,见第八节)。
策略一览
| 策略 | 行为 | 指定节点内存不足时 |
|---|---|---|
MPOL_DEFAULT |
沿用上一级的策略;最上一级是本地分配 | 退到其他节点 |
MPOL_LOCAL |
在当前核所在的节点分配 | 退到其他节点 |
MPOL_PREFERRED |
优先在指定的一个节点分配 | 退到其他节点 |
MPOL_PREFERRED_MANY(5.15 起) |
优先在指定的一组节点分配 | 退到其他节点 |
MPOL_BIND |
只在指定的节点分配 | 回收或换出本节点的页;仍不够则触发 OOM |
MPOL_INTERLEAVE |
按页在指定的节点之间轮流分配 | 跳到下一个节点 |
MPOL_WEIGHTED_INTERLEAVE(6.9 起) |
按各节点的权重轮流分配 | 跳到下一个节点 |
策略分三级,越具体的优先级越高:
系统默认 ← 线程策略(set_mempolicy、numactl) ← 地址范围策略(mbind)
MPOL_BIND 的风险
节点 0:已用满 节点 1:空闲 100 GB
│
▼
进程的策略是 bind 到节点 0,继续申请内存
│
▼
内核在节点 0 上回收页缓存、换出匿名页 → 延迟飙升
│
▼
仍不够 → OOM,尽管整机还有 100 GB 空闲
不确定内存用量时用 MPOL_PREFERRED:平时在指定节点分配,不够时退到其他节点。
节点内存不足时先回收还是先去别的节点
由 vm.zone_reclaim_mode 决定:
| 值 | 行为 | 适用 |
|---|---|---|
| 0(多数发行版的默认值) | 本节点不够时直接去其他节点分配 | 通用 |
| 非 0 | 先在本节点回收页缓存,回收不出来再去其他节点 | 对本地性要求极高、能接受回收造成的停顿 |
五、首次触碰的陷阱
主线程初始化了全部数据
std::vector<double> ; // 构造函数把 n 个元素置 0,全部页由主线程触碰
;
主线程在节点 0 上运行,data 的所有页都落在节点 0。节点 1 上的工作线程处理自己那一半数据时,全部是远端访问。
容易在主线程里触碰全部页的写法:
| 写法 | 原因 |
|---|---|
std::vector<T> v(n) |
值初始化,逐个元素写 0 |
memset(p, 0, n) |
直接写 |
new T[n]() |
带括号,值初始化 |
| 读文件到缓冲区 | read 把数据写进缓冲区 |
不会触碰的写法:
| 写法 | 原因 |
|---|---|
mmap 匿名映射 |
只分配虚拟地址 |
malloc 大块内存 |
glibc 对大块直接用 mmap |
calloc 大块内存 |
glibc 知道 mmap 来的页本来就是 0,不再写一遍(实现相关) |
std::make_unique_for_overwrite<T[]>(n)(C++20) |
默认初始化,平凡类型不写入 |
做法:谁用谁初始化
// numa_first_touch.cpp
static void
int
// g++ -O2 -std=c++17 -pthread numa_first_touch.cpp
顺序不能反:先绑核,再触碰。线程没绑核时可能在任意节点上运行,触碰的页落在哪个节点不确定。
内存复用
线程 A(节点 0):p = malloc(64),写入 → 物理页落在节点 0
线程 A: free(p) → 内存回到分配器,物理页仍在节点 0
线程 B(节点 1):q = malloc(64) → 分配器可能把同一块内存交给 B
线程 B: 读写 q → 远端访问
释放的内存回到分配器之后,物理页的位置不变。它再被分给另一个节点上的线程时,首次触碰早已发生过。
| 缓解办法 | 说明 |
|---|---|
| 每线程一个对象池 | 对象只在本线程内分配和释放 |
| 使用带线程缓存的分配器 | glibc 的 per-thread arena、tcmalloc、jemalloc 都让线程优先复用自己释放的内存(实现相关) |
| 跨线程传递的对象由接收方归还给发送方 | 避免内存在节点之间流动 |
页缓存占满一个节点
文件的页缓存也按首次触碰分配:哪个节点上的线程先读到这一页,页缓存就放在哪个节点。
节点 0 上的进程读了一个 100 GB 的文件 → 节点 0 的内存被页缓存占满
之后节点 0 上的线程申请匿名内存 → 本节点没有空闲页,退到节点 1 → 远端访问
| 做法 | 说明 |
|---|---|
| 启动关键进程之前清页缓存 | sync; echo 1 > /proc/sys/vm/drop_caches |
| 读大文件的进程用交错策略 | numactl --interleave=all,页缓存均摊到各节点 |
| 关键进程预先分配并锁住内存 | 启动时触碰全部页,再 mlockall |
六、命令行控制
numactl
| 命令 | 作用 |
|---|---|
numactl --hardware |
查看拓扑 |
numactl --show |
查看当前 shell 的策略 |
numactl --cpunodebind=0 --membind=0 ./app |
线程只在节点 0 的核上运行,内存只从节点 0 分配 |
numactl --cpunodebind=0 --preferred=0 ./app |
同上,但内存不够时可以去其他节点 |
numactl --physcpubind=2,3 --localalloc ./app |
线程只在 2、3 号核上运行,内存在本地分配 |
numactl --interleave=all ./app |
内存按页在所有节点之间轮流分配 |
--cpunodebind 和 --membind 要成对使用:
| 只指定 | 结果 |
|---|---|
--cpunodebind=0 |
线程在节点 0;内存按首次触碰也落在节点 0;节点 0 不够时退到节点 1 |
--membind=0 |
内存在节点 0;线程可能被调度到节点 1,变成远端访问 |
| 两者都指定 | 线程和内存都在节点 0 |
绑定还是交错
| 策略 | 适用 | 不适用 |
|---|---|---|
| 绑定到一个节点 | 进程的内存和线程都放得进一个节点 | 内存用量超过单节点容量 |
| 交错 | 一个大进程用掉大部分内存,线程分布在所有节点,访问模式随机(如数据库缓冲池) | 线程与数据有明确的对应关系 |
| 按线程各自首次触碰 | 每个线程处理固定的一块数据 | 数据在线程之间流动 |
交错让每个线程的访问有 1/N 是本地、其余是远端,平均延迟高于全本地,但各节点的内存占用均衡,不会出现一个节点耗尽。
taskset
taskset 只控制线程能在哪些核上运行,不控制内存:
cpuset
cgroup 的 cpuset 控制器同时限制核和内存节点,容器运行时用它实现绑定:
# cgroup v2
systemd
[Service]
CPUAffinity=0-15
NUMAPolicy=bind
NUMAMask=0
迁移已有的页
迁移期间访问这些页的线程会被阻塞,不在延迟敏感的时段执行。
七、程序内控制
线程亲和
void
线程启动后第一件事就是绑核,之后再分配和触碰内存。
libnuma
// numa_alloc.cpp
int
// g++ -O2 numa_alloc.cpp -lnuma 需要 libnuma 的开发包
| 函数 | 作用 |
|---|---|
numa_available() |
小于 0 表示不可用 |
numa_max_node() |
最大的节点编号 |
numa_node_of_cpu(cpu) |
某个核属于哪个节点 |
numa_run_on_node(node) |
线程限制在某个节点的核上 |
numa_alloc_onnode(len, node) |
分配内存并把策略设为指定节点 |
numa_alloc_interleaved(len) |
分配内存并设为交错 |
numa_alloc_local(len) |
分配内存并设为本地分配 |
numa_set_preferred(node) |
设置线程的优先节点 |
numa_alloc_* 每次调用都走 mmap,按页取整,比 malloc 慢。它用于一次性分配大块内存,不用于频繁的小对象分配。
mbind:对已有的地址范围设策略
// 把 [addr, addr + len) 绑定到 node;已经分配在别处的页一并迁过来
bool
// g++ -O2 -c bind_range.cpp;链接时加 -lnuma
| 标志 | 作用 |
|---|---|
| 不带标志 | 只影响之后新分配的页 |
MPOL_MF_MOVE |
把范围内已分配、且只被本进程映射的页迁到目标节点 |
MPOL_MF_STRICT |
有页不符合策略且迁不动时返回错误 |
addr 要按页对齐。
查询一页在哪个节点
// 返回 addr 所在页的节点编号;页还没分配时返回负值
int
// g++ -O2 -c node_of.cpp;链接时加 -lnuma
这个函数可以放在单元测试里,验证关键数据结构确实落在预期的节点。
系统调用一览
| 系统调用 | 作用范围 | 用途 |
|---|---|---|
sched_setaffinity |
线程 | 限定可运行的核 |
set_mempolicy |
线程 | 设置之后新分配的页的策略 |
get_mempolicy |
线程或地址 | 查询策略或页所在节点 |
mbind |
地址范围 | 设置策略,可选迁移已有的页 |
move_pages |
指定的页 | 迁移或查询 |
migrate_pages |
整个进程 | 把一组节点上的页迁到另一组节点 |
八、自动 NUMA 均衡
内核自动把页迁到访问它的线程所在的节点,或把线程迁到它的内存所在的节点。程序不需要做任何绑定。
工作过程
flowchart TD
A["内核周期性扫描进程的一部分地址空间"] --> B["把扫到的页表项改成不可访问"]
B --> C["线程访问这些页,触发提示缺页"]
C --> D["内核记录:哪个节点上的线程访问了哪个节点上的页"]
D --> E{"页和访问它的线程在不同节点,且连续两次都是同一节点来访问?"}
E -- 是 --> F["把页迁到访问者所在的节点"]
E -- 否 --> G["恢复页表项,不迁移"]
D --> H["调度器参考统计结果,把线程往它的内存所在的节点迁"]
代价
| 开销 | 来源 |
|---|---|
| 扫描 | 周期性遍历页表 |
| 提示缺页 | 每个被扫到的页下一次被访问时进一次内核 |
| 迁移 | 拷贝页内容、改页表、刷新 TLB |
| 延迟抖动 | 以上三项都发生在业务线程的执行路径上,时间点不可控 |
开还是关
| 场景 | 建议 | 原因 |
|---|---|---|
| 通用服务、没有做过任何绑定 | 开 | 自动改善本地性,不用改程序 |
| 已经手工绑定线程和内存 | 关 | 位置已经正确,扫描和缺页是纯开销 |
| 低延迟程序 | 关 | 不能接受不可控的缺页和迁移 |
| 内存被多个节点上的线程共同访问(如数据库缓冲池) | 关 | 页在节点之间来回迁移,没有稳定的归属 |
观察它做了多少事
| 计数器 | 含义 |
|---|---|
numa_pte_updates |
被改成不可访问的页表项数量 |
numa_hint_faults |
提示缺页的次数 |
numa_hint_faults_local |
其中访问者与页在同一节点的次数 |
numa_pages_migrated |
迁移的页数 |
numa_hint_faults_local 占 numa_hint_faults 的比例接近 1 时,本地性已经很好。
九、设备与中断
设备所在的节点
PCIe 设备挂在某一个插槽的总线上,它的 DMA 缓冲区、中断处理放在同一个节点时路径最短。
收包路径上的三个位置
网卡(节点 0) → 中断和软中断(在哪个核上处理?) → 业务线程(在哪个核上运行?)
│ │
包缓冲区在这里分配 读包数据、写应用缓冲区
| 位置 | 放在网卡所在节点的办法 |
|---|---|
| 中断 | /proc/irq/<n>/smp_affinity_list 写入同节点的核;停用 irqbalance 或为它配置排除规则 |
| 业务线程 | numactl --cpunodebind 或程序内绑核 |
| 内存 | --membind / --preferred,或靠首次触碰 |
大页按节点预留
显式大页的数量是每个节点单独计数的。进程绑定在节点 0 时,只能用到节点 0 上预留的大页。
通过 /proc/sys/vm/nr_hugepages 设置的总数会平均分到各节点。绑定到单节点的进程只能用到其中一部分,要按节点单独设置。
十、典型应用的做法
| 应用 | 特征 | 做法 |
|---|---|---|
| MySQL InnoDB | 缓冲池占整机大部分内存,线程分布在所有节点 | innodb_numa_interleave=ON,缓冲池交错分配 |
| Redis | 单线程处理命令,内存通常放得进一个节点 | numactl --cpunodebind=N --membind=N 绑到一个节点 |
| JVM | 堆很大,线程很多 | -XX:+UseNUMA:每个节点一块分配区,线程在本节点的区里分配对象 |
| DPDK | 每个核处理一个网卡队列 | 内存池建在网卡所在节点(rte_eth_dev_socket_id),工作线程用同节点的核,--socket-mem 按节点预留大页 |
| 低延迟交易 | 线程少,延迟和抖动优先 | 全部放在网卡所在的节点,见下表 |
| 多实例部署 | 一台机器跑多个相同的进程 | 每个实例绑一个节点,实例之间互不干扰 |
数据库为什么用交错
默认策略下启动数据库,缓冲池 200 GB,每节点内存 128 GB:
节点 0:先被占满 节点 1:还有大量空闲
│
▼
节点 0 上其他的内存申请 → 内核在节点 0 上回收和换出 → 数据库的页被换到磁盘 → 查询变慢
交错分配让缓冲池均摊到两个节点,各占 100 GB,两个节点都留有余量。
低延迟交易系统
| 项目 | 做法 |
|---|---|
| 选节点 | 网卡所在的节点 |
| 核 | 在该节点上隔离若干个核(isolcpus、nohz_full、rcu_nocbs),关键线程一对一绑定 |
| 内存 | --membind 到该节点;启动时触碰全部内存,mlockall 锁住 |
| 大页 | 在该节点上单独预留 |
| 中断 | 网卡中断固定到该节点上非关键线程的核 |
| 自动 NUMA 均衡 | 关闭 |
| 另一个节点 | 留给日志、监控、运维进程 |
容器
Kubernetes 的 kubelet 有三个相关组件:
| 组件 | 作用 |
|---|---|
CPU Manager(static 策略) |
给申请整数个 CPU 的 Guaranteed 容器分配独占的核 |
| Memory Manager | 给容器的内存指定节点 |
| Topology Manager | 协调前两者和设备插件;single-numa-node 策略要求 CPU、内存、设备都在同一节点,否则拒绝调度到本机 |
虚拟机
| 问题 | 做法 |
|---|---|
| 虚拟机看到的拓扑与物理机不一致 | 配置 vNUMA,让虚拟节点对应物理节点 |
| vCPU 在物理核之间漂移 | 把 vCPU 固定到物理核 |
| 虚拟机的内存跨了物理节点 | 把虚拟机的内存绑到对应的物理节点 |
十一、观测与排查
numastat
不带参数时输出的六个计数器:
| 计数器 | 含义 |
|---|---|
numa_hit |
想在本节点分配,并且分到了 |
numa_miss |
页分在了本节点,但申请者原本想要的是别的节点 |
numa_foreign |
申请者想要本节点,结果分到了别的节点 |
interleave_hit |
交错策略按计划分到了本节点 |
local_node |
分配时申请者正运行在本节点 |
other_node |
分配时申请者运行在别的节点 |
numa_miss 和 numa_foreign 持续增长,说明有节点内存不足,分配在向其他节点溢出。
numa_maps
输出样例:
7f3c00000000 default anon=262144 dirty=262144 N0=131072 N1=131072 kernelpagesize_kB=4
7f3d00000000 bind:0 anon=524288 dirty=524288 N0=524288 kernelpagesize_kB=4
| 字段 | 含义 |
|---|---|
| 第一列 | 这段映射的起始地址 |
default、bind:0、interleave:0-1 |
这段地址的内存策略 |
anon= |
匿名页的数量 |
N0=、N1= |
分别落在节点 0、节点 1 上的页数 |
kernelpagesize_kB= |
页大小 |
第一行的映射有一半的页在节点 1。进程只在节点 0 上运行时,这一半就是远端访问。
硬件计数器与采样
node-load-misses 是落到远端节点的访存次数。这两个事件是否可用取决于 CPU 型号。
perf mem report 按数据来源分类,其中 Remote RAM 是远端内存访问,可以定位到具体的函数和数据地址。
其他工具:
| 工具 | 看什么 |
|---|---|
numatop |
每个进程和线程的本地访问次数、远端访问次数及其比值 |
perf c2c |
被多个核争用的缓存行,包括跨节点的争用 |
/proc/vmstat 里的 numa_* |
自动 NUMA 均衡的活动量 |
排查流程
flowchart TD
A["怀疑有 NUMA 问题:吞吐上不去,或加核之后反而变慢"] --> B["numactl --hardware:机器有几个节点?"]
B -- "1 个" --> Z["与 NUMA 无关,查别的"]
B -- "多个" --> C["numastat -p:进程的内存分布在哪些节点?"]
C --> D["线程运行在哪些核上? ps -L -o pid,tid,psr,comm"]
D --> E{"线程所在节点与内存所在节点一致?"}
E -- 是 --> F["perf c2c:有没有跨节点争用的缓存行?"]
E -- 否 --> G{"数据由谁初始化?"}
G -- "主线程" --> H["改成由使用它的线程初始化"]
G -- "使用它的线程" --> I["线程没绑核,触碰之后被迁走了 → 绑核"]
C --> J{"某个节点的空闲内存接近 0?"}
J -- 是 --> K["查页缓存和其他进程的占用;考虑交错或 preferred"]
十二、面试速答卡
NUMA 是什么 每个插槽自带内存控制器和本地内存,核访问本节点的内存快,访问其他节点的内存要经过互联链路,更慢。
为什么不用 UMA 所有 CPU 共用一条总线和一个内存控制器,CPU 越多争用越严重,总带宽不增长。NUMA 每加一个插槽多一组内存通道。
远端访问慢多少 延迟约为本地的 1.5 到 2 倍,带宽也更低。只有未命中缓存的访问才受影响,大哈希表和指针追逐这类程序最明显。
内核默认把页放在哪 首次触碰:页在第一次被写入时分配,落在当时运行这个线程的核所在的节点。
首次触碰最常见的问题 主线程初始化了全部数据,页都落在主线程所在的节点,其他节点上的工作线程全是远端访问。做法是先绑核,再由使用数据的线程自己初始化。
--cpunodebind 和 --membind 的区别
前者限制线程能在哪些核上运行,后者限制内存从哪些节点分配。只用后者时线程可能跑到别的节点,变成远端访问。
bind 和 preferred 的区别
指定节点内存不足时,bind 在本节点回收、换出,仍不够就 OOM;preferred 退到其他节点分配。
什么时候用交错 一个大进程占用大部分内存、线程分布在所有节点、访问模式随机时,例如数据库缓冲池。交错避免一个节点先耗尽。
自动 NUMA 均衡是什么,什么时候关 内核周期性把页表项改成不可访问,靠缺页统计谁在访问,再迁移页或线程。已手工绑定的程序和低延迟程序要关掉,扫描、缺页和迁移都会造成抖动。
整机还有空闲内存,为什么进程被换出或 OOM
进程的策略是 bind 到某个节点,而这个节点满了。或者一个节点被页缓存占满。
网卡和 NUMA 的关系
网卡挂在某一个插槽上。中断、业务线程、包缓冲区都放在网卡所在的节点,路径最短。节点号在 /sys/class/net/<dev>/device/numa_node。
怎么确认程序有 NUMA 问题
numastat -p 看内存分布,ps -L -o psr 看线程所在的核,两者不一致就有远端访问。perf mem 或 numatop 给出远端访问的占比。
单插槽的机器需要关心 NUMA 吗
只显示一个节点时不需要。AMD EPYC 和开启了 Sub-NUMA Clustering 的至强在单插槽上也会呈现多个节点,以 numactl --hardware 的输出为准。
暂无评论,欢迎留下第一条评论。