BBR 真的抗丢包吗?

时间:2021-09-19 01:14:06

BBR 论文 展示的一幅猛图:
BBR 真的抗丢包吗?

很多人惊讶于 BBR 竟然对丢包无感,稍微近看一点,BBR 只是在 20% 以内的丢包率下对丢包无感,更深入探究,会发现抗 20% 丢包率与 pacing_gain = 1.25 有关。

但这个图还是欺骗了绝大多数人。

注意横轴标度,loss rate = 1% 之前采用 10 倍标度,1% 往后采用 2 倍标度,给人的观感是,描绘 BBR 的那条绿线是平的,哇,丢包无感。即使在 20% 的丢包率下,吞吐依然可以接近 100Mbps 而不是 80Mbps,从而产生广告效应。
幸亏还有一条黑色的 ideal 线揭穿了真相。对数横坐标下,黑色线事实上是一条 y = -x 线,它显示了理想情况下, throughout = 100 * (1 - loss_rate)

事实上,这幅图的真相如下:
BBR 真的抗丢包吗?

上面的图广告效应就不明显了。

简单计算一下 BBR 的抗丢包能力。

计算之前,必须按链路画像分类讨论,考虑以下的链路:
BBR 真的抗丢包吗?

设 loss rate = p,gain = g,由于 sender 侧不限带宽,它可以任意 pacing rate 发送,我们当然希望经过 p 损失后吞吐依然可以保持限速带宽 B,因此:

(g * B) * (1 - p) = B (式子右边的 B 是图右边的 B) x100*0.99 = 100

此时 g * B 作为整体,视为从 sender 网卡发出的 rate,经过 p 损失后,delivery rate 为 B,这就是一般意义上所谓的 “抗丢包” 神话,代价是 “从 sender 侧一直到丢包点” 需提供足够的余量发送带宽,用来包容丢包损失。
更严格的约束如下:
BBR 真的抗丢包吗?

这种情况下 sender 侧不再有余量带宽可供 probe 增益,因此:

g * (B * (1 - p)) = B (式子右边的 B 是图左边的 B)

和上一种情况相反,此时 B * (1 - p) 作为整体,表示经过 p 损失后的 delivery rate。这种情况很容易理解,单位时间内发 10 个包,50% 丢包率丢 5 个,receiver 永远只能收 5 个,p 的丢包率需要多发的 g * B - B 的数据来重传补偿。

第二种情况相当于从水管一端注水,水管是漏的,中间漏掉一部分,水管另一段不可能流出与注水等量的水。

设 p = 99%,B = 100,上述两种情况各举一例。

情况一,sender 发 g * 100 * 0.01 = 10000 个包,丢 99%,还剩 100 个,打满 100 带宽。

情况二,sender 只能发 g * (100 * 0.01) = 100 个包,丢 99%,还剩 1 个,只能打满 1% 带宽。

现实场景应是上述两者结合,如果 firstmile 丢包限速,就是后一种,若 lastmile 丢包限速,就是前一种。可能还有更复杂融合,理论上不矛盾。

同时,这两种情况提示了一种链路整形原则,loss 一定要离 sender 越近越好,坏事尽早发生,限速要离 receiver 越近越好,给补偿留足余量。lost 在前,限速在后,提高效能。

但现实中,BBR 的表现如何呢?设置 p = 20%,g = 1.25,得到接近 80% * B 的实际吞吐,但根据上述公式,考虑上面第二种情况,p = 40%,g = 1.7,却没有得到 B * 60% 的吞吐。Why?

周五早上起床很早,做了些测试和 fix,发了一则朋友圈:
BBR 真的抗丢包吗?
我来说说是怎么回事。

按照 BBR 算法逻辑,首先,连续两次 ProbeUP 必须在 10 round 以内才可能保持住 maxbw 以防止 maxbw 以 p 衰减下去,其次,每次 ProbeUP 必须探测到真实的上限才能获得真实的 maxbw,然而 BBR 却被 loss 打断:

/* A pacing_gain > 1.0 probes for bw by trying to raise inflight to at
 * least pacing_gain*BDP; this may take more than min_rtt if min_rtt is
 * small (e.g. on a LAN). We do not persist if packets are lost, since
 * a path with small buffers may not hold that much.
 */
if (bbr->pacing_gain > BBR_UNIT)
        return is_full_length &&
                (rs->losses ||  /* perhaps pacing_gain*BDP won't fit */
                inflight >= bbr_inflight(sk, bw, bbr->pacing_gain));

只要检测到 loss,就退出了 ProbeUP。BBR 的理由是 “We do not persist if packets are lost, since a path with small buffers may not hold that much.” 它害怕一些小 buffer 的链路兜不住 g * BDP 这么大的 inflight,所以才对 loss 进行反应。

