Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

NUMA 与 CXL 内存池

一句话

你看过 64 核的服务器跟普通桌面机 CPU 长得一样,但物理上 64 个核分成 2-8 个 socket,每个 socket 自己独享 1-4 TB 内存,跨 socket 访问那条"QPI/UPI / Infinity Fabric"链要 100-200ns 等 — 比本机内存慢两到四倍。这就是 NUMA。在 NUMA 机器上跑一个不知道 NUMA 概念的服务(如 Redis),P99 可能比理论值高 3 倍。理解了 NUMA 后,再看 CXL 这种新硬件,本质是"把 NUMA 横向伸到机柜"——同一个抽象在网络层重新物化。

1. UMA → NUMA 的物理演进

岁月静好 UMA 模型:

所有 CPU 共享一条前端总线 → 同一条 DRAM 总线 → 所有 CPU 看内存延迟一致

但当 CPU 核数从 4 → 16 → 64,总线带宽抢不过来了,必须分簇

Socket 0: 32 核 + 1 TB DRAM  ─ hyperlink ┘
                                │       ← 100-200 ns cross-socket
Socket 1: 32 核 + 1 TB DRAM  ─ hyperlink ┘

每个 socket 自己内存达"近",跨 socket 远。这就是 NUMA = Non-Uniform Memory Access.

2. 物理延迟常数表

本地 DRAM 访问      80-100 ns
跨 socket UPI      + 100-150 ns
跨 2 跳 NUMA       + 200 ns
HBM (本地 fusion)   50-100 ns
CXL.mem 直连        170-250 ns (新一代 < 200 ns)
RDMA 同机柜         2-5 μs

跨 socket 延迟接近翻倍——这就是 NUMA 不被知道的代价。

3. Linux NUMA 工具入口

# 查 NUMA 拓扑
numactl --hardware
lscpu = NUMA node(s):
# 一般 1-socket systems: NUMA nodes = 1
# 2-socket servers: 2 NUMA nodes
# AMD Epyc: 4 / 8 NUMA nodes per socket (NPS4)

# 进程当前 NUMA 分布
cat /proc/<pid>/numa_maps
numastat -p <pid>

在 SYstemD 里:

# 启动绑到第一个 node
numactl --cpunodebind=0 --membind=0 ./yourapp

# 或更精细, bind CPU list:
numactl --physcpubind=0-15 --membind=0 ./yourapp

4. NUMA 调优真实工程模式

模式 1:进程隔离

32 核服务器跑两个高内存服务,绑两个 NUMA node:

numactl --cpunodebind=0 --membind=0 service_a &
numactl --cpunodebind=1 --membind=1 service_b &

每个服务仅用本地节点内存,跨 socket 0 次。

模式 2:NUMA-aware 分配 libnuma

#include <numa.h>
struct bitmask *mask = numa_allocate_nodemask();
numa_bitmask_setbit(mask, 0);
numa_set_membind(mask);
void *p = numa_alloc_onnode(1024*1024, 0);

C / C++ 应用直接 libnuma 控制内存位置.

模式 3:线程 + CPU 共同绑定

更近一步: 用 pthread_setaffinity_np 把线程粘在 CPU 上, 同时把内存放本地 node. 这个组合通常能拿到 P99 1/3 的延迟下降 — 这一招是高频交易 / 大数据服务的标配.

模式 4:avoid remote alloc

Linux 默认 first-touch policy: 第一次写入页的 CPU 决定页落在哪. 所以初始化大数组时写它的线程就决定了它的 NUMA 归属. 程 service startup 时有个 worker 提前 touch 全部内存, 后面 worker 共享该内存经常跑跨 socket :

#pragma omp parallel for
for (int i = 0; i < N; i++) a[i] = 0;        // 每线程 touch 局部 → 跨 NUMA 拆分

默认 OpenMP 有 first-touch 也是这种语义. 但手动管理时容易踩坑.

5. Redis 案例: NUMA 不知道就 1.5-3× 慢

实验对比 (8-socket 服务器, 200 GB Redis Cache):

  • 默认启动 Redis: QPS ~ 220k, P99 ~3 ms;
  • numactl → 单 node 绑核: QPS ~ 450k, P99 ~ 1 ms;
  • 双 NUMA 分两个 Redis 实例 + 路由: QPS ~ 900k 加起来, P99 ~0.5 ms.

在不改 Redis 源码的情况下, 仅用 NUMA 工具拿到的 1.5-3× 加速. 这是工程上"懂得硬件 = 直接升职" 的 Classic.

6. 多线程同步原语在 NUMA 下的代价

