第七十三章:NUMA
元婴后期涉及内核源码:
一
林小源在内景中走了很远,走到了一片他从未见过的地貌。
面前是两座巨大的浮岛,悬浮在虚空中,相隔数百丈。每座浮岛上面都有几颗明亮的光球在运转——那是 CPU。浮岛的内部则是一片温暖的金色光芒——那是本地内存。
两座浮岛之间,有一条细细的光桥相连。光桥上偶尔有数据流过,但流速明显比浮岛内部慢得多。
"这是什么地方?"
"NUMA 域。" mm_struct 的声音从左边的浮岛上传来,"Non-Uniform Memory Access。你看到的每座浮岛是一个 NUMA 节点。节点内的 CPU 访问节点内的内存——快。跨节点访问——慢。"
林小源跳上左边的浮岛。脚下的地面温暖而坚实,每一步都能感觉到内存的存在——密集的、均匀的、触手可及。
"这个节点有四个 CPU," mm_struct 说,"本地内存延迟大约 100 纳秒。"
林小源走到浮岛的边缘,望向对面的浮岛。那座浮岛看起来和这边一模一样——四个 CPU,一片金色内存。但两者之间的光桥上,数据流动得缓慢而吃力。
"跨节点访问呢?"
"150 到 300 纳秒。" mm_struct 说,"数据要经过互联总线,从一个节点传到另一个节点。距离越远,延迟越高。"
林小源试着用意念触碰对面浮岛上的内存。他感觉到了——一种遥远的、模糊的、像隔着一层雾的感觉。和触碰本地内存完全不同。本地内存是"近"的、清晰的、立即响应的;远程内存是"远"的、延迟的、需要等待的。
"近和远在内存中也是相对的——他忽然明白了这个道理。"
"不是相对的。" mm_struct 纠正他,"是绝对的。物理距离就是物理距离——数据在导线上传播需要时间。你可以忽略这个距离,但你的程序不会。"
二
林小源在左边的浮岛上走了一圈,注意到一个奇怪的现象。
有些区域的内存特别"热"——频繁被访问,光芒耀眼。有些区域则冷冰冰的,很少被触碰。而那些热区的位置……似乎在缓慢地移动。
"那些热区是什么?"
"你的工作集。" mm_struct 说,"你频繁访问的内存页面。它们目前都在本地节点上——这是好事。但如果你的进程被调度到了对面的 CPU 上——"
林小源顺着 mm_struct 的指引看向光桥。他明白了——如果他被调度到对面的浮岛,那些热区就变成了远程内存。每次访问都要走光桥,延迟翻倍。
"所以 CPU 和内存的位置关系很重要?"
"非常重要。" mm_struct 说,"这就是为什么有内存策略。"
一个穿着灰色长袍的结构体从浮岛中央走过来。它的身形比 mm_struct 更瘦削,走路的姿态像一个谨慎的管家。
" 。"它自我介绍,声音平稳而精确,"内存策略管理器。我的工作是决定——内存分配在哪个节点上。"
"有哪些策略?"林小源问。
mempolicy 伸出四根手指。
"第一, ——在当前节点分配。最简单,也最常用。你的进程跑在哪个 CPU 上,就在哪个节点分配内存。"
"第二, ——只在指定节点分配。如果那些节点的内存用完了——不回退,直接报错。严格,但可预测。"
"第三, ——在多个节点间轮转分配。第一个页面在节点 0,第二个在节点 1,第三个又回节点 0……像织布一样交错。适合大块内存访问,分散带宽压力。"
"第四, ——优先在指定节点分配。如果那个节点内存不足,可以回退到其他节点。灵活,但不保证。"
林小源想了想。"如果我的进程在两个节点之间频繁迁移呢?"
mempolicy 的眼神暗了一下。"那是最糟糕的情况。你分配的内存可能在一个节点,你运行的 CPU 在另一个节点。每次访问都是远程访问——性能灾难。"
"怎么避免?"
"绑定。" mempolicy 说,"把进程绑定到特定的 CPU,或者设置 策略强制内存在本地分配。自由是有代价的——在 NUMA 世界里,自由意味着你可能不知不觉地访问远程内存。"
三
林小源坐在浮岛的边缘,双腿悬空。
他闭上眼睛,试着感受两座浮岛之间的"距离"。在意识深处,他"看到"了——数据从本地内存出发,穿过光桥,到达对面的浮岛。每一次穿越都有延迟——不是瞬间的,而是有节奏的,像潮水一样。
"内核有自动 NUMA 平衡。" mm_struct 的声音变得柔和了一些,"它可以检测进程频繁访问的远程内存,自动把页面迁移到本地节点。"
"自动的?"
"自动的。" mm_struct 说,"内核会周期性地扫描进程的页表,标记那些被频繁访问的远程页面。然后在后台把它们迁移到本地节点——对应用程序完全透明。"
林小源睁开眼。他看到光桥上有一些光点在缓慢地移动——不是数据流,而是页面本身在迁移。一个一个地,从一座浮岛飘向另一座浮岛。
"但自动 NUMA 平衡不是万能的。" mm_struct 继续说,"如果进程的工作集太大,本地内存放不下——迁移也没用。如果进程在多个节点间均匀访问——迁移反而会浪费带宽。"
"所以还是要手动设置策略?"
"看你的情况。" mm_struct 说,"如果你知道你的应用的内存访问模式——手动设置策略更好。如果你不确定——让自动 NUMA 平衡来处理,但要监控它的效果。"
林小源站起来,看着两座浮岛之间的光桥。光桥上的光点还在缓慢迁移,像一群萤火虫在夜空中飞舞。
"他喃喃道——距离是性能的关键因素,这不是比喻,是实打实的物理约束。"
"不是因素。" mm_struct 说,"是约束。在 NUMA 世界里,你的每一步都受到物理距离的限制。你可以选择忽略它——但你的程序会为此付出代价。"
林小源点了点头。他跳下浮岛,站在两座浮岛之间的虚空中。脚下是光桥,头顶是无限的虚无。他能同时感受到两座浮岛的温度——近处的温暖,远处的微凉。
在 NUMA 的世界里,"在哪里"和"做什么"一样重要。
/*
* NUMA 架构:
*
* Node 0 Node 1
* ┌─────┐ ┌─────┐
* │ CPU │ │ CPU │
* │ 0,1 │ │ 2,3 │
* └──┬──┘ └──┬──┘
* │ │
* ┌──┴──┐ ┌──┴──┐
* │ 本地 │ │ 本地 │
* │ 内存 │ │ 内存 │
* └─────┘ └─────┘
*
* 访问延迟:
* 本地内存: ~100ns
* 远程内存: ~150-300ns
*
* 内存策略:
* MPOL_DEFAULT — 在当前节点分配
* MPOL_BIND — 只在指定节点分配
* MPOL_INTERLEAVE — 在多个节点间轮转分配
* MPOL_PREFERRED — 优先在指定节点分配
*/
printf("=== NUMA — 非一致内存访问 ===\n\n");
printf("NUMA vs UMA:\n");
printf(" UMA (统一内存访问):\n");
printf(" 所有 CPU 访问同一内存\n");
printf(" 访问延迟相同\n");
printf(" 适合小规模系统\n\n");
printf(" NUMA (非一致内存访问):\n");
printf(" 每个 CPU 有本地内存\n");
printf(" 本地访问快,远程访问慢\n");
printf(" 适合大规模系统\n\n");
printf("--- NUMA 节点示例 ---\n");
printf("Node 0: CPU 0-3, 内存 0-16GB\n");
printf(" 本地访问: ~100ns\n");
printf("Node 1: CPU 4-7, 内存 16-32GB\n");
printf(" 本地访问: ~100ns\n");
printf(" 远程访问: ~200ns\n\n");
printf("--- 内存策略 ---\n");
printf("MPOL_DEFAULT:\n");
printf(" 在当前节点分配\n");
printf(" 最简单的策略\n\n");
printf("MPOL_BIND:\n");
printf(" 只在指定节点分配\n");
printf(" 如果内存不足,不回退到其他节点\n\n");
printf("MPOL_INTERLEAVE:\n");
printf(" 在多个节点间轮转分配\n");
printf(" 适合大块内存,分散带宽压力\n\n");
printf("MPOL_PREFERRED:\n");
printf(" 优先在指定节点分配\n");
printf(" 如果内存不足,回退到其他节点\n\n");
printf("--- set_mempolicy 系统调用 ---\n");
printf("set_mempolicy(MPOL_BIND, &nodemask, maxnode);\n");
printf(" 设置当前进程的内存策略\n\n");
printf("mbind(addr, len, MPOL_INTERLEAVE, ...);\n");
printf(" 设置特定内存区域的策略\n");#include <stdio.h>
/*
* NUMA 架构:
*
* Node 0 Node 1
* ┌─────┐ ┌─────┐
* │ CPU │ │ CPU │
* │ 0,1 │ │ 2,3 │
* └──┬──┘ └──┬──┘
* │ │
* ┌──┴──┐ ┌──┴──┐
* │ 本地 │ │ 本地 │
* │ 内存 │ │ 内存 │
* └─────┘ └─────┘
*
* 访问延迟:
* 本地内存: ~100ns
* 远程内存: ~150-300ns
*
* 内存策略:
* MPOL_DEFAULT — 在当前节点分配
* MPOL_BIND — 只在指定节点分配
* MPOL_INTERLEAVE — 在多个节点间轮转分配
* MPOL_PREFERRED — 优先在指定节点分配
*/
int main() {
printf("=== NUMA — 非一致内存访问 ===\n\n");
printf("NUMA vs UMA:\n");
printf(" UMA (统一内存访问):\n");
printf(" 所有 CPU 访问同一内存\n");
printf(" 访问延迟相同\n");
printf(" 适合小规模系统\n\n");
printf(" NUMA (非一致内存访问):\n");
printf(" 每个 CPU 有本地内存\n");
printf(" 本地访问快,远程访问慢\n");
printf(" 适合大规模系统\n\n");
printf("--- NUMA 节点示例 ---\n");
printf("Node 0: CPU 0-3, 内存 0-16GB\n");
printf(" 本地访问: ~100ns\n");
printf("Node 1: CPU 4-7, 内存 16-32GB\n");
printf(" 本地访问: ~100ns\n");
printf(" 远程访问: ~200ns\n\n");
printf("--- 内存策略 ---\n");
printf("MPOL_DEFAULT:\n");
printf(" 在当前节点分配\n");
printf(" 最简单的策略\n\n");
printf("MPOL_BIND:\n");
printf(" 只在指定节点分配\n");
printf(" 如果内存不足,不回退到其他节点\n\n");
printf("MPOL_INTERLEAVE:\n");
printf(" 在多个节点间轮转分配\n");
printf(" 适合大块内存,分散带宽压力\n\n");
printf("MPOL_PREFERRED:\n");
printf(" 优先在指定节点分配\n");
printf(" 如果内存不足,回退到其他节点\n\n");
printf("--- set_mempolicy 系统调用 ---\n");
printf("set_mempolicy(MPOL_BIND, &nodemask, maxnode);\n");
printf(" 设置当前进程的内存策略\n\n");
printf("mbind(addr, len, MPOL_INTERLEAVE, ...);\n");
printf(" 设置特定内存区域的策略\n");
return 0;
}道藏笔记
内核启示
NUMA 架构中,内存访问延迟取决于位置。
NUMA 的特点:
- 每个 CPU 有本地内存
- 本地访问快,远程访问慢
- 适合大规模多处理器系统
内存策略:
- — 在当前节点分配
- — 只在指定节点分配
- — 在多个节点间轮转分配
- — 优先在指定节点分配
自动 NUMA 平衡:
- 内核检测远程内存访问
- 自动迁移页面到本地节点
- 对应用程序透明
NUMA 让"距离"成为性能的关键因素。
NUMA 之试
本章讲不同内存节点有远近之分,让距离成为性能关键因素的架构叫什么?