Google's BBR TCP拥塞控制算法的四个变速引擎

台风海马来临前的两个几乎通宵的夜晚,我用一篇关于BBR算法的文章的迎接海马!公司在昨晚响应深圳市停工,停课号召,发布了在家办公(请注意,不是放假...)通知...其实吧,我觉得停电才是王道,你觉得呢?
在前一篇关于bbr算法的文章《来自Google的TCP BBR拥塞控制算法解析》(这可能是第一篇中文版的bbr算法相关的文章)中,我简述了bbr算法的框架,算是大致介绍了。本文中,我想深入bbr算法pipe状态机的一些细节,但是我不会继续使用”状态机“这样的术语,我选择使用变速引擎这样的说法,来展示一个现代高速的TCP变速引擎是如何为数据传输提供强劲动力的。
        这组引擎完全从bbr算法中抽象生成,想法来自于我们奔跑或者开车的过程,突然发现,bbr算法的思想精髓简直就和我们在人多的跑道上奔跑以及在高速公路上开车时的思路完全一致啊!
        我不禁感叹万物的统一并诅咒虚无,在胃里灼烧感正嗨的时候,完成了本文!
        就要开始了!

1.Linux TCP迄今为止的拥塞控制机制

我并不了解其它平台的TCP拥塞控制算法实现,但是我了解Linux的,迄今为止,在bbr刚刚被引入之后,Linux的拥塞控制算法分为两类:

保守模式

bbr之前以Reno为基础,包括Reno,NewReno,...原理几乎都不变,这些算法有两个特点:
1).反馈性差
以Reno为例,TCP发送端在拥塞避免阶段收到ACK后,无条件地将cwnd增加1/cwnd,在慢启动阶段收到ACK后cwnd增加1,这是毫无根据的,然而Reno并没有什么更好的做法,只能瞎猜!后来的Westwood,Vegas,BIC等算法,相对Reno/NewReno更加智能了一步,但还是傻瓜!再往后,CUBIC搞了一个高大上的以三次方程凸凹曲线来抉择的增窗机制,看似十分地“博士”,并且十分地“经理”,然而还是无法高效利用互联网的空闲带宽,相反在碰到异常现象,比如丢包,拥塞的时候,反应太过保守,在保守的路线上趋于激烈,即激烈地保守降低拥塞窗口,更加可悲的是,这个窗口下降的过程并不受这些算法所控制。
2).拥塞算法被接管
在TCP拥塞控制机制发现丢包时(即RTO或者N次重复的ACK等),TCP会完全接管拥塞控制算法,自己控制拥塞窗口。然而问题是,这种所谓的丢包可能并不是真的丢包,这只是TCP认为丢包而已,这是30年前的丢包判断机制了...真的丢包了吗?不一定啊!然而只要TCP认为丢包,就会接管拥塞控制算法(起码在Linux上是这样...)。这使我不得开心颜!我曾经修改过Linux TCP的PRR逻辑,只求降窗过程不那么猛而已...Linux TCP为了这个降窗也是费尽心思,先后经历了多种方案,比如Halving RatePRR等,唉,干嘛不直接都交给拥塞控制算法呢??

        总的来讲,bbr之前的拥塞控制逻辑在执行过程中会分为两种阶段,即正常阶段和异常阶段。在正常阶段中,TCP模块化的拥塞控制算法主导窗口的调整,在异常阶段中,TCP核心的拥塞控制状态机从拥塞控制算法那里接管窗口的计算,在Linux的实现中,这是由以下逻辑表示的:

static void tcp_cong_control(struct sock *sk, u32 ack, u32 acked_sacked,
                 int flag)
{
    if (tcp_in_cwnd_reduction(sk)) { // 异常模式
        /* Reduce cwnd if state mandates */
        // 在进入窗口下降逻辑之前,还需要tcp_fastretrans_alert来搜集异常信息并处理异常过程。
        tcp_cwnd_reduction(sk, acked_sacked, flag);
    } else if (tcp_may_raise_cwnd(sk, flag)) { // 正常模式或者安全的异常模式!
        /* Advance cwnd if state allows */
        tcp_cong_avoid(sk, ack, acked_sacked);
    }
    tcp_update_pacing_rate(sk);
}

