K10 —— NUMA 系统:内存节点和本地分配

金句:在多路服务器上,”本地内存”和”远程内存”的访问延迟可以相差 3 倍——NUMA 就是这个差异的根源。


NUMA 的概念

NUMA(Non-Uniform Memory Access,非均匀内存访问)是指多路服务器中,每个 CPU 核心有自己”本地”的内存,访问本地内存比访问其他 CPU 的内存快。

Bash
NUMA 系统示例(2 路服务器):

CPU 0 ── 本地内存 64GB(节点 0)        CPU 1 ── 本地内存 64GB(节点 1)
  │                                        │
  └── 访问延迟 60ns(本地)                  └── 访问延迟 60ns(本地)
  └── 访问延迟 100ns(跨节点)                └── 访问延迟 100ns(跨节点)

NUMA 的核心问题:
  - 本地内存访问快(60ns)
  - 远程内存访问慢(100ns)
  - 如果分配策略不对,程序会变慢

UMA(Uniform Memory Access):
  - 所有内存统一访问
  - 多核通过总线访问同一个内存
  - 访问延迟相同(但总线是瓶颈)

NUMA 节点和内存布局

Bash
NUMA 节点(Node)的定义:

每个节点包含:
  - 1~N 个 CPU 核心
  - 本地内存(DRAM)
  - 本地 PCI/IO 设备

Linux 用 struct pglist_data 表示 NUMA 节点:

typedef struct pglist_data {
    int node_id;              // 节点 ID(0, 1, ...)
    unsigned long node_start_pfn;  // 起始物理页号
    unsigned long node_present_pages;  // 节点总页数
    pg_data_t *node_zones[MAX_NR_ZONES];  // zone(DMA/Normal/HighMem)
    struct zoneref *node_zonelists;  // 备用节点列表
    // ...
} pg_data_t;

查看 NUMA 布局:
  $ numactl --hardware
  available: 2 nodes (0-1)
  node 0 size: 65536 MB
  node 1 size: 65536 MB
  node 0 free: 12345 MB
  node 1 free: 23456 MB

  $ cat /proc/buddyinfo
  Node 0, zone      Normal    free pages 12345/67890
  Node 1, zone      Normal    free pages 23456/78901

NUMA 感知分配

Bash
Linux 默认的 NUMA 分配策略:

1. First Touch(首次触碰):
   - 页面在哪第一次访问,就在哪个节点分配
   - 线程在 Node 0 访问,内存就分配在 Node 0
   - 这是默认策略

2. interleaved(交错分配):
   - 轮询在所有节点分配
   - 用于避免单节点内存耗尽

3. membind(绑定到节点):
   - 绑定到特定节点
   - 强制在该节点分配

numactl 控制分配策略:

$ numactl --localalloc ./program
  → 本地分配(默认)

$ numactl --interleave=all ./program
  → 交错分配(所有节点均匀)

$ numactl --membind=0 ./program
  → 强制在 Node 0 分配

$ numactl --cpunodebind=0 --membind=0 ./program
  → 在 Node 0 的 CPU 上运行,内存也在 Node 0

NUMA API:mbind 和 set_mempolicy

Bash
C 程序控制 NUMA 分配:

#include <numaif.h>

// 方法 1:mbind(单个内存范围)
struct bitmask *nodemask = numa_allocate_nodemask();
numa_bitmask_setbit(nodemask, 0);  // 节点 0

mbind(addr, size, MPOL_BIND, nodemask->maskp, nodemask->size, 0);
numa_free_nodemask(nodemask);

// 方法 2:set_mempolicy(全局策略)
struct bitmask *nodemask = numa_allocate_nodemask();
numa_bitmask_setbit(nodemask, 0);
set_mempolicy(MPOL_BIND, nodemask->maskp, nodemask->size);
numa_free_nodemask(nodemask);

// 方法 3:alloc_pages(直接分配)
struct page *page = alloc_pages_node(node_id, GFP_KERNEL, order);
// 或者
alloc_pages(GFP_KERNEL, order);  // 自动 first-touch

MPOL_* 策略:
  MPOL_DEFAULT:系统默认(first-touch)
  MPOL_BIND:严格绑定到指定节点
  MPOL_INTERLEAVE:交错分配
  MPOL_PREFERRED:优先节点,回退到其他

NUMA 的内存回收和迁移

Bash
NUMA 内存回收的特点:

1. 每个节点独立的水位线:
   - kswapd 运行在每个节点上
   - 每个节点独立回收

2. 跨节点回收:
   - 当本地节点内存紧张时
   - 可以从远程节点回收(如果页面可迁移)

3. 内存迁移(migration):
   - 把页面从节点 A 移动到节点 B
   - move_pages(2) 系统调用
   - 成本高(复制 + 更新页表)

迁移场景:
  - 进程绑定了 CPU(cpuset)
  - 但内存不够,需要迁移到另一个节点
  - 或者 NUMA 平衡器(numa_balancing)自动迁移

numa_balancing(自动 NUMA 优化):
  - 内核线程 numa_balancing
  - 监控进程的 NUMA 访问模式
  - 如果发现访问和分配不在同一节点,迁移页面
  - 透明,不需要应用修改

总结

  • NUMA:Non-Uniform Memory Access,多路服务器中本地内存快,远程内存慢(60ns vs 100ns)
  • 节点:每个 CPU + 本地内存 = 一个 NUMA 节点,用 struct pglist_data 表示
  • First Touch:默认策略,页面在首次访问的节点分配
  • numactl:–localalloc/–interleave=all/–membind=0 控制分配策略
  • NUMA API:mbind / set_mempolicy / alloc_pages_node 控制分配
  • NUMA 平衡器:numa_balancing 自动迁移页面到访问节点

K 层总结:K01~K10 覆盖了 SLAB/SLUB 分配器 → VMA/地址空间 → COW 复制 → OOM Killer → Page Cache → mmap/demand paging → malloc/brk/ptmalloc → shm共享内存 → 内存泄漏 → NUMA 的完整链路。

下篇预告(K11):buddy system 是怎么分配物理页的?alloc_pages 的完整路径,以及物理内存管理的边界情况。


关注公众号「AI不着急」,回复”资料”获取内存学习路线图。

最后修改: 2024年10月16日

作者

评论

发表评论

您的邮箱地址不会被公开。