漫谈TCP-AIMD/BBR的公平性以及buffer bloat

周三的时候,我发了一则朋友圈:

aimd是公平的这个事实很容易从数学上证明,但是朴素的aimd会带来全局同步。
解决reno家族全局同步的策略就是概率性随机丢弃,避免所有流同时md,然而bbr却没法从数学上证明这种策略是公平的,至少我是证明不了。。。

周六早上3,4点钟自然醒,写篇意识流。


了解TCP Reno/CUBIC的都应该知道,基于AIMD的Reno家族的窗口/时间曲线是一个锯齿状。

锯齿看似是固有的,不可消除的,因为包括CUBIC在内的Reno家族CC(congestion control)算法只有没有检测到丢包,均是无条件AI(加性增窗)的,如此一来拥塞窗口总是会超过实际带宽的限制。至少看起来是这样。

然而,继续分析之前,我们必须回答一个问题:

  • 主机真的有能力发送任意多的数据包吗?

意思是说,如果CC算法提供了大小为W的拥塞窗口,主机一定有能力发送W个数据包吗?并不是!

然而,继续分析之前,我们必须了解一个现实:

  • 顾名思义,拥塞控制是控制拥塞的,独享带宽,没有拥塞,不需要拥塞控制!

我们首先要理解什么是 带宽。

理想情况下,如果我们把网络看作是一根粗细均匀管道,那么带宽就是该管道的横截面积,它一方面表示主机的发包能力,另一方面也表示网络的吞吐率。

我们常说的1Gbits/Sec千兆以太网卡,意思就是一台主机通过该网卡每秒可以发送1000Mbits的数据,这就是主机的发包能力。以一个包1500字节计算,1Gbits差不多就是89478个包,此时即便你提供了10000000的拥塞窗口,主机也只能发89478个包!这种情况下,如果网络带宽被一条TCP流独占,那么Reno的拥塞窗口理论上会无限AI涨下去,不会出现锯齿,而是一条直线!

然而,无限增长拥塞窗口有意义吗?

答案很明确,不但没有意义,还会出问题!

Reno并不理解带宽的概念,它只知道拥塞窗口。假如同一个网络管道中又出现一条TCP流,那么二者势必要分享带宽。需要分享出让带宽的信号就是拥塞丢包,按照AIMD窗口控制理论,当发生拥塞时,二者均会丢包,二者均要执行MD(乘性减窗),然而如果第一个TCP流的窗口已经增长的过大,即便它执行了MD,也依然远大于实际可用带宽,拥塞将持续,丢包将加剧,这就是问题!

因此,拥塞窗口必须维持在 刚刚够用 的状态!我们可以从Linux 2.6.13中新添加的两行代码中看到这个处理:

void tcp_reno_cong_avoid(struct tcp_sock *tp, u32 ack, u32 rtt, u32 in_flight,
             int flag)
{
    // 如果在途包无法填满拥塞窗口(很可能已经达到了最大的发包能力),说明没有增加窗口的必要,直接返回!
    if (in_flight < tp->snd_cwnd)
        return;

        if (tp->snd_cwnd <= tp->snd_ssthresh) {
                /* In "safe" area, increase. */
        if (tp->snd_cwnd < tp->snd_cwnd_clamp)
            tp->snd_cwnd++;
    } else {
                /* In dangerous area, increase slowly.
         * In theory this is tp->snd_cwnd += 1 / tp->snd_cwnd
         */
        if (tp->snd_cwnd_cnt >= tp->snd_cwnd) {
            if (tp->snd_cwnd < tp->snd_cwnd_clamp)
                tp->snd_cwnd++;
            tp->snd_cwnd_cnt = 0;
        } else
            tp->snd_cwnd_cnt++;
    }
}

如果你真的去看代码,会发现在Linux 2.6.13以前的Reno算法中,拥塞窗口真的就是无限增长的…

OK,我们可以确认,在单流独享带宽的情况下,拥塞不会发生,拥塞窗口最终会稳定在BDP不再继续增长,这种情况下,不会出现锯齿。