是否进入tcp_cwnd_reduction的异常模式,是由下面的逻辑来判断的:
if (tcp_ack_is_dubious(sk, flag)) {
    is_dupack = !(flag & (FLAG_SND_UNA_ADVANCED | FLAG_NOT_DUP));
    tcp_fastretrans_alert(sk, acked, is_dupack, &flag, &rexmit);
}

这个会让TCP拥塞模块怎么想?!除了抛出一个ssthresh之外对异常的处理无能为力,事实上这根本与它无关!

全速模式

bbr之后以bbr为基础的算法,其核心并不是bbr本身,而是bbr算法为了运行对Linux TCP框架的修改!bbr并不是最终的算法,更不是神话,它只是个开始,它把TCP的tcp_ack完全改了,改了之后,如果你有什么好的想法,便可以自由发挥了!
        都改了什么呢?我们看一下核心函数tcp_cong_control就知道了(我这里不谈2.6内核以及3.x内核中没有抽离cong_control的版本):
static void tcp_cong_control(struct sock *sk, u32 ack, u32 acked_sacked,
                 int flag, const struct rate_sample *rs)
{
    const struct inet_connection_sock *icsk = inet_csk(sk);
    // 这里是新逻辑,如果回调中宣称自己有能力解决任何拥塞问题,那么交给它!
    if (icsk->icsk_ca_ops->cong_control) {
        icsk->icsk_ca_ops->cong_control(sk, rs);
        // 直接return!TCP核心不再过问。
        return;
    }
    // 这是老的逻辑。
    if (tcp_in_cwnd_reduction(sk)) {
        /* Reduce cwnd if state mandates */
        // 如果不是Open状态...记住,tcp_cwnd_reduction并不受拥塞控制算法控制!!
        tcp_cwnd_reduction(sk, acked_sacked, flag);
    } else if (tcp_may_raise_cwnd(sk, flag)) {
        /* Advance cwnd if state allows */
        tcp_cong_avoid(sk, ack, acked_sacked);
    }
    tcp_update_pacing_rate(sk);
}

bbr算法实现了cong_control回调函数,而你可能实现一个别的,写在cong_control中。
        理解了现状,当然就知道问题之所在了,bbr解决了问题。当然,Appex也解决了问题,但是那帮人是傻逼!这不符合耶稣的逻辑,Appex会受到诅咒。

2.bbr并不基于预测

不基于数据反馈的预测都是假的,不真实的,骗人的。这就是为什么基于大数据的人工智能比基于算法的人工智能更厉害的原因,算法再猛都是扯淡,只有数据才能训练出聪明的模型。在TCP拥塞控制算法领域,CUBIC的三次凸凹函数看起来是那么的晦涩却显得高大上,大多数人无力深究这个算法的细节,然而bbr算法却是一个随便什么人都看得懂的....

        bbr不断采集连接内时间窗口内的最大带宽max-bw和最小RTT min-rtt(见下面的win_minmax),并以此计算发送速率和拥塞窗口,依据反馈的实际带宽bw和max-rtt调节增益系数。其背后的思想是,一旦在一个时间窗口内采集到更大的带宽和更小的rtt,bbr就认为客观上它们的乘积,即BDP是可以填充的客观管道容量,并按此作为基准,万一没有达到,bbr会认为这是发生了拥塞,调小增益系数即可,但在时间窗口范围内并不改变基准(时间局部性使然),由于增益系数是根据反馈调节的且基准BDP不变,一旦拥塞缓解,bbr可以第一时间发现并增大增益系数!

        bbr能做到以上这些并工作地很好,全在于tcp拥塞状态机控制逻辑不会打扰它的行为,而这些在之前的拥塞控制算法中几乎是做不到的,比如CUBIC算法,一旦丢包,CUBIC便会被接管,直到Linux按照硬性标准判断其TCP拥塞状态已经恢复到了Open状态。
        此外,更加重要的,bbr算法采用真实的值,它的带宽bw是真实测量出来的(测量方法见《来自Google的TCP BBR拥塞控制算法解析》),而RTT则包含了原始数据包的RTT以及SACK数据包的RTT等所有可以测量的RTT,然而我们无法预测一个TCP连接的生命周期到底有多久,为了让采样结果更加平滑,几乎所有的算法都会采用“移动指数平均”的方案,RTO为了展示波动的影响,加入了一个srtt的类似方差的值,但是这些都是不真实的。brr算法采用了真实的值!
        我们知道移动指数平均是不真实的,我们同样知道间隔久远的两个采样值之间几乎没有关联的,如何来解决长时间的波动性呢?这就是将要谈到的win_minmax机制。

