NUMA:核心、内存及它们之间的距离

Hacker News Top 新闻

摘要

本文解释了非统一内存访问(NUMA)的概念、历史背景以及它在多插槽服务器上对性能的影响,同时介绍了Edera在使基于Xen的虚拟化实现端到端NUMA感知方面所做的工作。

暂无内容
查看原文
查看缓存全文

缓存时间: 2026/06/29 11:03

# NUMA - 第 1 部分:核心、内存以及它们之间的距离 来源:https://edera.dev/stories/numa-part-1-cores-memory-and-the-distance-between-them 同一台宿主机上的两台虚拟机,配置完全相同,运行相同的工作负载。其中一台始终比另一台慢 20%。工作负载没问题,宿主机没问题,也没有来自其他租户的争用。慢的那台虚拟机只是恰好内存位于互联链路的“错误”一侧,而运行它的 CPU 在另一侧,并且操作员没有任何可以在客户机内部调整的旋钮来修复它。 这就是本系列文章要讲述的故事。Edera 贡献了一系列更改,使得基于 Xen 的虚拟化从端到端实现了 NUMA 感知——从客户机内部,经过半虚拟化 I/O 驱动,进入 dom0,再回到 Hypervisor 对宿主机硬件的视图。据我们所知,其中一些组件是首次在任何地方实现。为了说明这一切为何重要,我们必须从 NUMA 到底是什么开始说起。 ## NUMA 的起源:从 UMA 到多插槽服务器 NUMA(https://en.wikipedia.org/wiki/Non-uniform_memory_access)代表非统一内存访问(Non-Uniform Memory Access)。其定义性特征就在名称中:在 NUMA 机器上,内存访问成本不是统一的。哪个 CPU 执行访问很重要,数据位于哪个物理内存条也很重要,成本取决于这两者之间的关系。 历史上与 NUMA 相对的是 UMA,即统一内存访问(Uniform Memory Access),其中每个 CPU 通过同一个内存控制器以相同的成本访问每一字节内存。UMA 是单插槽通用服务器和许多嵌入式系统的世界。它在概念上很简单,并且可以扩展到一定限度——这个限度大致是“你可以集成在一个芯片上且带有一套内存通道的核心数量”。 当你需要突破这个限度时会发生什么?UMA 会一直扩展直到它停止,而“停止”有几个相互交织的原因。电气走线长度变长。信号在两端之间传播需要更长时间,而在现代内存总线运行的数据速率下,较长的走线也会迫使你降低总线速度以保持信号完整性——这就在延迟之上又叠加了延迟。(这与 L1/L2/L3 缓存层次结构背后的物理原理相同:L1 很小,部分原因是因为保持缓存小巧且物理上靠近核心才能让它快速运行。)单个内存控制器无法以满带宽为任意数量的核心提供数据;封装上的引脚数为一个插槽可以连接多少个内存通道设定了硬性上限。多个 CPU 同时尝试读取内存造成的总线争用不再可以忽略。行业的解决方案是给每个插槽(或芯片)配备自己的内存控制器,并停止假装内存是一个对称的池子。机器变成了 NUMA。每个节点都有自己的 CPU 和属于自己的内存切片;CPU 仍然可以从任何节点读取,但读取远程节点需要穿过一条互联链路,而这条互联链路在绝对速度上很快,但相对于本地 DRAM 访问来说仍然很慢。 UMA CPU 0 CPU 1 CPU 2 CPU 3 内存控制器 内存 一个池子,由所有 CPU 共享 每次访问成本相同 NUMA 节点 0 CPU 0 CPU 1 内存控制器 节点 0 内存 节点 1 CPU 2 CPU 3 内存控制器 节点 1 内存 互联链路 本地访问便宜;远程访问需要穿过互联链路 把互联链路想象成连接两个相邻城镇的桥梁会有所帮助。在空旷的道路上,过桥只比本地交通慢一点点;在高峰时段,当每个人都在同时尝试使用同一座桥时,成本就是队列当时有多长。当我们后面讨论 NUMA 在繁忙的生产工作负载下与在安静的微基准测试中看起来有何不同时,会再回到这个区别。 这里提供一个简短的时间线来帮助理解其渊源。商业 NUMA 出现在 1990 年代,面向那些需要超过单条总线可供应核心数量的小众买家的大型机器:SGI 的 Origin 系列、Sequent NUMA-Q、Compaq 的 AlphaServer GS 系列,都采用了定制的节点间互连结构。NUMA 于 2003 年通过 AMD 的 Opteron 进入通用 x86 领域,它给每个插槽配备了自己的内存控制器,并通过 HyperTransport 连接插槽。Intel 在 2008 年凭借 Nehalem 和 QuickPath Interconnect (QPI) 追赶上来,此前经历了很长一段时间的前端总线芯片组时代,那些芯片组是真正的 UMA(所有 CPU 共享一条总线连接到北桥,每个插槽完全没有独立内存)。今天的 Intel 部件使用 Ultra Path Interconnect (UPI),它是 QPI 的后继者;今天的 AMD 部件使用 Infinity Fabric,它是 HyperTransport 的后继者。每一代都大幅提升了绝对带宽,但远程到本地 DRAM 成本的比率一直顽固地保持在大致相同的范围内。 看到这里,你可能会倾向于认为,每个插槽就是一个 NUMA 节点,仅此而已。在 Opteron 之后的十多年里,这确实是正确的。操作员将 NUMA 理解为“拓扑结构反映了插槽布局”,因为在那个时代,每个插槽的核心数较少,单个内存控制器可以毫无问题地为整个插槽提供数据。这种心智模型一直有效。 这种情况在 2010 年代末左右开始不再成立。AMD 基于 Zen 的 EPYC 将每个插槽的核心数推得足够高——并且使得制造一个巨大的单片芯片的权衡变得不再吸引人——以至于封装变成了一个由小芯片组成的星座:一个插槽内包含多个核心复合芯片 (CCD),每个 CCD 都有自己的 L3 缓存切片和内存控制器切片,通过 Infinity Fabric 粘合在一起。关键在于,Infinity Fabric 现在被用在插槽 *内部* 连接 CCD,而不仅仅是在插槽之间:它是同一种结构,做着相同的工作,只是规模更小。因此,单个 EPYC 插槽可以向操作系统呈现多个 NUMA 节点,具体取决于型号和 BIOS 配置——“每插槽节点数”(NPS) 旋钮,在当前世代上从 NPS1 到 NPS4。 Intel 实现相同想法的方式是子 NUMA 集群 (Sub-NUMA Clustering, SNC)。在最近的 Xeon 部件上默认关闭但可通过 BIOS 开启,SNC 将芯片上的内存控制器网格进行分区,并将结果呈现为每个插槽多个 NUMA 节点。 “一个插槽,一个节点”曾是一个有用的初步近似,但在几年前它在通用硬件上就不再正确了。大于两个插槽的 x86 服务器除了一些特定的小众领域外已经变得罕见;现代系统上有趣的 NUMA 复杂性来自于单个双插槽机箱内部,现在四到八个节点是常规配置。 ## “慢”到底有多大? 精确的数字是平台相关的,人们对于精确的方法论也有争论,但一个有用的经验法则是,在现代服务器上,一次远程 DRAM 访问的成本大约是本地访问的 1.5 倍到 3 倍。带宽也更低,有时比例相似。这些数字是你在空闲机器上使用微基准测试测量出来的。 节点 0 CPU 0 CPU 1 内存控制器 节点 0 内存 节点 1 CPU 2 CPU 3 内存控制器 节点 1 内存 互联链路 本地 远程 在实际工作负载下,差距通常会更大,因为互联链路是共享的。如果许多核心同时进行跨节点访问,它们会争用相同的互联带宽,因此每个请求的延迟都会增加。这就是许多“为什么这台服务器在高负载下很慢”之谜背后的机制。微基准测试说跨节点访问慢 2 倍。生产工作负载可能会看到慢 4 倍甚至 5 倍,因为大家都在争抢同一条管道。 顺便提一个推论:NUMA 效应不仅仅会拖慢受影响的工作负载,还会使它们的性能变得 *更不可预测*。同一个请求可能需要 80 纳秒或 250 纳秒,具体取决于当时互联链路上还有什么其他流量。平均延迟是一个有用的数字,但尾部延迟才是 NUMA 问题通常首先显现的地方。 ## 交错:牺牲峰值换取下限 有一种众所周知的方法可以平坦化成本曲线,而无需做任何理解内存应该放在哪里的工作:内存交错。在所有节点之间轮询分配页面,无论请求任务在哪个节点上。在 Linux 上这是 `numactl --interleave=all`;一些数据库和 JVM 在自己的分配器中做了类似的事情;一些 Hypervisor 在无法使用更智能的方式时为客户机内存隐式地这样做。 BIOS 级别的版本也存在。大多数双插槽服务器固件都有一个“节点交错”或“跨节点内存交错”开关,它以缓存行或页面粒度将物理内存分布到各插槽。开启后,操作系统会看到一个大的 UMA 池,而不是 NUMA 拓扑,并且任何 NUMA 感知代码都会变得无效:没有拓扑可供感知了。 那个开关在双插槽机器上最常见,但更深层次的问题在任何地方都一样:交错扩展性很差。节点越多,任何核心与任何内存条之间的最坏情况路径就越长,互联链路承载所有这些轮询流量所承受的压力就越大。交错所做的权衡——用可预测的平庸换取不可预测的混合——随着拓扑的扩大只会变得更严峻。 同一个任务在节点 0 上;它的页面,三种放置方式 NUMA 感知 快速且一致 每次访问都是本地的 NUMA 无感知 不可预测 没有模式,随运行而异 交错 一致但平庸 “让所有东西都一样差” 位于节点 0 的页面(本地) 位于节点 1 的页面(远程) 交错使得任何单一分配的成本大致等于机器上所有节点间成本的平均值。在两节点宿主机上,这意味着大约一半的访问是本地的,一半是远程的,这是由构造决定的。延迟方差消失了,因为工作负载不再因为碰巧触及哪个页面而获胜或失败。 一位前同事对此权衡有一个最简洁的一句话总结:交错让所有东西都一样差。这是以牺牲峰值为代价换取的一致性。一个本来可以完全放在单个节点上、只访问本地内存的工作负载,现在每次分配中有一半都要付出远程访问成本。 这是一个真实的工具,也有真正的理由去使用它。在 NUMA 宿主机上运行的无 NUMA 感知软件通过交错获得可预测的性能,而“可预测的差”通常比“不可预测的混合”更容易运维。即使是那些跨整个机器运行的内存带宽密集型工作负载,甚至可能受益,因为它们可以聚合每个内存控制器的带宽,而不是让一个饱和。 但它也留下了很多潜力未被挖掘。一个确实知道自身访问模式的工作负载,运行在一个能提供正确拓扑并尊重其放置请求的栈上,可以在延迟和吞吐量上都远胜于交错。本系列文章所涉及工作的全部意义在于,Xen 客户机不再需要在“盲目并祈祷”和“盲目并交错”之间做出选择。客户机可以知道自己在哪里,并据此采取行动。 ## 两种亲和性,而非一种 很多开发者在整个职业生涯中可能都不需要考虑 NUMA。如果你的部署目标是笔记本电脑、单插槽工作站,或者那种从单个宿主机节点分割出来的云虚拟机,那么你实际上一直处于 UMA 机器上,而 NUMA 感知工具最多只在你视野的边缘。当你把工作负载迁移到一个更大的机器上时,它就会出现,通常没有任何比“这个怎么这么慢”更响亮的警告,而标准开发者反应是发现一个从未有人提及的完整的第二个放置轴。 第一个轴,熟悉的那个:如果你曾经使用 `taskset` 或 `sched_setaffinity` 将进程绑定到特定 CPU,那么你已经了解了图景的一半。任务对 CPU 具有亲和性。内核调度器会尽力让它们待在你放置的地方。CPU 亲和性是那一半通常在教程和博客文章中出现的部分。 另一半是内存亲和性,它出现的频率要低得多。在 Linux 上,它通过 `mbind()`、`set_mempolicy()` 以及像 `numa_alloc_onnode()` 这样的 libnuma 辅助函数来控制,所有这些函数决定分配来自哪个 NUMA 节点;`numactl(8)` 手册页(https://man7.org/linux/man-pages/man8/numactl.8.html)是实际的入口点。内核也有自己的自动 NUMA 平衡功能——`NUMA_BALANCING` 开关——将页面迁移到频繁触摸它们的 CPU 附近。 关于 Linux 内存放置的默认行为值得了解,因为它会坑到人。它被称为“首次触摸”。当用户空间请求一个页面时——通过 `malloc()`、`mmap()`,或任何归结为缺页中断的操作——页面实际上不会立即被分配。它是在某个 CPU *首次触摸* 它时才被分配的。页面落在哪个节点取决于*触摸* 的 CPU 在哪个节点上,而不是如果它们不同的话*请求* 的 CPU 在哪个节点上。这对于从同一个线程分配和使用内存的进程来说没问题。但对于一种常见模式来说,却是一场无声的灾难:一个初始化线程分配并清零一个大缓冲区,然后其他节点上的工作线程来消费它。每个页面都落在初始化者的节点上;每次工作线程的访问都是远程的;操作员会看到“这个基准测试比应该的慢,我搞不懂为什么”。 CPU 亲和性与内存亲和性之间的碰撞是大多数有趣行为发生的地方。它们必须一致,优化才能生效。如果你的任务被固定到节点 0,而它的内存在节点 1 上,那么你做得比根本不固定还差——你确保了每次访问都是远程的。反过来,如果你在节点 0 上分配了内存,但你的任务被调度到了节点 1,结果一样。首次触摸是两者最终在操作员未注意的情况下不一致的一种具体情况。 有三种情况会导致远程 NUMA 访问: - 任务相对于其数据在错误的 CPU 上运行, - 数据相对于任务存储在错误的内存条中, - 或两者皆具。 普通的 Linux 为用户空间提供了处理这一切的工具,用于你控制的进程。更困难的问题出现在上一层:操作系统本身对拓扑了解多少,谁将这些信息暴露给谁,以及当其中一层变得“盲目”时会发生什么? ## 操作系统看到了什么——以及 Xen 的 dom0 遗漏了什么 一个 NUMA 感知的内核所做的不仅仅是知道存在拓扑。它将物理内存映射划分为每个节点的块,并跟踪哪些 CPU 是每个节点本地的,然后将这些信息暴露给用户空间。`numastat`、`numactl -H` 和 `/sys/devices/system/node/` 层级结构是这些机制可见的末端:用户空间进程可以询问“节点 0 上有多少空闲内存?”或“哪些 CPU 是我刚分配的内存本地的?”并得到一个真实的答案。底层的信息来源是固件的系统资源亲和性表 (SRAT) 和系统局部性信息表 (SLIT),我们将在本系列后面再次遇到它们。 同样的机制也是另一个意外发生的地方。历史上的“一个插槽,一个节点”机器有慷慨的每节点内存:一个 256 GiB 的双插槽服务器有两个 128 GiB 的节点,“将工作负载适配到一个节点内”很容易。多节点插槽迅速打破了这种直觉。拿同一个双插槽机箱来说,装上 EPYC 部件,将固件设置为 NPS4:每个插槽四个节点,每台宿主机八个节点,每个节点 32 GiB。一个以前明显是单节点的工作负载,现在可能仅仅因为每节点 DRAM 的算术就需要两到三个节点,即使其 CPU 占用可以轻松放在一个节点上。这种算术是毫不留情的,并且会在人们第一次遇到时让他们感到惊讶。 这里有必要具体说明涉及到的感知层次,因为搞错成本取决于哪一层是不知道的那一个: - **在 NUMA 硬件上,在 NUMA 感知内核上运行 NUMA 感知应用程序。** 自 NUMA 进入主流以来,每个人一直在努力实现的设计点。

相似文章

本地 AI 硬件内存带宽(2026 年版)

X AI KOLs

本文深入解析内存带宽作为本地 AI 硬件性能的关键指标,对比了 NVIDIA、Apple、AMD、Intel 等厂商在不同性能层级下的当前 GPU 与统一内存系统。

AMD 力推统一内存架构

Reddit r/LocalLLaMA

AMD 将统一内存架构视为下一代产品(如 Ryzen AI MAX 400 系列(Gorgon Halo))的关键推动力,从而影响其 AI 和计算工作负载的产品路线图。