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

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以外对异常的处理无能为力,事实上这根本与它无关!app

全速模式

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算法倒是一个随便什么人都看得懂的....tcp

        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机制!