3.win_minmax原理

在bbr中,所有提到的关于最大值最小值的采集都是采用win_minmax机制得到的,在我的文章中,在我提到最大带宽和最小RTT的时候,指的并不是TCP连接的生命周期内的最大带宽和最小RTT,而是指的基于win_minmax的最大值和最小值。
        win_minmax的原理非常简单,简单来讲,它可以记录一个时间窗口内的一个变量的最大值或者最小值,并没有使用任何平均值,一切都是真实的原始值。之所以说一个时间窗口内的最大值或者最小值是可信的,背后的思想是时间空间局部性原理,可计算的马尔科夫到达过程可以说明时间局部性,而互联网的任何角落都不会突然产生或者吞噬大量的流量则表征了空间局部性,利用局部性原理,我们就可以说“3秒或者5秒内的最大带宽或者最小RTT是可以达到的。”
        win_minmax下的最大值和最小值计算也是非常简单的,如果没有时间窗口限制,那么如果要求得带宽的最大值,就要在TCP连接生命周期内持续冒泡,直到连接结束才能得出结果,由于大多数时候不得不采用移动指数平均,那么偶尔一次的噪点可能会影响后续结果的真实性,win_minmax则不同,它不必在连接内持续冒泡,只要在时间窗口内的3个值中做比较即可。这是bbr所谓的事不过三的第一例。
        win_minmax背后的思想是,很久之前(比如时间局部性之外的5分钟之前)的最大值或者最小值对于一个控制系统(比如TCP)来讲是没有意义的,但是又不想让这种值影响到我(无疑使用移动指数平均算法是肯定会影响的)。
        win_minmax的代码不到100行,非常简单,请自行查阅lib/win_minmax.c文件。
        值得注意的是,bbr的实现中,最小RTT并没有采用win_minmax的算法,我们从其结构体的注释中可以看到:
/* BBR congestion control block */
struct bbr {
    u32    min_rtt_us;            /* min RTT in min_rtt_win_sec window */
    u32    min_rtt_stamp;            /* timestamp of min_rtt_us */
    u32    probe_rtt_done_stamp;   /* end time for BBR_PROBE_RTT mode */
    struct minmax bw;    /* Max recent delivery rate in pkts/uS << 24 */
    ...
};
我们注意表示最小RTT和最大带宽的min_rtt_us和bw字段,后者就是一个win_minmax变量,而前者只是一个u32类型变量。但是看min_rtt_us的注释,就会发现它确实不是连接内的最小值,而只是在时间窗口min_rtt_win_sec内的最小值,然而全文搜索窗口min_rtt_win_sec,并没有找到其定义!这样的歧义性当然不能让人满意,于是找一下更新这个RTT的代码:
/* Track min RTT seen in the min_rtt_win_sec filter window: */
filter_expired = after(tcp_time_stamp,
               bbr->min_rtt_stamp + bbr_min_rtt_win_sec * HZ);
if (rs->rtt_us >= 0 &&
    (rs->rtt_us <= bbr->min_rtt_us || filter_expired)) {
    bbr->min_rtt_us = rs->rtt_us;
    bbr->min_rtt_stamp = tcp_time_stamp;
}
我们看到,bbr_min_rtt_win_sec就是bbr结构体里注释说的min_rtt_win_sec,默认是10秒。以上的这段代码表示:
1).在时间窗口min_rtt_win_sec内,冒泡取最小值更新min_rtt_us;
2).一旦超过了一个min_rtt_win_sec时间窗口周期,无条件用新值更新min_rtt_us,开启新一轮冒泡。

