跳转到内容

NUMA、内存层级与缓存局部性

延迟存在于内存层级之中。在热路径上,数据所处的位置比代码运行的快慢更为关键。本页梳理了一台现代双路服务器的布局,并说明为什么 NUMA 放置与缓存局部性主导着 Aeron 的尾延迟。

请配合参数参考一起阅读——其中关于 NUMA 与 L3 的两个调优旋钮,本页会从第一性原理出发加以解释。

一台现代服务器拥有两个 CPU socket,每个 socket 都有自己的核心、自己的内存控制器(MC)、自己的 DRAM,以及连接到 NIC 的独立 PCI-e 通道。两个 socket 之间通过互连总线(QPI)通信,而跨越这条链路的开销很高。

核心结构是:L1 与 L2 是每核心独享的,L3 在同一 socket 的所有核心之间共享,而 DRAM 则挂在各自 socket 的内存控制器上。NIC 在物理上只接到某一个 socket 的 PCI-e 通道——而不是两个都接。

层级每往下一级,开销大致就要增加一个数量级。下面这些数值是一台典型 Xeon 级服务器的实用参考值。

层级延迟备注
寄存器/缓冲区< 1ns
L1 缓存~4 周期,~1ns每核心独享
L2 缓存~12 周期,~3ns每核心独享
L3 缓存~40 周期,~12ns每 socket 共享
L3(脏命中)~75 周期,~25nssocket 内跨核心
DRAM(本地)~70nssocket 本地内存控制器
QPI(跨 socket)额外 > 40ns访问远端 socket 内存的惩罚

要点在于:L1 约 1ns,DRAM 约 70ns。这是 40 倍的差距,而且它正好落在你的热路径上。

有三个放置决策对延迟的影响最大,每一个都清晰地对应着一个调优旋钮。

  • 让数据驻留在 L3 缓存是最佳甜蜜点。 12ns 对比 DRAM 的 70ns 以上。如果你的 term buffer 能放进 L3,就能在每条消息上避开 DRAM 延迟——既降低 p50,更重要的是减少由缓存未命中带来的 p99 抖动。
  • NUMA 局部性。 通过 QPI 访问远端 socket 上的内存,每次访问会额外增加 40ns 以上。如果你的 NIC 在 Socket 0 上,而 Aeron 跑在 Socket 1 上,那么每个数据包都要两次跨越 QPI。这个惩罚会直接打在尾延迟上。
  • PCI-e 到 NIC。 NIC 在物理上只连接到某一个 socket。把 Aeron 的 media driver 跑在该 socket 本地的核心上,就能彻底消除跨 socket 的 PCI-e 穿越。

参考中有两个参数,正是从上述内存层级中直接推导出来的:

  • NUMA 局部性(CPU 靠近 NIC) —— 将 IRQ、driver 线程和应用线程绑定到 NIC 所在 NUMA 节点的核心上。这能消除 QPI 惩罚并稳定 p99。
  • 让 term buffer 放进 L3 —— 把活跃的 term 占用量控制在有效 L3 容量之内。一个常驻的工作集能把 DRAM 速率的未命中转变为缓存速率的命中,从而降低 p50 并收紧尾延迟。

CPU 架构补充:AMD CCD vs Intel 单一大芯片 vs Graviton

Section titled “CPU 架构补充:AMD CCD vs Intel 单一大芯片 vs Graviton”

上文的所有内容都基于经典的 Xeon 图景:一个 socket = 一块共享的大 L3。这个假设在 AMD EPYC 上会悄然失效——而 EPYC、Intel Xeon 和 AWS Graviton 分别驱动着不同的 EC2 实例家族,因此”绑定到 NUMA 节点”在每种架构上的含义并不相同。本节梳理这三种拓扑,确保上文的绑核建议落在正确的核心上。

AMD 用 chiplet(小芯片)拼装 EPYC。每个 CCD(Core Complex Die)承载一个最多 8 核的 CCX,共享一块 32 MB 的 L3 切片——Zen 3(Milan,c6a 中的 EPYC 7R13)和 Zen 4(Genoa,c7a/m7a/r7a 中的 EPYC 9R14,每 socket 最多 12 个 CCD)都是如此。这块 L3 是 CCD 私有的:一个核心永远不会把数据放进另一个 CCD 的 L3,所有跨 CCD 的流量都要绕道 I/O die、走 Infinity Fabric。

实测数据让这道悬崖一目了然。在 EPYC 7R13(c6a)上,核心间通信延迟在 CCD 内约 23ns,跨 CCD 则飙升到 约 90–110ns——比上文 Xeon 图中跨 socket 的 QPI 跳转还要昂贵,而且这一切发生在同一个 NUMA 节点内部。如果调度器把一个线程从 CCD 0 迁移到 CCD 1,它醒来时面对的是一块冷的 L3:整个工作集都要以缓存未命中的方式重放,这一波惩罚会直接打在 p99 上。

