跳转到内容

发送端排队与突发配速

有一类延迟尖刺能扛过所有接收端的修复。你正确地配置了接收路径缓冲区, 看到零 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 是模型唯一需要的经验常数。直接测量它——不要靠猜:

  1. 用单个 publisher 跑一个已知大小 N(例如 90,000 条)的未配速突发。
  2. 在发送端 NIC 上抓取这次突发,对每条消息计算其发送端排队时间 = driverSendTimestamp − applicationOfferTimestamp。(Aeron 的 channel-snd-ts-offset 会把 driver 发送 时间戳写入 payload;offer 时间戳由你自己写在相邻字段。)
  3. 排队时间随突发中的位置线性上升,最终封顶于 max ≈ N / D。读出最大值并求倒数:
D ≈ N / (最大发送端排队延迟)

一个有代表性的单线程 Sender 测量,160 字节消息,AWS 上的现代 x86:

突发大小 N90,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/sN = 90,000:

目标 T最大瞬时(tight-loop)突发 D×TN=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_maxP_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_RCVBUFrmem_max、ENA ring、窗口单个 Sender 线程排空速率 D
修复配置接收路径缓冲区把派发配速到 P_max = D / (1 − T×D/N)

关于 media driver 的 Sender 线程在内部如何合并与序列化发送,见 The Aeron Files。 本文专注于操作模型,以及如何按延迟预算配速。