pthread_mutex 不是公平的: 不是 FIFO, 不是 LIFO, Linux 内核 futex 是 task-fair spinlock, 但 lock handoff across NUMA 拥有时间成本:

同 socket thread 互相抢: ~ 100 ns (L3 hit)+ wakeup;
cross-socket thread 抢: 200-500 ns (UPI latency) + wakeup;

NUMA topology-aware spinlock/sh mutex 在 Linux 上叫 NUMA-aware lock, 各种 lock ticker 包括 futex 改造能用 MCS lock, TTAS w backoff, CNA lock 之类减少 cross-socket. Linux qspinlock 加强 num-aware since 5.x.

业务代码 → 拆线程 → 一致在 socket 内做 sync. SHard data → per-socket storage.

// 反例:
static mutex m;
void run() {
    lock_guard<mutex> g(m);
    ...                                  // 多线程从 2 个 socket 同时进入 = 跨 socket 抢锁
}

// 正例:
per_socket mutex m[N_SOCKETS];
void run(int s) {
    lock_guard<mutex> g(m[s]);
    ...                                  // 同 socket 线程抢同 / 同 socket 锁
}

7. PGAS / DSM 的历史

把"跨 NUMA 内存" 推到极致就是 DSM (Distributed Shared Memory) — 用网络硬件支持 CPU 远程访问其他机内存. 90 年代 Stanford DASH / Flash (Dash-Flash), MIT Alewife 都是这条路. 但网络延迟微秒级远超 DRAM 纳秒级, 实操成本高, 商业上 JPG.

但 GPU 一脉/CXL 一脉把这条路拖回来了:

  • NVLink 4.0 → 400 GB/s GPU-GPU direct peer access;
  • CXL 2.0 → CPU ↔ device 一致缓存, host peer memory;
  • CXL.mem → 远地 DRAM 当作本地但延迟 ~170-250 ns, 类似 cross-socket NUMA.

8. CXL: 在同一台机柜里的 NUMA 延伸

CXL 是基于 PCIe 5 的 cache-coherent 互连协议, 看上去是 NUMA 的延续:

传统 NUMA:   CPU A ↔ UPI ↔ CPU B (本地 DRAM)
CXL:         CPU A ↔ CXL.mem ↔ 远端内存池
CXL.cache:   CPU ↔ CXL device (NIC / 加速器) 共享 cache 一致性

CXL.mem 让一台 4 TB 内存的服务器把"远端 1 TB 内存池"视为远端 NUMA node 4. ** numa 工具、 numactl --cpunodebind 全都直接适用**.

工程意义:

  • 内存池化 → 多台 server 共享一个 DRAM pool, 业务 server 内存按需扩 out. 冷启动快几十 GB;
  • 持久内存 → 把 PMem/NVDIMM 放到远端;
  • 加速器互连 (如 GPU 内存) 借助 CXL 互通, 互访延迟 ms 级 → μs 级.

这个时代从 2024-2025 开始部署最早, GC + Redis + KV 仓库陆续适配. NUMA 知识直接迁移过去.

9. NUMA 与分配器的协同

jemalloc 内置 NUMA 感知: arena 数 = NUMA nodes × 4, 内部 init 时绑定 arena → node. 这是分配器与 NUMA 同源工程示例:

  • arena 0-N 在 Node 0 上 alloc 页;
  • arena N-2N 在 Node 1 上 alloc 页;
  • thread x → arena hash → 选择本地 node;

这是 NUMA + allocator 的"两个抽象层共同优化 cache 行为" 的同构案例.

10. 多语言同一抽象

语言NUMA 触达备注
C / C++libnuma + first-touch最底层
Rustlibnuma-bind + jemalloc高度可定制
Goruntime.LockOSThread + cgo libnuma没有 first-touch 自动
Java-XX:+UseNUMA + JEP 369 (Hotspot, GCCore)JVM 把 heap per node
Python多进程 + numactl默认单核绑核

所有语言在 NUMA 机器上都有办法适配, 但需要显式配置, 否则默认行为不 NUMA-aware.

11. 这章带走的东西

  • NUMA 是 multi-socket 服务器的常态, 默认行为不优化, 跨 socket 延迟翻 2-4×;
  • 思维: 同 socket 内 才应该共享 data / lock; 跨 socket 用分多实例 / partition / NUMA-aware 数据结构
  • 工具链: numactl + libnuma + numastat 直接可用, 不需修改 language runtime;
  • CXL 是 NUMA 互连在网络层扩展, 同抽象可拓到机柜级;
  • allocator (jemalloc) 内嵌 NUMA arena 是 OS 层 + 运行时层共同优化的同构示例.

下一节 → 文件系统