跳转到内容

接收路径上的四个缓冲区

线缆上的一个数据包,在被 Aeron 订阅者读到之前,要穿过 四个嵌套缓冲区。每一个都必须足够大, 以吸收其上层的停顿——否则就会丢包、产生间隙并触发 NAK。本页从最外层(线缆)到最内层(应用)梳理这个栈、 各层之间被强制的约束,以及设置它们的先后顺序。下面的每条约束都已对照 Aeron 驱动源码与 ENA 驱动的 最佳实践指南核实——见来源

请配合参数参考BDP 一起阅读—— 本页正是那些旋钮在物理上所处的位置。

缓冲区属性默认角色
ENA RX ringethtool -G rx1024(256 至最高 16K,随实例类型而异——用 ethtool -g 查看)NIC descriptors;在主机排空之前 NIC 能容纳多少数据包
OS 接收缓冲区aeron.socket.so_rcvbuf,受 net.core.rmem_max 封顶128KBNAPI 与 Aeron 的 socket 读取(C 驱动用 recvmmsg)之间的内核 socket 队列
Term bufferaeron.term.buffer.length16MB(最小 64KB,最大 1GB,2 的幂)订阅者实际读取的、mmap 出来的日志
初始窗口aeron.rcv.initial.window.length128KB接收方授予发送方的流控额度

两个 128KB 的默认值并非巧合。Aeron 源码把窗口默认值按教科书式 BDP 推导—— 10 Gbps × 100 µs 局域网 RTT = 125,000 字节,取整为 128KB——并把 SO_RCVBUF 的默认值设为 完全相同的数:恰好能通过它自己启动校验(约束 C)的最小值。

这些不是建议——驱动会在启动时强制执行。

约束 A —— 窗口受 term 长度限制

Section titled “约束 A —— 窗口受 term 长度限制”