这对上文两个旋钮意味着:

  • “term buffer 放进 L3”指的是 32 MB,而不是整个 socket 的总量。 一颗 Genoa socket 标称 384 MB L3,但一个被绑定的线程永远只能命中自己 CCD 的那 32 MB。预算要按 32 MB 来算。
  • 只绑定到 NUMA 节点是不够的。 要把 media driver 的 conductor/sender/receiver 线程以及访问同一批 term buffer 的应用线程都约束在一个 CCD(8 核)之内lscpu -e(看 L3 列)和 lstopo 都会把 CCD 边界显示为 L3 分组;在 c7a 上每个 vCPU 都是物理核心(SMT 已关闭),连续的 vCPU 自然按 CCD 分组。

在裸金属上,BIOS 还提供 NPS(NUMA-per-socket:NPS1/NPS2/NPS4)把每个 socket 呈现为 1、2 或 4 个 NUMA 节点,以及 ACPI SRAT L3 Cache as NUMA Domain 选项把每个 CCX 暴露为独立节点——便于让调度器感知 CCD。在 EC2 上你无法控制 BIOS;用 lscpunumactl --hardware 验证实际拿到的拓扑。

Intel Xeon:一块大而(相对)慢的 L3

Section titled “Intel Xeon:一块大而(相对)慢的 L3”

Ice Lake(c6i 中的 Xeon 8375C:32 核,54 MB L3)把所有核心和 L3 切片挂在单一大芯片(monolithic die)的一张 mesh 上——每个核心都能以大致均匀的延迟看到整块 L3。Sapphire Rapids(c7i 中的定制 Xeon 8488C:48 核,105 MB)在物理上是用 EMIB 拼接的四块 tile,但 mesh 跨越 tile 边界、呈现为一块逻辑 L3,行为上接近单一大芯片(quasi-monolithic)。

代价是:用速度换容量和均匀性。Sapphire Rapids 的实测 L3 延迟约为 33ns——大约是 AMD 单 CCD L3(Zen 4 约 9ns)的 3 倍——因为每次访问都会被哈希分散到一张巨大 mesh 上的所有 L3 切片。Intel 在 BIOS 中提供 sub-NUMA clustering(SNC),可以把一个 socket 的核心、L3 和内存控制器切分成 2 或 4 个 NUMA 域以收紧局部性,但同样:在 EC2 上你够不到这个旋钮。

绑核因此宽松得多:socket 上任意核心看到的都是同一块 L3,所以只要守住本页前文的规则——留在 NIC 所在的 socket / NUMA 节点上——就够了。

AWS Graviton:三者中最简单的拓扑

Section titled “AWS Graviton:三者中最简单的拓扑”

Graviton3(c7g:64 核,32 MB L3)和 Graviton4(c8g/r8g:每 socket 96 核,36 MB L3,每核 2 MB L2)把所有核心放在一张一致性 mesh 上,呈现为单个 NUMA 节点——并且没有 SMT:每个 vCPU 都是物理核心(AWS 规格表中 threads per core = 1)。没有 CCD 边界,没有超线程兄弟污染你的 L1/L2,没有跨 die 的意外。

两点注意:这块共享 L3 很(整颗芯片只有 32–36 MB——约等于 AMD 一个 CCD 的量,却要被 64–96 个核心分享),所以 term buffer 放进 L3 的预算既紧张又有竞争;另外 192 vCPU 的 48xlarge 规格(如 r8g.48xlarge)是双 socket、2 个 NUMA 节点,本页开头的所有跨 socket 规则在那里重新生效。

EC2 家族CPU每个共享域的 L3每个 L3 域的核心数绑核经验法则
c6a / m6aAMD EPYC 7R13(Milan,Zen 3)每 CCD 32 MB8driver + 热路径应用线程约束在一个 CCD 内
c7a / m7a / r7aAMD EPYC 9R14(Genoa,Zen 4)每 CCD 32 MB8(1 vCPU = 1 物理核心)同上——绝不让热线程横跨 CCD
c6i / m6iIntel Xeon 8375C(Ice Lake)每 socket 54 MB32留在 NIC 所在的 socket / NUMA 节点
c7iIntel Xeon 8488C(Sapphire Rapids)每 socket 105 MB(4 tile,EMIB,一块逻辑 L3)48留在 socket 上;L3 均匀但慢(约 33ns)
c7gGraviton3每芯片 32 MB64任意核心;单 NUMA 节点,无 SMT
c8g / r8gGraviton4每 socket 36 MB96socket 内任意核心;48xlarge = 双 socket

本页只覆盖硬件层面的背景。关于 Aeron 的 term buffer、日志结构和计数器是如何布局以充分利用缓存局部性的,请参阅 The Aeron Files——它对内部机制的剖析远比我们这里重复的内容更为深入。