跳转到内容

丢包注入与 CUBIC 拥塞控制

Java media driver 自带一个扩展包 —— io.aeron.driver.ext —— 其中有两个在运维上很有价值、但默认 driver 不会为你开启的能力:

  1. 带丢包生成器的 debug channel endpoint —— 主动注入丢包,让你在生产环境替你做这个实验之前先测出恢复行为。
  2. CUBIC 拥塞控制 —— 用一个会在丢包时回退、随后重新探测上升的窗口,替换 Aeron 固定的接收窗口,适用于共享或长肥网络。

两者默认都不接入。本页讲的是运维侧 —— 如何开启、有哪些旋钮,以及它们如何影响 p50 / p99 / 吞吐。至于 Aeron 内部如何检测间隙、重传、施加流控,请参阅 The Aeron Files

第一部分 —— 用于混沌测试的丢包注入

Section titled “第一部分 —— 用于混沌测试的丢包注入”

丢包率由一个系统属性控制,但只设这个属性什么都不会发生。默认的 channel endpoint 根本不会去查丢包 生成器。你必须先换上 debug endpoint,这是另一个独立的属性:

所以一个 10% 接收侧数据丢包的 driver 需要同时设置两个属性:

Terminal window
java \
-Daeron.ReceiveChannelEndpoint.supplier=io.aeron.driver.ext.DebugReceiveChannelEndpointSupplier \
-Daeron.debug.receive.data.loss.rate=0.10 \
-Daeron.debug.receive.data.loss.seed=42 \
io.aeron.driver.MediaDriver

DebugSendChannelEndpointSupplier 是发送侧对应的开关。你用到的丢包旋钮作用在哪一侧,就设哪一侧。

它们由 DebugChannelEndpointConfiguration 读取。每个 rate[0.0, 1.0] 区间内的概率, 每个都有配套的 seed

属性丢弃什么视角
aeron.debug.receive.data.loss.rate / .seed数据接收侧入站 —— 到达接收方的数据
aeron.debug.receive.control.loss.rate / .seed控制(SM / NAK)接收侧出站 —— 离开接收方的控制
aeron.debug.send.data.loss.rate / .seed数据发送侧出站 —— 离开发送方的数据
aeron.debug.send.control.loss.rate / .seed控制(SM / NAK)发送侧入站 —— 到达发送方的控制

默认值:每个 rate 默认为 0.0(无丢包),每个 seed 默认为 -1

决定你的测试是否有意义的两个事实

Section titled “决定你的测试是否有意义的两个事实”

rate 这条路径只用 RandomLossGenerator,仅此而已。 DebugChannelEndpointConfiguration.lossGeneratorSupplier(rate, seed)rate == 0 时短路成一个空操作生成器,否则返回一个 RandomLossGenerator。它的判定逐帧就是 random.nextDouble() <= lossRate。这个包里还有 FixedLossGeneratorMultiGapLossGenerator —— 但系统属性这套机制从不实例化它们;它们用于编程式的测试搭建(例如 JavaTestMediaDriver)。因此:

  • rate 旋钮给你的是均匀随机丢包,而非编排好的间隙模式。
  • 由于每一帧都掷一次骰子,随机丢包也会以同样的概率丢掉重传帧。这对一条有损链路而言是真实的 —— 但意味着高丢包率会让重传不断累积。(相比之下,编排式的 FixedLossGenerator / MultiGapLossGenerator 对一帧只丢一次,并放行它的重传。如果你需要这种行为,得走编程式路径,而不是这些属性。)

seed 决定可复现性。 seed == -1(默认)会构造 new Random() —— 每次运行都是不同的丢包模式, 这会让一个偶发失败无从二分定位。固定任意非负 seed,丢包模式就逐次完全一致,即相同 seed 意味着 相同的测试结果 —— 这是有效 A/B(默认 timer vs 推导 timer)和 CI 的前提。

  • 吞吐受冲击最直接:重传与新数据争抢同一带宽,而且一旦重传开始累积,代价上升得比丢包率本身更快。
  • p99 / 长尾最先被拉长。一个被丢的帧会让订阅方停顿,直到检测 + 重传完成;这段停顿由 NAK/重传 timer 决定 —— 这正是 NAK timer tuning 成为坍缩丢包长尾那个杠杆的原因。
  • p50 在低丢包率下相对稳定 —— 大多数消息第一次就到达 —— 只有当丢包高到影响中位数时才退化。

测量上述任何一项时都要用固定 seed,否则逐次运行的方差会淹没你想读出的效应。

默认情况下,Aeron 接收方通告一个固定窗口(StaticWindowCongestionControl,即 cc=static 策略): 接收窗口不会因丢包而改变。在一条按你的 BDP 大小专建的局域网上, 这是理想的 —— 不做无谓的回退。但在共享或长肥链路上就不然了:固定的大窗口会持续往一条已拥塞的路径里 塞数据,加剧排队丢包和长尾。