正如本文开始时所说的,顾名思义,拥塞控制算法是控制拥塞的,如果没有拥塞,显然它们不应该起作用!显然,均匀带宽理想情况下,当一个网络只有一条流通过时,不可能会有拥塞,因此拥塞控制毫无意义,也就不必纠结这个时候为什么不会出现锯齿了。

然而,在现实中,网络并不是一根粗细均匀的管道,因此对于端到端主机而言,带宽特指该主机的可用带宽,就是这个网络管道中最细位置的截面积,即瓶颈带宽(Bottleneck)。瓶颈带宽的传输能力很可能小于主机的发包能力,这个时候就需要拥塞控制来应对了。

换句话说,在现实的不均匀网络中,拥塞控制的目标之一就是探测瓶颈带宽,控制主机发包速率使流量恰好通过瓶颈带宽。显然,BBR正是围绕这一目标被设计的。

回到我们的理想情况,理想单流假设的前提下,主机的发包能力决定了拥塞窗口的上限,这个过程无需任何外在干预,单流完全可以自适应网络带宽,Reno算法只是单纯的将拥塞窗口慢启动或者AI到BDP而已(在此我们忽略随机噪声丢包)。理想单流的情况,网络就是一条单纯的管道,其中不需要buffer!

然而如果多条流复用同一个网络,网络管道必然会过载,此时需要需要引入buffer来暂存过载的数据包,buffer部署在交换节点,如果没有buffer,网络将退化到CSMA/CD总线,丢弃所有过载的数据包。详见:
https://blog.csdn.net/dog250/article/details/90446782

当多个流复用一个部署了buffer的网络,在端到端尚未感知到网络过载之前,buffer的作用就是暂存过载的数据包,然后等待端主机在感知到拥塞的时候自行执行MD从而排空buffer,这就是现代分组交换网络拥塞控制的运作机制。

现在你可能会问两个问题:

  • 为什么MD就能排空buffer?
  • AIMD能保证对所有流都公平吗?

好吧,我来简单的数学证明一下。

假设一种初始状态,一共

N

N

N条流复用同一个网络,它们在网络过载的时刻拥塞窗口分别为

W

1

W_1

W1

W

2

W_2

W2

W

3

W_3

W3,…

W

N

W_N

WN,而整个网络可容纳的数据包量为

W

W

W,即

W

W

W是单流独享网络时的最大拥塞窗口,此时有:

W

=

W

1

+

W

2

+

W

3

+

.

.

.

W

N

W=W_1+W_2+W_3+...W_N

W=W1+W2+W3+...WN

随即它们均感知到了丢包,它们均执行MD,我们跟踪第一条流的窗口值变化,其它的流与此一致。

发生了MD之后,第一条流的窗口变为

W

1

2

\dfrac{W_1}{2}

2W1,此外,如前所述,如果交换节点发生拥塞,那肯定是针对所有流的,即所有流都会感知到拥塞,都会丢包,都会执行MD下降一半的窗口以排空网络,所以说此时网络的总空闲窗口变为为

W

2

\dfrac{W}{2}

2W,接下来大家执行AI,均分这么多的空闲窗口,即每个流增加

W

2

N

\dfrac{\frac{W}{2}}{N}

N2W个窗口。

第一条流在接下来感知到丢包时的窗口分别为:

第2次感知到丢包:

W

t

1

_

1

=

W

1

2

+

W

2

×

N

W_{t1\_1}=\dfrac{W_1}{2}+\dfrac{W}{2\times N}

Wt1_1=2W1+2×NW

第3次感知到丢包:

W

t

1

_

2

=

W

t

1

_

1

2

+

W

2

×

N

W_{t1\_2}=\dfrac{W_{t1\_1}}{2}+\dfrac{W}{2\times N}

Wt1_2=2Wt1_1+2×NW

第4次感知到丢包:

W

t

1

_

3

=

W

t

1

_

2

2

+

W

2

×

N

W_{t1\_3}=\dfrac{W_{t1\_2}}{2}+\dfrac{W}{2\times N}