receiverWindow = min(initialWindowLength, termBufferLength / 2) (Configuration.java,receiverWindowLength()

流控窗口 永远不能超过 term buffer 的一半。把初始窗口设得超过 termLength / 2 毫无作用—— 你必须先增大 term buffer。(例如 16MB 的 term 会把窗口封顶在 8MB,无论你设多大。)

约束 B —— 初始窗口必须 ≥ MTU

Section titled “约束 B —— 初始窗口必须 ≥ MTU”

MTU ≤ initialWindowLength (Configuration.java,validateInitialWindowLength();否则驱动拒绝启动)

当 MTU 为 8192 时,128KB 的默认窗口没问题——只是永远不要把窗口设得低于 MTU。

约束 C —— SO_RCVBUF 必须容纳窗口(BDP)——同样被强制执行

Section titled “约束 C —— SO_RCVBUF 必须容纳窗口(BDP)——同样被强制执行”

initialWindowLength ≤ SO_RCVBUF (Configuration.java,validateSocketBufferLengths();否则驱动拒绝启动)

源码把 so_rcvbufinitial.window 两者 都标注为”必须足以容纳带宽时延积(BDP)”, 并且驱动会强制这对关系:启动时若窗口超过已配置(或操作系统默认)的 socket 缓冲区,会直接抛出 initialWindowLength > SO_RCVBUF — increase aeron.socket.so_rcvbuf。否则内核会丢弃那些 窗口已允许在途的数据包。

驱动唯一 无法 强制的缺口是 net.core.rmem_max:如果内核上限低于申请的 so_rcvbuf, 内核会静默地把真实缓冲区封顶,而驱动只打印一条 警告——照样启动,但实际 socket 缓冲区 比窗口还小。“内核丢弃在途数据包”这个失败模式,正是从这扇后门溜进来的。

ENA ring(最大)─ 排空速度要快到能喂饱 ─►
SO_RCVBUF ≥ receiverWindow = min(initialWindow, termLength/2)
▲ 前提是 net.core.rmem_max ≥ SO_RCVBUF(否则内核静默封顶)

这四个值是相互关联,还是彼此独立?

Section titled “这四个值是相互关联,还是彼此独立?”

两者皆是——这正是关键。四个里有三个是相互关联的(一条以窗口为锚的字节链); ENA ring 则位于另一条轴上。

流控 窗口是锚——它是 意图(你允许多少字节在途 = 你的 BDP 目标)。另外两个字节缓冲区由上述约束 相对于它 来确定大小:

实线箭头是驱动在启动时强制执行的约束;虚线箭头是它 无法 强制的那一条——由内核静默执行。 右边这条轴与左边毫无交集:ring 按停顿 时间 取大小,而不是按在途字节。

所以它们 不是独立的:先选定窗口,term buffer 就有了硬性下限(≥ 2 × 窗口), socket 缓冲区也有启动时检查的硬性下限(≥ 窗口)——而 rmem_max 是那道静默的天花板, 决定你申请的 SO_RCVBUF 是否真正兑现。

算例——目标窗口 256KB(同 AZ 默认值的 2 倍——10 Gbps 下可覆盖约 200 µs 以内的 RTT;真正的 10 Gbps 跨 AZ 需要约 2MB+ 的窗口):

缓冲区取值原因
初始窗口256KB锚——你的 BDP 目标
Term buffer≥ 512KB约束 A:term ≥ 2 × 窗口(16MB 默认值早已满足)
SO_RCVBUF≥ 256KB(建议设 ~2MB)约束 C:≥ 窗口 否则无法启动,外加停顿余量——并把 rmem_max 抬到匹配
ENA RX ring最大(ethtool -g时间 预算而非字节预算——见下文

ENA ring 在另一条轴上:时间,而非字节

Section titled “ENA ring 在另一条轴上:时间,而非字节”

ring 不以字节计——它是 数据包 descriptor 数量(256–16K 条,随实例类型而异), 守护的是 另一种失败模式:熬过一次 排空停顿——为接收路径供血的 vCPU 不可用, NAPI 无法排空 ring。它的余量以时间来度量:

可熬过的停顿 ≈ ring 条目数 ÷ 入站包速率

在 10 Gbps、约 1500 字节帧(约 82 万 pps)下,1024 条的默认值只能熬过 ≈ 1.2 ms 的停顿; 8192 条能换来 ≈ 10 ms(巨型帧会让每个条目顶更久)。所以 ring 不随窗口缩放——它随 你必须熬过多长的停顿 缩放。ENA 指南对突发性 CPU 负载的建议正是如此:增大 RX ring 以补偿 vCPU 的短暂不可用。实用规则:设到最大然后忘掉它——在部署时就做,因为调整 ring 大小会 造成短暂的流量中断。

窗口告诉发送方”你可以有 N 字节在途”。这 N 字节必须能穿过它下方的每一层,否则就会丢失并 NAK:

  1. SO_RCVBUF 实际小于窗口(配置层面的情形会被驱动在启动时拦下,但 rmem_max 可以静默封顶)→ 内核 socket 溢出 → RcvbufErrors → 间隙 → NAK。
  2. SO_RCVBUF 够用,但接收路径的 vCPU 停顿(GC、CPU steal,或调度抖动——例如 CFS 唤醒抢占把刚被唤醒的接收线程压在 CPU 外约 10 ms)→ NAPI 来不及把 ring→socket 搬完 → ENA RX ring 溢出(ethtool -S 中的 rx_overruns)→ NAK。这正是典型的生产环境溢出图: socket 本身够大,但一次停顿让 NIC ring 被填满。
  3. 窗口对 termLength/2 而言过大 → 被静默封顶(约束 A),于是你 以为 抬高了吞吐余量,其实并没有。

要增加在途余量(吸收突发/停顿 → 减少溢出 → 减少 NAK),请 自下而上、一致地 抬高各层:

  1. ENA ring → 最大(先用 ethtool -g 查上限——大实例最高 16K,再 ethtool -G <dev> rx <max>)。在部署时就做:调整大小会造成短暂的流量中断。
  2. rmem_max ≥ 期望的 SO_RCVBUF,然后把 so_rcvbuf 设为 BDP(高速率下例如 2–8MB)。 rmem_max 只是上限——在 so_rcvbuf 申请之前,单独抬高它毫无作用;而漏掉它, so_rcvbuf 申请到的会被静默封顶。
  3. Term buffer ≥ 2 × 期望窗口 —— 由约束 A 要求,窗口才能随之增大。
  4. 初始窗口 = 你的 BDP 目标≥ MTU≤ termLength/2≤ SO_RCVBUF)。这才是真正的流控额度。

关于第 2、4 步背后的 BDP 计算,见 带宽时延积。 关于流控与 NAK 的协议级机制,见 The Aeron Files

输入你的 SBE 消息大小、目标 TPS、节点间 RTT 与实例家族——计算器会串联本页的每条约束 (窗口 ← BDP、term ≥ 2 × 窗口、SO_RCVBUF ≥ 窗口、rmem_max 上限、ring 停顿预算), 给出最小值/建议值,并判断工作集是否能放进所选 CPU 的 L3 共享域。

接收路径容量计算器
推算结果
每条消息在线大小(32B 帧头,32B 对齐)
带宽 λ = TPS × 每条消息大小
BDP = λ × RTT
初始窗口 aeron.rcv.initial.window.length2–4 × BDP,且 ≥ 128KB 默认值、≥ MTU
Term buffer aeron.term.buffer.length≥ 2 × 窗口(约束 A)且 ≥ 2 × λ × 停顿余量(发布方最多领先 term/2),2 的幂,64KB–1GB
操作系统 socket 接收缓冲区 SO_RCVBUF(aeron.socket.so_rcvbuf)≥ 窗口(约束 C,否则拒绝启动);建议 2× 留停顿余量
net.core.rmem_max内核允许的 SO_RCVBUF 上限(sysctl)——低于申请值时内核静默封顶
PPS(数据包/秒)
ENA RX ring 可熬过的停顿(按最坏 PPS)
ENA RX 队列(ethtool -L)RSS 按流(5 元组)哈希到一个队列——单条 Aeron 流只会落在一个队列/ring/IRQ vCPU 上
NAK 定时器(杠杆 B——丢包恢复)由 RTT 推导:backoff ≥ 2×RTT(≈ ½ 单次丢包预算);linger ≈ 3×(backoff+RTT);group.size = 真实接收方数。完整推导与实测见下文「杠杆 B」页
MTU 建议
L3 缓存匹配

范围说明:本计算器只覆盖丢包预防(把链路各层配够)。丢包发生后的恢复由 NAK/重传定时器决定——默认约 10ms 的退避意味着每次丢包在尾部付出 10–20ms,小扇出流可考虑调小相应参数(见下文「杠杆 B」一节)。另:输出与输入成正比——RTT 与停顿余量请填实测值。

同样的输入还能直接推出两个数:

MTU。 Aeron 的默认 aeron.mtu.length1408——一个 Aeron 帧恰好装进标准 1500 字节 以太网 MTU,并给 IP/UDP 头留出余量。当带宽较高、或单条消息装不进 1408 时,提到 8192 (配合 VPC 巨型帧,MTU 9001):更少、更大的包能降低每包开销与 PPS。代价是丢包放大——丢一个 8KB 数据报要 NAK 的数据比丢一个 1.4KB 的多——并记住约束 B:窗口必须 ≥ MTU,且启动时强制 SO_SNDBUF ≥ MTU。跨区域或公网路径通常不支持巨型帧;那里保持 1408。

PPS。 EC2 在带宽配额之外还按实例强制 PPS 配额。你的包速率介于 TPS ÷ 每数据报消息数 (攒批)与 TPS × 每消息数据报数(未攒批最坏情况)之间——计算器两个都会显示。压低 PPS 有两个 理由:ENA ring 按 排空,PPS 减半,同样的 ring 能熬过的停顿就翻倍;超出实例配额会表现为 ethtool -S 里的 pps_allowance_exceeded 丢包——内核看不见,只在间隙与 NAK 中现形。 Smart batching 与更大的 MTU 是仅有的两个杠杆。

杠杆 B —— 丢包恢复:NAK 定时器

Section titled “杠杆 B —— 丢包恢复:NAK 定时器”

以上全部是 杠杆 A:预防——把链路各层配够,让停顿不至于丢包。但当丢包仍然发生时, 尾部代价由另一组完全不同的旋钮决定:NAK/重传定时器。你可以通过本页的所有检查, 却仍然因为一个丢包看到 ~10–20 ms 的 p99.9——因为丢包事件的延迟下限是 恢复时间, 而不是缓冲区大小。

默认值(Configuration.java):

参数默认角色
aeron.nak.unicast.delay1 µs单播:接收方几乎立即发 NAK
aeron.nak.unicast.retry.delay.ratio100单播:若重传未到,按 delay × 100 重发 NAK
aeron.nak.multicast.max.backoff10 ms组播:接收方先等 随机 0–10 ms 再发 NAK,避免大组对发送方形成 NAK 风暴
aeron.nak.multicast.group.size10退避随机化所假设的组规模
aeron.retransmit.unicast.delay0发送方收到 NAK 立即重传
aeron.retransmit.unicast.linger10 ms重传后 10 ms 内忽略同一范围的重复 NAK

集群/行情场景的陷阱:组播退避是为 ~10+ 的扇出调校的,但 3 节点集群或 2 订阅者的 MDC 通道也要付出同样的随机 0–10 ms 等待——纯粹的尾延迟,却没有风暴可防。对小而已知的扇出, 调小 aeron.nak.multicast.max.backoff(例如 100 µs – 1 ms),每次丢包的尾部代价大致同步下降。 单播流本就近乎立即,一般无需改动。

恢复还要回读 term buffer(重传数据来自日志)——这是 term buffer 即重传历史的 另一层含义,也是追赶式读取掉出 L3(见上文)会放大丢包事件的原因。

推导公式(由 RTT 定 backoff、由双重丢包预算定 linger)、有效性检查与实测 79 倍尾延迟坍缩, 见 NAK 定时器调优。完整的 NAK 协议机制, 请参阅 The Aeron Files