这就是所说的”min RTT in min_rtt_win_sec window“。
        至于rs->rtt_us的采集,为什么不直接取系统的srtt呢?并不属于本节win_minmax的原理的范畴,我单独整理了附录,见本节附录。
        到这里,你的疑点可能是,为什么不用win_minmax来实现”min RTT in min_rtt_win_sec window“的采集机制呢?这个问题的答案最终可以揭开win_minmax的终极面纱!
        要理解这个,首先你要知道,为什么”最大带宽“的值就可以用win_minmax来计算,然后再考虑一下最小RTT是用来干什么的,基本就知道答案了!前文提到win_minmax的基本原理,它的作用是”保存“一个时间窗口内的”最有意义的最大值或者最小值“,在时间流逝的过程中,时间窗口不断向后滑动,基于win_minmax,在需要时间窗口内极值的时候,你只要取保存的3个值中的第一个值就好了。win_minmax算法本身保证了时间窗口随着时间的滑动已经窗口内极值的冒泡更新,如下图所示:




对于最大带宽而言,bbr只是”记录并使用“,类似LRU。

        而对于最小RTT则完全不同,最小RTT的作用除了计算窗口之外,还有一个作用就是触发PROBE_RTT引擎(参见第6节)的运作,因此,对于”min RTT in min_rtt_win_sec window“中的min_rtt_win_sec而言,在采集到新的最小RTT前,窗口是不会向前滑动的,窗口的不自动滑动最终会触发一个”过期“事件,而这个过期事件正是切换到PROBE_RTT引擎的契机!好了,我们现在可以总结一下最大带宽的采集和最小RTT采集之间的不同了:

最大带宽采集:使用窗口随着时间自动滑动的win_minmax算法进行更新。
最小RTT采集:使用不会随时间自动滑动的窗口机制,仅在最小RTT更新时滑动,在时间触及窗口边缘时触发超时事件。

所以说,最小RTT的采集并不使用win_minmax!

附:关于rs->rtt_us的采集

一个最大的疑问就是,为什么不直接使用系统的srtt来更新bbr的min_rtt_us?
        答案很简单,因为srtt是经过移动指数平均的,虽然已经过滤了噪点,但还是会受到噪点的影响,这里的噪点问题主要是BufferBloat的影响,因此这个srtt并不受bbr信任!此外,系统的srtt计算时并不会针对被SACK的数据包采集RTT样本,而bbr并不在乎ACK和SACK,不在乎丢包和乱序,所以bbr必须自己维护一套采集真实RTT(起码相对真实)的方法。
        我们看到,bbr的min_rtt_us来自rate_sample结构体rtt_us的赋值,我们也可以看到,rs->rtt_us的值是在TCP主逻辑收到ACK的时候更新的,位于tcp_clean_rtx_queue函数中被更新:
/* 如果这是被正常顺序ACK的数据包,那么ca_rtt_us就取当前时间和最后被ACK的数据包发送时间之差。*/
if (likely(first_ackt.v64) && !(flag & FLAG_RETRANS_DATA_ACKED)) {
    seq_rtt_us = skb_mstamp_us_delta(now, &first_ackt);
    ca_rtt_us = skb_mstamp_us_delta(now, &last_ackt);
}
/* 如果数据是被SACK的,那么同样,ca_rtt_us也是取当前时间与最后被SACK的时间之差 */
if (sack->first_sackt.v64) {
    sack_rtt_us = skb_mstamp_us_delta(now, &sack->first_sackt);
    ca_rtt_us = skb_mstamp_us_delta(now, &sack->last_sackt);
}
sack->rate->rtt_us = ca_rtt_us; /* RTT of last (S)ACKed packet, or -1 */

以上的代码很好理解,注释已经很清楚了。


4.bbr的4个核心引擎

