NUMA、内存层级与缓存局部性
延迟存在于内存层级之中。在热路径上,数据所处的位置比代码运行的快慢更为关键。本页梳理了一台现代双路服务器的布局,并说明为什么 NUMA 放置与缓存局部性主导着 Aeron 的尾延迟。
请配合参数参考一起阅读——其中关于 NUMA 与 L3 的两个调优旋钮,本页会从第一性原理出发加以解释。
双路 NUMA 布局
Section titled “双路 NUMA 布局”一台现代服务器拥有两个 CPU socket,每个 socket 都有自己的核心、自己的内存控制器(MC)、自己的 DRAM,以及连接到 NIC 的独立 PCI-e 通道。两个 socket 之间通过互连总线(QPI)通信,而跨越这条链路的开销很高。
核心结构是:L1 与 L2 是每核心独享的,L3 在同一 socket 的所有核心之间共享,而 DRAM 则挂在各自 socket 的内存控制器上。NIC 在物理上只接到某一个 socket 的 PCI-e 通道——而不是两个都接。
各层级的延迟
Section titled “各层级的延迟”层级每往下一级,开销大致就要增加一个数量级。下面这些数值是一台典型 Xeon 级服务器的实用参考值。
| 层级 | 延迟 | 备注 |
|---|---|---|
| 寄存器/缓冲区 | < 1ns | |
| L1 缓存 | ~4 周期,~1ns | 每核心独享 |
| L2 缓存 | ~12 周期,~3ns | 每核心独享 |
| L3 缓存 | ~40 周期,~12ns | 每 socket 共享 |
| L3(脏命中) | ~75 周期,~25ns | socket 内跨核心 |
| DRAM(本地) | ~70ns | socket 本地内存控制器 |
| QPI(跨 socket) | 额外 > 40ns | 访问远端 socket 内存的惩罚 |
要点在于:L1 约 1ns,DRAM 约 70ns。这是 40 倍的差距,而且它正好落在你的热路径上。
为什么这对 Aeron 很重要
Section titled “为什么这对 Aeron 很重要”有三个放置决策对延迟的影响最大,每一个都清晰地对应着一个调优旋钮。
- 让数据驻留在 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 穿越。
这如何对应到调优旋钮
Section titled “这如何对应到调优旋钮”参考中有两个参数,正是从上述内存层级中直接推导出来的:
- 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 EPYC:socket 不等于缓存域
Section titled “AMD EPYC:socket 不等于缓存域”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;用 lscpu 和 numactl --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 / m6a | AMD EPYC 7R13(Milan,Zen 3) | 每 CCD 32 MB | 8 | driver + 热路径应用线程约束在一个 CCD 内 |
| c7a / m7a / r7a | AMD EPYC 9R14(Genoa,Zen 4) | 每 CCD 32 MB | 8(1 vCPU = 1 物理核心) | 同上——绝不让热线程横跨 CCD |
| c6i / m6i | Intel Xeon 8375C(Ice Lake) | 每 socket 54 MB | 32 | 留在 NIC 所在的 socket / NUMA 节点 |
| c7i | Intel Xeon 8488C(Sapphire Rapids) | 每 socket 105 MB(4 tile,EMIB,一块逻辑 L3) | 48 | 留在 socket 上;L3 均匀但慢(约 33ns) |
| c7g | Graviton3 | 每芯片 32 MB | 64 | 任意核心;单 NUMA 节点,无 SMT |
| c8g / r8g | Graviton4 | 每 socket 36 MB | 96 | socket 内任意核心;48xlarge = 双 socket |
- AWS Graviton Getting Started——处理器规格表(Graviton2/3/4 核心数、L2/L3 容量、NUMA 节点数、互连)
- AWS EC2 实例类型——通用型规格(Graviton 与 m7a 上 vCPUs = cores、threads per core = 1)
- Wikipedia——Zen 3(CCD = 一个 8 核 CCX,共享 32 MB L3)
- Wikipedia——Zen 4(每 CCD 32 MB L3;Genoa 最多 12 个 CCD)
- Wikipedia——Sapphire Rapids(四块 tile、EMIB、最高 112.5 MB L3)
- Chips and Cheese——A Peek at Sapphire Rapids(跨 tile 的准单芯片 L3、约 33ns 的 L3 延迟、mesh 切片哈希、分簇模式)
- Chips and Cheese——Zen 4 内存子系统(Zen 4 L3 延迟约 9ns)
- nviennot/core-to-core-latency(EPYC 7R13:CCD 内约 23ns vs 跨 CCD 约 90–110ns;Xeon 8375C:约 51ns 均匀延迟)
- Broadcom KB——AMD EPYC BIOS 与 NUMA 指南(NPS 将每 socket 呈现为 1/2/4 个 NUMA 节点;CCX-as-NUMA 选项)
- Phoronix——Intel sub-NUMA clustering(SNC 将核心、缓存和内存切分为多个 NUMA 域)
- AWS——c6i、c7i、c7g、c8g 实例页面(各家族对应的 CPU 代次)
本页只覆盖硬件层面的背景。关于 Aeron 的 term buffer、日志结构和计数器是如何布局以充分利用缓存局部性的,请参阅 The Aeron Files——它对内部机制的剖析远比我们这里重复的内容更为深入。