Wt1_3=2Wt1_2+2×NW

i

+

1

i+1

i+1次感知到丢包:

W

t

1

_

i

=

W

t

1

_

(

i

1

)

2

+

W

2

×

N

W_{t1\_i}=\dfrac{W_{t1\_{(i-1)}}}{2}+\dfrac{W}{2\times N}

Wt1_i=2Wt1_(i1)+2×NW

将上面的式子整理,递推式子分别带入,这其实就是一个等差数列,最终初始窗口

W

1

W_1

W1的作用削减到无所谓:

W

1

_

f

i

n

a

l

=

W

1

2

j

+

1

+

W

2

j

+

1

×

N

+

W

2

j

×

N

+

W

2

j

1

×

N

+

.

.

.

W

2

×

N

W_{1\_final}=\dfrac{W_1}{2^{j+1}}+\dfrac{W}{2^{j+1}\times N}+\dfrac{W}{2^{j}\times N}+\dfrac{W}{2^{j-1}\times N}+...\dfrac{W}{2\times N}

W1_final=2j+1W1+2j+1×NW+2j×NW+2j1×NW+...2×NW

忽略掉无穷小的量

W

1

2

j

+

1

\dfrac{W_1}{2^{j+1}}

2j+1W1,这个约等于:

W

N

\dfrac{W}{N}

NW

好家伙,这就是均分带宽啊!

N

N

N条流,每一条流分得

W

N

\dfrac{W}{N}

NW的带宽!这就是公平!

我们已经知道,当全部TCP流启用Reno家族算法的时候,最终它们将收敛到公平!然而公平归公平,带宽利用率高吗?

上面我们的讨论均基于一种buffer管理策略,即 buffer满载后丢弃后来到达的数据包。 这会带来一个问题,即 全局同步:

  • 所有共享带宽的TCP流同时遭遇丢包。
  • 所有共享带宽的TCP流同时执行MD。
  • 所有共享带宽的TCP流同时MD后带宽大量闲置。

解决这个问题的方法和 避免桥梁垮塌 的方法一致,军队渡桥的时候要用碎步,TCP过buffer的时候要随机丢包,让所有的TCP流不要在同一时刻执行MD即可。

全局同步不是本文的核心话题,就此别过…


我们已经充分理解为什么Reno家族的CC在网络拥塞时为什么必须需要buffer,接下来就聊聊 buffer bloat ,它的成因和现状。在此稍后,我会通过buffer bloat引向BBR公平性的讨论。

buffer bloat很大的成因在于Reno家族(包括BIC/CUBIC)的CC算法看待网络的方式。 Reno的本质问题在于它将网络看成了一个容器而忽略了带宽。 举个现实的例子,Reno将网络看作一个停车场而非一条高速公路。

我们可以从Reno家族的拥塞控制变量cwnd中看出来,cwnd的单位是数据包的个数,而带宽的单位则是单位时间内的数据包的个数。这里可以看出,Reno家族是buffer友好的,却不是带宽友好的。

BBR正好与此相反,我们看到BBR的拥塞控制变量pacing rate的单位就是带宽的单位。因此BBR是带宽友好的,却不是buffer友好的。

Reno的行为促使路由器厂商倾向于在设备中配置越来越大的buffer ,因为这样会让使用Reno家族CC(显然,很长一段时间,Reno家族遍布全世界)的用户觉得他们可以向网络发送更多的数据包,这会显示出自己设备处理能力的强大,因此,设备buffer的大小往往是衡量设备性能的一个重要指标,在Reno看来,这个指标当然是越大越好。

现在让我们想象一个理想的BBR算法实现,并且整个网络均使用这个BBR实现进行拥塞控制。在这种情况下,路由器的buffer将不再有大用,因为BBR总是可以通过不断测量瓶颈带宽和最小RTT(任何排队都会让RTT增加),最终收敛到所有的流均不在buffer中排队。如果整个网络均切换到BBR进行拥塞控制,这势必会促使路由器厂商逐渐放弃将buffer大小作为重要性能指标的策略,从而缓解整个网络的buffer bloat。