理解了基本思想,就可以展示bbr核心的引擎了!在展示之前,要明白的是,在TCP连接开始引擎发动之后,就不会有人去打扰它了!它会一直运行到TCP连接的结束。计算遇到丢包,就算遇到乱序,bbr也是全速发送数据。请看下面的注解:
“就算已经出现了丢包,但bbr并不在乎,这并不属于它管辖,bbr仅仅要求有数据可发就好,换句话说,它相信tcp的拥塞状态机控制逻辑可以把数据准备好,不管是新数据还是标记为Lost的重传数据”
标记Lost并不归bbr管辖,但如果标记Lost很高效的话,无疑是一种好事,因为它会给bbr提供要发送的数据包(不管是新数据还是重传数据)!而RACK帮bbr完成了这一切!但要记住,这并不是必须的。
        bbr的4个引擎就是4个状态机,这些本来是属于一个整体的,如果画在一起一张图,那简直就是一个8缸引擎了,但是那样有点太复杂,所以我把它们拆开了了,用笔连在一起的话,就是全部。我们从STARTUP引擎开始。
注意:
1).阴影的方框里表示的是全局字段,属于一个TCP连接的生命周期;
2).所有的MAX/MIN均采用win_minmax算法(但请注意最小RTT在实现上与最大带宽的不同);
3).每个引擎中,类似门电路,从上到下的空间顺序表示了计算发生的时间顺序,比如越在上面的逻辑越早发生。

4.1.STARTUP引擎细节

这个引擎如下图所示:




这是一个快加速阶段,不再像传统TCP慢启动那般盲目。

4.2.DRAIN引擎细节

这个状态比较有意思,DRAIN是为了排空什么呢?先看下逻辑框图:




之所以要DRAIN,是因为在进入DRAIN状态之前多发了一些数据造成了拥塞,吃了才会拉...那么多发了哪些呢?仔细看STARTUP图示边上的注释也许你就明白了。

4.3.PROBE_BW引擎细节

图示如下:




这是一个稳定状态,该状态非常重要,相当于你在告诉公路上跑得最欢的时候那种状态。注意图中的那个顺序循环的8个增益系数,在代码中表现为一个增益系数数组:
static const 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, BBR_UNIT, BBR_UNIT,    /* cruise at 1.0*bw to utilize pipe, */
    BBR_UNIT, BBR_UNIT, BBR_UNIT    /* without creating excess queue... */
};
这个数组中,6个增益系数都是1,也就是说,6种情况下都是完全基于反馈回来的在10轮bbr周期内最大带宽和最小RTT来设置速率和cwnd(在bbr的当前实现中,处在PROBE_BW状态时,计算cwnd使用的增益系数并不是bbr_pacing_gain数组定义的,而是固定的2*BBR_UNIT,但是我个人感觉还是使用另一个数组比较好...)的。另外两个增益系数,一个是5/4,这个值大于1,如注释所说,这是为了探测更多的带宽,即时利用其它TCP清空的带宽,另外一个小于1的增益系数则是为了收敛,这是不是跟高速公路上开车一样呢?虽然油门和刹车就在脚下,但也只是将脚轻触其上,时不时看车少了,加下速,偶尔看到前面车子越来越近了,也会稍微减速,但是除非严重拥堵,速率几乎是匀速的。
        好了,看到这里,一种TCP加速方案在中国好司机的心里便生成了,即将增益系数改的大一些,比如5/4改成6/4,将后面6个1改几个为大于1的...

4.4.PROBE_RTT引擎细节

请参见第6节!


将以上4个引擎连接起来,bbr实现的状态机就完全有了。

5.bbr全速模式的含义