我认为这个对 loss 反应的理由不伤大雅,挺合理,但这只是一个微小的假设,却放在 ProbeUP 的关键路径无条件执行,只要发生 loss,即便不是因为小 buffer 无法 hold that much,也会退出 ProbeUP,这让 ProbeUP 更容易违背 “raise inflight to at least pacing_gain*BDP” 的承诺。此其一不合理之处。

再看 bbr_set_cwnd_to_recover_or_restore,同样对 loss 进行反应,一旦检测到 loss 便进入数据包守恒,此时便失去了 cwnd_gain = 2 的 cwnd 增益,如果恰有 new-data 和 retrans-data 需要以 pacing_gain * delivery_rate 发送,数据包守恒可能导致 cwnd-limited。此其二不合理之处。

BBR 理论上对 new-data 和 retrans-data 一视同仁不 care 丢包,依赖 maxbw,minrtt 驱动发送,同时以 secondary controller cwnd 辅助限制 buffer 的侵占。但实际上这些几乎全部被违背。

BBR 不 care 丢包事实上只是在 loss 状态 “记住了 maxbw”,并且多流共存场景下 BBR 会逐渐侵占 buffer 直到 ProbeRTT。和 BBR 不 care 丢包的宣传相反,BBR 对丢包的反应过激,loss 状态下,BBR 无法进行完整周期的 ProbeUP,由于数据包守恒,它甚至没有足够的潜在 inflight 进行 ProbeUP。

如果 BBR 属实如宣传般那样名副其实,它不需要在 loss 状态如此谨慎。BBR 的拥塞自适应逻辑就像一个无级变速装置,早就内置其中,一旦发生拥塞,无需丢包指示,有效测量 delivery rate 会逐步滑跌,而 RTT 也总以 10s 内最小的采样值算数,BDP 在拥塞状态趋向变小,这本身就有拥塞控制的效果。

但仅从算法本身看,我们也能看得出,BBR 对事件反应非常迟钝,所以才需要加些催化剂,但不管怎么说,BBR 名不副实。我一直说 BBRv2 是妥协的产物,它确实是,它最终还是不得不回归 Reno/AIMD 那套逻辑,然而数据包守恒确实也是前 AIMD 时期的产物,如今在 BBR 依然有影子。

将 loss 判断全部忽略,完全靠 BBR 自身收敛,就是我朋友圈发的图了:
BBR 真的抗丢包吗?

预设 40% 丢包率,重传率完全一致,忽略 loss 反应的吞吐要大很多。

如果执念不忽略丢包,还有一种方法。loss 退出 ProbeUP 导致其未竟全功,保留这个逻辑,多几次 ProbeUP 也是等效的。ProbeUP 碰到 loss 的概率随丢包率增加而增加,在 8 round 中多次 ProbeUP 可以累加 ProbeUP 结果,低效 loss 提前退出 ProbeUP 的 inflight 损失:

static int bbr_pacing_gain[] = {
        BBR_UNIT * 5 / 4,       /* probe for more available bw */
        BBR_UNIT * 3 / 4,       /* drain queue and/or yield bw to other flows */
        BBR_UNIT * 5 / 4,       /* probe for more available bw */
        BBR_UNIT * 3 / 4,       /* drain queue and/or yield bw to other flows */
        BBR_UNIT * 5 / 4,       /* probe for more available bw */
        BBR_UNIT * 3 / 4,       /* drain queue and/or yield bw to other flows */
        BBR_UNIT * 5 / 4,       /* probe for more available bw */
        BBR_UNIT * 3 / 4,       /* drain queue and/or yield bw to other flows */
        //BBR_UNIT, BBR_UNIT, BBR_UNIT, /* cruise at 1.0*bw to utilize pipe, */
        //BBR_UNIT, BBR_UNIT, BBR_UNIT  /* without creating excess queue... */
};

但无论怎样,BBR 大开合的本质是改变不了的,这是它所有优势的根源,也是它固有的缺陷。多流共存场景,所有上述看起来合理的假设和更正全部失效,不管是 BBR 本身的假设还是我的假设,都将拉胯,唯一留下来的效果是比其它流高的实际吞吐,比其它流高的重传率,以及对 buffer 的侵占,而这里面成也萧何,败也萧何。

BBR 依赖 gain * delivery_rate 来探测余量带宽,但在丢包场景下,同样行为的语义便成了补偿丢包。delivery_rate 作为 pacing rate 随丢包率衰减,gain 作为增益系数增益补偿重传。可是至少 Linux Kernel TCP BBR 的实现却不是这样,相反,它对丢包反应太过激,以至于 BBR 未竟全功。然而修改却不容易,我们不能光考虑吞吐效率,更要考虑共存公平性,显然这二者对于 BBR 是一个得此失彼的矛盾双方,而 BBR 的大开合就在这两者之间伸展收缩。换句话说,BBR 是一个粗糙的算法。

浙江温州皮鞋湿,下雨进水不会胖。