buffer对于BBR唯一的作用在于暂存其probe bandwidth阶段超发的数据包 ,这些数据包随即会在接下来的Drain阶段中排空。除此之外,buffer对于BBR(请注意,这里的BBR是理想实现,并且没有Reno家族与其共存)再无用处。

然而,在现实网络中,必然是Reno家族和BBR共存的状态,那么Reno家族和BBR可以相安无事吗?

我们设想一个拥有无穷大buffer的网络,Reno家族和BBR共享该网络的资源。Reno家族倾向于占满尽可能多的buffer,这个行为会导致队列不断变长,测量得到的RTT不断增加,这会将BBR不断逼入probe rtt阶段的超慢速传输。虽然现实网络中不可能拥有无穷大的buffer,但可以确定的是, 即便是有限的buffer,由于空闲buffer耗尽丢包导致的Reno家族MD以及重传行为也仅仅是缓解了上述BBR的窘境,而决非解决。

我常听人们说,在deep buffer场景中,Reno家族的表现比BBR好,在shallow buffer场景中,BBR的表现要更优秀。这其实并不正确!正确的说法应该是,不管那种buffer,只要是buffer,对BBR均不友好,buffer是为Reno家族准备的。说shallow buffer中BBR表现优秀是因为Reno家族在该场景中表现更差而相对的优秀,但绝非真正的优秀!

得到shallow buffer场景中BBR表现比其在deep buffer中表现更好这个结论,完全是因为Reno家族与BBR的共存,且Reno家族在shallow buffer场景中比deep buffer场景中表现更差而产生的错觉。对于Reno家族而言,buffer当然是越deep越好,而对于BBR,无论是shallow buffer还是deep buffer,只要不与Reno家族共存,BBR在两种场景中的表现将趋于一致。

BBR只需要足以应对probe bandwidth的buffer就够了。

虽然BBR有效治愈了buffer bloat,或者至少说是有效缓解了buffer bloat,但遗憾的是,没有方法证明BBR是公平的。在BBR背后是一个静态的成本收益模型,而不是一个AIMD类似的动态反馈模型。我曾经分析过这个成本收益模型:
https://blog.csdn.net/dog250/article/details/82892267

我们想象两条BBR流共享一个100Mbits/Sec的网络,它们恰好同时都处在probe bandwidth状态,它们中的一个pacing rate是90Mbits/Sec,另一个的pacing rate是10Mbits/Sec,总带宽利用率恰好100%. 它们在probe up阶段将使得各自的inflight徒劳地增长1.25倍,然后在drain down阶段落到各自的初始值,即90Mbits/Sec和10Mbits/Sec,如此反复下去,显然这也是一种全局同步,显然这是一种不公平的全局同步!BBR并没有一种显而易见的方式让所有共享带宽的流收敛到公平。

解释为什么Reno家族可以收敛到公平看起来很简单,因为丢包是在buffer中实施的,而buffer是全局的,虽然共享网络的各条TCP流分别执行AIMD,但本质上仍然是由路由器集中控制的,是路由器的丢包促使了各条流AIMD状态的变化。

然而以上的解释对于BBR而言则略显苍白。BBR更像是通过一种主动的测量行为来计算pacing rate的,这种测量行为是在各个端到端主机上分别进行的,这种控制并非集中式的,测量的过程网络侧(指交换机,路由器等交换节点)不加任何干预,很难想象各条独立的TCP流会形成一张相同一致的的带宽分布视图,指导它们收敛到公平。

buffer正是Reno家族的统一控制中心,buffer是一张全局视图,而BBR则没有这个控制中心,没有这张全局视图。BBR的公平性必须由每条流自身来保证,而在这些相互独立的流之间,如何传递公平性信号,这是一个难题!

  • 当有新的空余带宽出让(比如有流退出了网络)时,如何保证争抢的公平性。

  • 当没有空余带宽出让而仅仅维持稳态(持续共享100%带宽利用率)时,如何收敛到公平。

