接收路径上的四个缓冲区
线缆上的一个数据包,在被 Aeron 订阅者读到之前,要穿过 四个嵌套缓冲区。每一个都必须足够大, 以吸收其上层的停顿——否则就会丢包、产生间隙并触发 NAK。本页从最外层(线缆)到最内层(应用)梳理这个栈、 各层之间被强制的约束,以及设置它们的先后顺序。下面的每条约束都已对照 Aeron 驱动源码与 ENA 驱动的 最佳实践指南核实——见来源。
请配合参数参考与 BDP 一起阅读—— 本页正是那些旋钮在物理上所处的位置。
栈结构:线缆 → 应用
Section titled “栈结构:线缆 → 应用”默认值与角色
Section titled “默认值与角色”| 缓冲区 | 属性 | 默认 | 角色 |
|---|---|---|---|
| ENA RX ring | ethtool -G rx | 1024(256 至最高 16K,随实例类型而异——用 ethtool -g 查看) | NIC descriptors;在主机排空之前 NIC 能容纳多少数据包 |
| OS 接收缓冲区 | aeron.socket.so_rcvbuf,受 net.core.rmem_max 封顶 | 128KB | NAPI 与 Aeron 的 socket 读取(C 驱动用 recvmmsg)之间的内核 socket 队列 |
| Term buffer | aeron.term.buffer.length | 16MB(最小 64KB,最大 1GB,2 的幂) | 订阅者实际读取的、mmap 出来的日志 |
| 初始窗口 | aeron.rcv.initial.window.length | 128KB | 接收方授予发送方的流控额度 |
两个 128KB 的默认值并非巧合。Aeron 源码把窗口默认值按教科书式 BDP 推导——
10 Gbps × 100 µs 局域网 RTT = 125,000 字节,取整为 128KB——并把 SO_RCVBUF 的默认值设为
完全相同的数:恰好能通过它自己启动校验(约束 C)的最小值。
被强制的约束
Section titled “被强制的约束”这些不是建议——驱动会在启动时强制执行。
约束 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_rcvbuf 与 initial.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 则位于另一条轴上。
三个字节缓冲区,以窗口为锚
Section titled “三个字节缓冲区,以窗口为锚”流控 窗口是锚——它是 意图(你允许多少字节在途 = 你的 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 大小会 造成短暂的流量中断。
它们如何相互作用 —— 失败链
Section titled “它们如何相互作用 —— 失败链”窗口告诉发送方”你可以有 N 字节在途”。这 N 字节必须能穿过它下方的每一层,否则就会丢失并 NAK:
SO_RCVBUF实际小于窗口(配置层面的情形会被驱动在启动时拦下,但rmem_max可以静默封顶)→ 内核 socket 溢出 →RcvbufErrors→ 间隙 → NAK。SO_RCVBUF够用,但接收路径的 vCPU 停顿(GC、CPU steal,或调度抖动——例如 CFS 唤醒抢占把刚被唤醒的接收线程压在 CPU 外约 10 ms)→ NAPI 来不及把 ring→socket 搬完 → ENA RX ring 溢出(ethtool -S中的rx_overruns)→ NAK。这正是典型的生产环境溢出图: socket 本身够大,但一次停顿让 NIC ring 被填满。- 窗口对
termLength/2而言过大 → 被静默封顶(约束 A),于是你 以为 抬高了吞吐余量,其实并没有。
实用设置规则 —— 自下而上
Section titled “实用设置规则 —— 自下而上”要增加在途余量(吸收突发/停顿 → 减少溢出 → 减少 NAK),请 自下而上、一致地 抬高各层:
- ENA ring → 最大(先用
ethtool -g查上限——大实例最高 16K,再ethtool -G <dev> rx <max>)。在部署时就做:调整大小会造成短暂的流量中断。 rmem_max≥ 期望的SO_RCVBUF,然后把so_rcvbuf设为 BDP(高速率下例如 2–8MB)。rmem_max只是上限——在so_rcvbuf申请之前,单独抬高它毫无作用;而漏掉它,so_rcvbuf申请到的会被静默封顶。- Term buffer ≥ 2 × 期望窗口 —— 由约束 A 要求,窗口才能随之增大。
- 初始窗口 = 你的 BDP 目标(
≥ MTU,≤ termLength/2,≤ SO_RCVBUF)。这才是真正的流控额度。
关于第 2、4 步背后的 BDP 计算,见 带宽时延积。 关于流控与 NAK 的协议级机制,见 The Aeron Files。
交互式计算器
Section titled “交互式计算器”输入你的 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)——低于申请值时内核静默封顶 | |
| 发送方出口(N × λ)MDC 单播扇出:每条消息发 N 份独立拷贝——带宽与 PPS 均 ×N,先撞 NIC/实例配额而非缓冲区 | |
| SO_SNDBUF(aeron.socket.so_sndbuf)+ ENA TX ring发送侧停顿按 N×λ 填充;TX ring 仅 256–1K 条(大 LLQ 默认 512) | |
| 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 与 PPS:按包计的那条轴
Section titled “MTU 与 PPS:按包计的那条轴”同样的输入还能直接推出两个数:
MTU。 Aeron 的默认 aeron.mtu.length 是 1408——一个 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.delay | 1 µs | 单播:接收方几乎立即发 NAK |
aeron.nak.unicast.retry.delay.ratio | 100 | 单播:若重传未到,按 delay × 100 重发 NAK |
aeron.nak.multicast.max.backoff | 10 ms | 组播:接收方先等 随机 0–10 ms 再发 NAK,避免大组对发送方形成 NAK 风暴 |
aeron.nak.multicast.group.size | 10 | 退避随机化所假设的组规模 |
aeron.retransmit.unicast.delay | 0 | 发送方收到 NAK 立即重传 |
aeron.retransmit.unicast.linger | 10 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。
- Aeron 驱动源码 ——
Configuration.java:receiverWindowLength()、validateInitialWindowLength()、validateSocketBufferLengths(), 以及INITIAL_WINDOW_LENGTH_DEFAULT上的 BDP 推导注释;term 长度上下限见LogBufferDescriptor.java。 - ENA Linux Driver Best Practices and Performance Optimization Guide
—— RX ring 大小范围与默认值、
rx_overruns的语义,以及”突发性 CPU → 增大 RX ring”的建议。