CubicCongestionControl 用一个遇丢包收缩、随后沿三次曲线回升的窗口取而代之 —— 与 Linux TCP 默认 算法同一族。它是接收侧控制器:操纵驱动 status message 的接收窗口长度

CUBIC 按 channel 通过 cc URI 参数选择 —— 不需要 driver 级属性,因为 DefaultCongestionControlSupplier 本身就认得它:

// 按 publication/subscription channel:
aeron:udp?endpoint=192.168.1.10:40456|cc=cubic

cc=static(或省略 cc)保持固定窗口的默认行为;cc=cubic 让该 stream 启用 CUBIC。 若要让 driver 范围内的每条 stream 都强制走 CUBIC,则设置 supplier:

Terminal window
-Daeron.CongestionControl.supplier=io.aeron.driver.ext.CubicCongestionControlSupplier

CubicCongestionControlConfiguration 读取:

属性默认值作用
aeron.CubicCongestionControl.measureRttfalsetrue 时实时测量 RTT(RTT 探测),而非使用静态估计
aeron.CubicCongestionControl.initialRtt100us不测量时使用的 RTT 估计;同时也是 RTT 超时的下限
aeron.CubicCongestionControl.tcpModefalse加入 TCP 友好窗口项,使 CUBIC 在低 RTT 区丢包后不会低于 TCP

算法自身的常量在代码中固定:缩放系数 C = 0.4、乘性递减 B = 0.2、初始拥塞窗口 INITCWND = 10 个 MTU。

它如何反应(直接取自控制器)

Section titled “它如何反应(直接取自控制器)”
  • 遇丢包: 把当前窗口记为 w_max,然后将窗口砍到 cwnd × (1 − B) —— 即减少 20%(下限为 1 个 MTU)。这正是固定窗口做不到的回退。
  • 恢复: 窗口沿 W_cubic = C·(T − K)³ + w_max 回升,其中 K = cbrt(w_max · B / C) 是回到 w_max 所需的时间。靠近 w_max 时增长慢、远离时增长快 —— 这就是三次曲线的形状。(类注释给了一个算例:MTU 4 KB、 最大窗口 128 KB ⇒ K ≈ 2.5 s。)
  • tcpMode 开启时: 额外计算 TCP 等效窗口并取较大者,使 CUBIC 在低 RTT 丢包后的区段里至少和 TCP 一样激进。

CUBIC 暴露了两个 per-image 计数器,可用 aeron-stat 实时观察:rcv-cc-cubic-wnd(它当前通告的窗口) 和 rcv-cc-cubic-rtt(它的 RTT 估计)。看着窗口锯齿式上升、遇丢包坍缩,是确认它确实生效的最快方式。

static vs CUBIC —— 各自的适用场景

Section titled “static vs CUBIC —— 各自的适用场景”
cc=static(默认)cc=cubic
最适合按 BDP 大小专建的局域网共享 / 广域网 / 长肥链路
遇丢包窗口不变 —— 继续推送窗口砍 20%,三次曲线恢复
吞吐干净链路上最高干净链路上略低;拥塞链路上更高(避免坍塌)
长尾 (p99)若固定大窗口压垮拥塞路径,可能膨胀拥塞下更紧;回退封顶排队

经验法则:干净专用链路 → 保持 static 并把窗口设为 BDP;争用或长肥链路 → 试 cubic 并对照你的 吞吐目标观察 rcv-cc-cubic-wnd

ext 的这两半可以组合:用 cc=cubic 驱动一条 stream,通过 debug endpoint 注入一个固定 seed 的丢包率, 然后观察 rcv-cc-cubic-wnd 的反应。这就是一个可复现的工作台,用来观察你的拥塞控制在已知丢包下如何表现 —— 这种测试若没有确定性丢包源与动态窗口同时在场,是没法诚实做出来的。

附录 —— C driver 的丢包注入方式不同

Section titled “附录 —— C driver 的丢包注入方式不同”

C media driver(aeronmd使用 Java 的 ext 属性。它通过 UDP transport 拦截器注入丢包, 由环境变量配置:

Terminal window
export AERON_UDP_CHANNEL_INCOMING_INTERCEPTORS="loss"
export AERON_UDP_CHANNEL_TRANSPORT_BINDINGS_LOSS_ARGS="rate=0.2|recv-msg-mask=0xF"
aeronmd

参数是用竖线分隔的 key=value 对:rate(概率,按 double 解析)、seed(无符号),以及 recv-msg-mask(一个十六进制位掩码,选择哪些入站消息类型会被丢弃)。关于你何时会运行 C driver, 参见 Media Driver Selection: Java vs C