以上两种情况,均要求 带宽越低,probe up系数越大,drain down系数越小。 这显然是一个合理的负反馈模型,可以试着用pacing rate的绝对值来计算系数,从而替代固定的

5

4

\dfrac{5}{4}

45

3

4

\dfrac{3}{4}

43,设两条流的pacing rate分别为

p

1

p_1

p1

p

2

p_2

p2,则probe bandwidth阶段它们的probe up系数和drain down系数分别为:

probe up系数 drain down系数
流1

p

1

+

1

p

1

\dfrac{p_1+1}{p_1}

p1p1+1

p

2

1

p

2

\dfrac{p_2-1}{p_2}

p2p21

流2

p

2

+

1

p

2

\dfrac{p_2+1}{p_2}

p2p2+1

p

1

1

p

1

\dfrac{p_1-1}{p_1}

p1p11

说白了就是让各条流的probe up和drain down的系数彼此交错,以实现奖惩。

如此一来,无论在任何情况,各条流的pacing rate均会向公平中心收敛:

  • 如果一个流的pacing rate很大,那么它将缓慢地probe up并且快速地drain down。
  • 如果一个流的pacing rate很小,那么它将快速地probe up并且缓慢地drain down。

无奈的是,当我大半夜着手改代码的时候,却不知道如何实现pacing rate的归一计算,毕竟它可是要做分母的啊!同时我也不善于用大数模拟浮点计算,于是我只能退而求其次,试图写死一个数组,依然失败。

由于BBR是各自独立测量pacing rate的,因此在第一条流里拿不到

p

2

p_2

p2,在第二条流里也拿不到

p

1

p_1

p1,这便使上面的理论变成了空谈,但还是可以给出一些行得通的策略来的。

下面的这个修改实现了 在有空闲带宽出让时的公平性

 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,    /* probe for more available bw */
+   BBR_UNIT,    /* 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... */
 };
+static int bbr_pacing_gain_rnd[5][8] = {
+   {22 / 16, 10 / 16, 1, 1, 1, 1, 1, 1},
+   {21 / 16, 11 / 16, 1, 1, 1, 1, 1, 1},
+   {20 / 16, 12 / 16, 1, 1, 1, 1, 1, 1},
+   {19 / 16, 13 / 16, 1, 1, 1, 1, 1, 1},
+   {18 / 16, 14 / 16, 1, 1, 1, 1, 1, 1}
+};
+static int bbr_probe_rnd = 4;
...
+       if (bbr->cycle_idx == 0) // 所有周期开始的时候,均应该重新随机
+           bbr->rnd_idx = prandom_u32_max(bbr_probe_rnd);
...
    case BBR_PROBE_BW:
        bbr->pacing_gain = (bbr->lt_use_bw ?
                    BBR_UNIT :
-                   bbr->params.pacing_gain[bbr->cycle_idx]);
+                   BBR_UNIT)*bbr_pacing_gain_rnd[bbr->rnd_idx][bbr->cycle_idx];

这个修改并没有实现多个流共享100%带宽利用率时的公平性,至于如何是好,我还没有修改好。但理还是那个理,既然测量独立进行,何不用自己的pacing rate反着计算呢:

probe up系数 drain down系数

i

i

i

P

(

p

)

=

p

+

1

p

P(p)=\dfrac{p+1}{p}

P(p)=pp+1

D

(

p

)

D(p)

D(p)(单调递增,

p

p

p越大增长越慢,

y

=

1

y=1

y=1为渐近线,上凸)

值得注意的是,这种策略只能在probe bandwidth阶段使用。

经理的领带👔是纯金的。


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

原文链接: https://blog.csdn.net/dog250/article/details/113275864

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍;

也有高质量的技术群,里面有嵌入式、搜广推等BAT大佬

    漫谈TCP-AIMD/BBR的公平性以及buffer bloat

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/405781

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年4月26日 上午9:25
下一篇 2023年4月26日 上午9:25

相关推荐