一般而言,汽车在告诉公路上飞驰的感觉和在城市干道蠕动的感觉是完全不同的。以手动档车子为例,在城市干道开车,会非常累,因为要经常等红灯,经常经历拥堵,...换挡,刹车,油门,离合...搞不好就出事故了...然而在高速公路上,却是非常轻松,基本上在过了收费站之后,一边脚踩油门,一边眼观即时流量,加速到比较适合的速度后,然后刹一下车,此后基本不用再加速减速了,除非偶尔发现前车太慢,刹一下车,或者看到前面没车,踩一脚油门...bbr就是这么个情形,完全根据上次的反馈来调节这次的行为。
        bbr算法消除了不必要的锯齿。这种锯齿在bbr之前简直就是TCP的动力源,各种算法盲目地增窗,一旦TCP认为丢包发生(虽然可能并不是真的丢包。所以才有了各种越来越复杂的机制,比如DSACK之类的...),在留下一个几乎拍脑袋拍出来的ssthresh之后,所有逻辑均被接管,而这里就是锯齿的齿尖之所在。事实上,锯齿是由于TCP拥塞状态机控制逻辑和TCP拥塞控制算法之间在拥塞事件发生时“工作交接”而形成的,bbr算法中取消了这种不必要的交接,因此锯齿也自然变钝甚至磨平了。
        不是Vegas,CUBIC等无法发现拥塞,是TCP并不将权力全权交给它们从而导致的Vegas,CUBIC等如此眼瞎如此盲目。这事实上可能是最初的TCP实现中的做法,比如ssthresh这个概念,事实上很多算法中并不需要这个东西,只是为了迎合“大师的标准”罢了。bbr没有使用ssthresh(ssthresh体现了拥塞算法与TCP拥塞状态机之间的耦合,bbr没有这种耦合,所以不需要ssthresh)。

6.关于bbr周期和PROBE_RTT状态

最后,PROBE_RTT这个状态比较特殊!之所以这么说是因为其它任何状态都是可以进入PROBE_RTT状态的,进入这个状态的条件完全是异步的,即:
连续设定好的时间窗口内(即min_rtt_win_sec,默认10秒)没有采集到新的最小RTT,就会进入PROBE_RTT状态。这个时间段是可以配置的,默认是10秒钟...这貌似有点太久了...
这句话的意思是说,连续一段时间(min_rtt_win_sec窗口内)采集到的RTT均比系统已保存的最小RTT(上个min_rtt_win_sec窗口内的最小RTT)更大,这说明了什么?这说明十有八九发生了拥塞,既然最小RTT曾经达到过,那么它就是可达到的,如今没有达到,那一定是发生了什么事情阻止系统RTT达到最小RTT。这个事件一定是拥塞!要知道,噪声丢包是不会延长RTT的!
        请注意以下这个任何引擎中都存在的局部图放大后的样子:


这个图里实际上存在着很多东西,只是我实在不知道该怎么画出来它。也就是说,任何引擎在运行过程中都可能会切换到PROBE_RTT,这就好像随时刹车减速一样。

....
在PROBE_RTT状态中,cwnd的值会保持一个低值,目标是避免丢包(其实可以说已经检测到了拥塞!),那么什么时候摆脱这个PROBE_RTT状态呢?bbr在这里采用了保守的做法,即采用了事先设置好的“时间段”,在超过这个时间段后再进行一次判断,如果TCP连接当前已经占满了网络管道,那么就会再次进入PROBE_BW,即准匀速状态,如果没有达到网络的满载状态,那么就会进入STARTUP状态:
static void bbr_reset_mode(struct sock *sk)
{
    if (!bbr_full_bw_reached(sk))
        bbr_reset_startup_mode(sk);
    else
        bbr_reset_probe_bw_mode(sk);
}
到底会进入PROBE_RTT状态多久呢?最短也要是bbr_probe_rtt_mode_ms(200ms by default)。
        在这里再次重申一下,所谓网络管道是否满载的判断是当前时间窗口内的最大带宽值来计算的,与当前状态无关。为了避免文字进一步拖沓,我只能将代码列如下:
static bool bbr_full_bw_reached(const struct sock *sk)
{
    const struct bbr *bbr = inet_csk_ca(sk);

    return bbr->full_bw_cnt >= bbr_full_bw_cnt; // 其实可以将bbr_full_bw_cnt看成是3!
}
static void bbr_check_full_bw_reached(struct sock *sk,
                      const struct rate_sample *rs)
{
    struct bbr *bbr = inet_csk_ca(sk);
    u32 bw_thresh;

    // 注意round_start的计算!
    if (bbr_full_bw_reached(sk) || !bbr->round_start || rs->is_app_limited)
        return;

    // 这里是一个简单的冒泡法,只要不是连续的带宽增长小于25%,那么就将计数“不增长阈值”加1,事不过三,超过三次,切换到DRAIN。
    bw_thresh = (u64)bbr->full_bw * bbr_full_bw_thresh >> BBR_SCALE;
    if (bbr_max_bw(sk) >= bw_thresh) {
        bbr->full_bw = bbr_max_bw(sk);
        bbr->full_bw_cnt = 0;
        return;
    }
    ++bbr->full_bw_cnt;
}

