发送端排队与突发配速
有一类延迟尖刺能扛过所有接收端的修复。你正确地配置了接收路径缓冲区, 看到零 UDP 丢包、零 NAK 恢复、零流控反压——但毫秒级的尾部依然存在。它既不在线上,也不在接收端。 它是消息在发送端自己的 term buffer 里,等待发送端的 media driver 把它放上线的时间。
本文是接收端视角的微突发模型的发送端补充:同一个触发因素(未配速的突发), 不同的故障面。
一个 publisher 有两个容易混淆的速率:
- 派发速率
P——应用调用offer()的速度。未配速的循环以内存速度把消息灌入 term buffer——每秒数千万条。 - 排空速率
D——media driver 单个Sender线程用sendmmsg()把 term buffer 序列化上线的速度。 它受限于单线程的每包 CPU/系统调用开销,而非 NIC 带宽(除非消息很大——见“注意事项”)。
当 P > D 时,应用远远领先于 driver,消息在 term buffer 里堆积。每条消息在那里的等待都是纯粹的发送端延迟——
对接收端的丢包和反压计数器完全不可见,因为没有任何丢失,流控窗口也从不是瓶颈。
把 driver 视为一个恒速排空队列的服务器。对于以速率 P 派发的 N 条消息的突发,堆积在派发结束时达到峰值,
最后一条消息等待最久:
峰值堆积 Q = N × (1 − D/P) (P ≤ D 时为 0)最大发送端排队延迟 = Q / D = (N / D) × (1 − D/P)最清晰的解读方式:延迟目标 T 最多允许 B = D × T 条消息的堆积。 其余一切由此推出。
- 未配速(tight-loop)突发,
P → ∞:延迟下限是N / D。一个N条的 tight-loop 突发仅当N ≤ D × T时才满足目标T。 - 配速突发:要把
N控制在T以内,把派发配速到至多:
P_max = D / (1 − T × D / N)- 持续流(而非有限突发):队列仅当
P ≤ D时才有界。任何持续的P > D都会让堆积无限增长,没有任何目标 能扛住它。最大可持续派发速率就是D。
测量你的排空速率 D
Section titled “测量你的排空速率 D”D 是模型唯一需要的经验常数。直接测量它——不要靠猜:
- 用单个 publisher 跑一个已知大小
N(例如 90,000 条)的未配速突发。 - 在发送端 NIC 上抓取这次突发,对每条消息计算其发送端排队时间
=
driverSendTimestamp − applicationOfferTimestamp。(Aeron 的channel-snd-ts-offset会把 driver 发送 时间戳写入 payload;offer 时间戳由你自己写在相邻字段。) - 排队时间随突发中的位置线性上升,最终封顶于
max ≈ N / D。读出最大值并求倒数:
D ≈ N / (最大发送端排队延迟)一个有代表性的单线程 Sender 测量,160 字节消息,AWS 上的现代 x86:
| 量 | 值 |
|---|---|
突发大小 N | 90,000 条 |
| 最大发送端排队延迟(未配速) | ≈ 55 ms(最差观测 ≈ 67 ms) |
推导排空速率 D | ≈ 1.6 M msg/s 典型;≈ 1.3 M msg/s 保守 |
| 等效线上速率 | ≈ 3 Gbps 瞬时(远低于 NIC 的 ~10+ Gbps) |
做硬性 SLA 边界时用保守 D——它为 GC 暂停和调度抖动留了余量。典型值是在安静、已 pin 的核上看到的数字。
代入典型的 D ≈ 1.6 M msg/s 和 N = 90,000:
目标 T | 最大瞬时(tight-loop)突发 D×T | 把 N=90k 突发配速到 P_max |
|---|---|---|
| 0.5 ms | ≈ 800 条 | ≈ 1.61 M msg/s |
| 1 ms | ≈ 1,600 条 | ≈ 1.63 M msg/s |
| 5 ms | ≈ 8,000 条 | ≈ 1.76 M msg/s |
| 10 ms | ≈ 16,000 条 | ≈ 1.95 M msg/s |
| 25 ms | ≈ 40,000 条 | ≈ 2.88 M msg/s |
两种读法:
- 如果你的突发足够小(
N ≤ D×T),tight-loop 已经满足目标——无需配速。在T = 1 ms时这个上限仅约 1,600 条, 这正是大突发总是需要配速的原因。 - 如果突发更大,把派发配速到
P_max。P_max可以超过D,因为堆积只在有限的注入窗口内累积;真正的约束是注入 结束时的堆积D×T。当T逼近未配速下限N/D(此处约 56 ms)时,P_max → ∞——配速毫无意义,因为 tight-loop 本身已经满足这个宽松目标。
- 在源头配速突发。 最省事的修复是把突发摊开,让派发永不超过 driver。多数突发型 publisher 都暴露了速率旋钮;
按上面的
P_max设置它。 - 这与芯片无关。
D由单个Sender线程的每条消息开销决定,因此同一个 publisher 在不同 CPU 厂商上会呈现相同的 发送端排队尾部。别把它当硬件问题去追。 - 它与接收端问题叠加。 同一个建立发送端队列的未配速突发,也会在接收端投递出最陡的微突发。 给发送端配速能一次缓解两端。
- idle strategy 决定
D。 busy-spin 的Sender排空更快(D更高),sleeping 的更慢;见 Smart Batching 与 Idle Strategy。不要加应用层批处理来“帮忙”—— driver 已经通过sendmmsg()合并,批处理只会增加等待时间。
| 接收端微突发 | 发送端排队 | |
|---|---|---|
| 时间花在哪 | OS socket / RX ring 溢出 → NAK 恢复 | 等待 driver Sender 的 term buffer |
| 确凿证据 | UdpRcvbufErrors、NAK 尖刺上升 | 所有丢包/反压计数器保持为零 |
| 受限于 | SO_RCVBUF、rmem_max、ENA ring、窗口 | 单个 Sender 线程排空速率 D |
| 修复 | 配置接收路径缓冲区 | 把派发配速到 P_max = D / (1 − T×D/N) |
关于 media driver 的 Sender 线程在内部如何合并与序列化发送,见 The Aeron Files。
本文专注于操作模型,以及如何按延迟预算配速。
本站与 Adaptive Financial Consulting Limited 或 Aeron 项目无任何关联,未获其背书或赞助。 Aeron 是 Adaptive Financial Consulting Limited 的注册商标。
Aeron 是 Adaptive Financial Consulting Limited 在英国及其他国家/地区的商标。