上述代码描述了“如何判断带宽已满载”。代码比较简单,只是那个round_start赋值比较模糊(也许你不觉得模糊,但我在刚看bbr的时候觉得这里比较松散...)。关于bbr周期的一些机制,我在这里简述一二。我们先看bbr周期的概念:
“可测量”的一个“发送/接收”周期。
注意,“可测量”三个字特别关键!有人会认为一个周期就是一个SRTT,但是RTT是可测量的吗?No!SRTT是猜的,其结果大部分都是骗人的!因此RTT并不是可测量的(即便是启用时间戳的连接,也不能控制接收端Delay ACK...)那么什么是可测量的呢?
        bbr采用可一种实实在在的方式,我们来看一个周期开始和截止时的判定代码:
/* See if we've reached the next RTT */
if (!before(rs->prior_delivered, bbr->next_rtt_delivered)) {
    bbr->next_rtt_delivered = tp->delivered;
    bbr->rtt_cnt++;
    bbr->round_start = 1;
    bbr->packet_conservation = 0;
}
以上代码如果if语句成立,就意味着进入了一个新的周期,其关键点在于三个变量:
tp->delivered:当前被ACK或者SACK的最大值(与序列号无关,只是标量计数)。它会被赋值给锚点以及紧接着被发送的数据包的scb,赋值给scb的delivered字段。
bbr->next_rtt_delivered:中间变量锚点。保存一个周期开始时被ACK或者SACK的最大值。
rs->prior_delivered:当前被ACK的数据包在发送的时候,被ACK或者SACK的数据最大计数值。当前被ACK或者SACK的数据包的scb中保存有delivered值,赋值给rs->prior_delivered。
基于以上解释,我们很容易明白一个“bbr周期”的概念,即“当前发送的包开始的时间到此包被ACK或者SACK的时间之间的差”!很符合RTT的概念,不是吗?
        有了这个“bbr周期”的概念,就可以很好的理解以上“事不过三”的意思了,一次探测需要一个周期,这个周期内给接收端时间增长接收窗口。

7.全速bbr的依托

话说bbr可以运行在全速模式下,一组变速引擎提供源源不断的强劲动力,然而单单靠承诺的动力还不行,这种承诺的标称功率之外,能源才是最重要的!TCP的能源就是数据包!不断的数据包触发接收端回复ACK,不断的ACK反馈到发送端,作为时钟驱动更多的数据包发送...
        TCP内部还有一个动力源,那就是滑动窗口,如果窗口不再滑动,那么动力就会消失,数据将无法发送。引发窗口不再滑动的原因就是不连续丢包造成的空洞,这些空洞被补上促使接收端按序接收之前,窗口会一直呈卡死状态!
        问题是,如何填补空洞?
        OK,当然是快速重传(而不是超时重传或者TLP之类...),可是,快速重传只会将数据包重传一次!如果重传的数据包又丢了怎么办?!OK,还有LOST Retransmit机制(重传过被判定丢失的数据包再重传一遍!),可是要想使用LOST Retransmit机制需要满足的条件比较苛刻,比如,只要检测到乱序,就不会进入这个逻辑(TCP的保守性格)...总之,你可以认为,快速重传只会将数据包重传一遍,如果再丢了,就只能等超时了。
        有没有什么办法可以快速检测到重传数据的丢失呢?如果有这样的机制的话,可以将这些数据包进行LOST标记,然后就可以为bbr引擎喂入能源了。这种机制当然有,那就是RACK机制!
...
已经要天亮了,加之本文已经过于拖沓,我准备马上重新整理一篇bbr+RACK的文章,然后迎接四级飓风”海马“!
阅读更多
版权声明:本文为博主原创,无版权,未经博主允许可以随意转载,无需注明出处,随意修改或保持可作为原创! https://blog.csdn.net/dog250/article/details/52879298
上一篇关于TCP单边加速的困境
下一篇RACK为TCP BBR提供动力源
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