本文试图给出一些与BBR算法相关但倒是其以外的东西。算法
注意,我并无把题目定义成网络拥塞的本质,否则又要扯泊松到达和排队论了。事实上,TCP拥塞的本质要好理解的多!TCP拥塞绝大部分是因为其”加性增,乘性减“的特性形成的!
也就是说,是TCP本身形成了拥塞!TCP加性增乘性减的特性引起了丢包,而丢包的拥塞误判带来了巨大的代价,这在深队列+AQM情形下尤为明显。
我尽量快的解释。争取用一个简单的数学推导过程和一张图搞定。
除非TCP端节点之间的网络带宽是均匀点对点的,不然就必然要存在第二类缓存。TCP并没有法直接识别这种第二类缓存。正是这第二类缓存的存在致使了拥塞的代价特别严重。我依然用经典的图做为基准来解释:数组
第二类缓存的时间墙特征致使了排队的发生,而排队会致使一个TCP链接中数据包的RTT变大。为了讨论方便,咱们假设TCP端节点之间管道最细处(即Bottleneke处)的带宽为B,那么正如上图所代表的,我把TCP端节点之间的网络中,凡是带宽比B大的网络均包含在第二类缓存中,也就是说,凡是会引发排队的路径,均是第二类缓存。
假设TCP端节点之间的BDP为C,那么:
C = C1 + C2 (其中C1是网络自己的管道容量,而C2是节点缓存的容量)
因为路径中最小带宽为B,那么整个链路的带宽将由B决定,在排队未发生时(即没有发生拥塞时),假设测量RTT为rtt0,发送速率为B0=B,则:
C1 = B0*rtt0
C = B0*rtt0 +C2 > B*rtt0
此时,任何事情均为发生,一切平安无事!继续着TCP”加性增“的行为,此时发送端继续线性增长发送速率,到达B1,此时:
B0*rtt0 < B1*rtt1
C是客观的不变量,这会致使C2开始被填充,即开始轻微排队。排队会形成RTT的增长。假设C2已经被加性增特性填充到满载的临界,此时发送带宽为B2,即:
C = B2*rtt2 = B*rtt0 + C2缓存
B2*rtt2是定值,rtt2在增大,B2则必须减少!可是”临界值已经达到“这件事反馈到发送端,至少要通过1/2个RTT,在忽略延迟ACK和ACK丢失等反馈失灵情形下,最多的反馈时间要1个RTT。问题是,TCP发送端怎么知道C2已经被填满了??它不知道!除非再增长一些窗口,多发一个数据包!这行为是如此的当心翼翼,以致于你会认为这是多么正确的作法!在发送端不知情的状况下,会持续增长或者保持当前的拥塞窗口,可是绝对不会下降,然而此时RTT已经增大,必须降速了!事实上,在丢包事件发生前,TCP是必定会加性增窗的,也就是说,丢包是TCP惟一能够识别的事件!网络
TCP在临界点的加性增窗行为,目的只是为了探测C2是否是已经被填满。咱们来根据以上的推导计算一下此次探测所要付出的代价。因为反馈C2已满的时间是1/2个RTT到1个RTT,取决于C2的位置,那么将会在1/2个RTT到1个RTT的时间内面临着丢包!注意,这里的代价随着C2的增长而增长,由于C2越大,RTT的最终测量值,即rtt2则越大!这就是深队列丢包探测的问题。
然而,在30多年前,正是这个”加性增“行为,直接导出了”基于丢包的拥塞控制算法“。那时没有深队列,问题貌似还不严重。但随着C2的增长,问题就愈来愈严重了,RTT的增大使得丢包处理的代价更大!
记住,对丢包的敏感不是错误,基于丢包的拥塞探测的算法就是这样运做的,错误之处在于,丢包的代价太大-窗口猛降,形成管道被清空。这是因为深队列的BufferBloat引起的问题,在浅队列中问题并不严重。随着路由器AQM技术的发展,好的初衷会对基于丢包的拥塞探测产生反而坏的影响。
如今,咱们明白了,之因此基于丢包的拥塞控制算法的带宽利用率低,就是因为其填充第二类缓存所平添排队延迟形成的虚假且逐渐增大的RTT最终致使了BDP很大的假象,而这一切的目的,却仅仅是为了探测丢包,自觉得在丢包前已经100%的利用了带宽,然而在丢包后,全部的一切都加倍还了回去!是丢包致使了带宽利用率的降低,而不是增长!!
总结一句,用第二类缓存来探测BDP是一种透支资源的行为。
我一直以为这不是TCP的错,但在发现BBR是如此简单以后,再也不这么认为了,事实上,经过探测时间窗口内的最大带宽和最小RTT,就能够明确知道是否是已经填满了第一类缓存,并中止继续填充第二类缓存,即向最小化排队的方向收敛!曾经的基于时延的算法,好比Vegas,其实已经在走这条路了,它已经知道RTT的增长意味着排队了,只是它没有采用时间窗口过滤掉常规波动,而是采用了RTT增量窗口来过滤波动,最终甚至因为RTT抖动主动减小窗口,因此会形成竞争性不足。无论怎样,这是一种君子行为,它老是无力对抗基于丢包算法的流氓行为。
BBR综合了两者,对待君子则君子(不会填充第二类缓存,形成排队,由于一旦排队,全部链接的RTT均会增长,对相似Vegas的不利),对待流氓则流氓(采用滑动时间窗口抗带宽噪声,采用固定超时时间窗口抗RTT噪声,时间窗口内,决不降速),这是一种什么行为?我以为比较相似警察的行为...
若是不是很理解,那么看看那些高速公路上随意变道或者占用应急车道的行为致使的后果吧(大多数没有什么后果,缘由在于监管的不力,这就好像CUBIC遇到了Vegas同样!)。基于此,即使不使用BBR算法,最好也不要使用基于时延的Vegas等算法,可是也许,咱们能够更好的改进CUBIC,咱们也许已经知道了如何去更改CUBIC了。CUBIC的问题不是其算法自己致使的,而是TCP拥塞控制的框架致使的。见本文”CUBIC更改前奏-实现NCL(非拥塞丢包)“小节。
本节的最后,咱们来看点关于第二类缓存的特性。
第二类缓存既然不是用来进行”BDP探测“(事实上,BDP的组成里根本就该有第二类缓存)的,那要它干什么??
我想这里能够简单解释一下了。第二类缓存的做用是为了适配统计复用的分组交换网络上路由器处理不过来这个问题而引入的。若是没有路由器交换机节点的存在,那么第二类缓存这里什么也没有:框架
若是你想最快速度理解上图中泊松到达这个点的入口行为和固定速率发出的出口行为,请考虑丁字路由或十字路口,和路由器同样,只有在交叉点的位置才须要第二类缓存来平滑多方瞬时速率的不匹配特征!我以丁字路口为例:dom
无论哪里为应对瞬时到达率而加入的”缓存“,都是第二类缓存,这类缓存的目的是临时缓存瞬时到达过快的数据或者车流,这就是统计复用的分组交换网节点缓存的本质!然而一旦这些缓存被误用了,拥塞就必定会发生!误用行为不少,好比UDP毫无节制的发包,好比TCP依靠填满它而发现拥塞,讽刺的是,很大程度上,拥塞是TCP本身形成的,要想发现拥塞,就必需要先制造拥塞。
本节完!tcp
这里仅仅提一点,那就是突发最容易形成排队!这也是能够从泊松到达的排队论中推导出来的。为了避免被人认为我在这里装逼,就不展现过程了,须要的请私下联系我。
解决突发问题的方法有两种,一种就是边缘网络路由器上设置整形规则,这有效避免了汇聚层以及核心层路由器的排队。另一种更加有效的方法就是直接在端主机作Pacing。Linux在3.12内核之后已经支持了FQ这个sched模块,它基于TCP链接发现的Pacing Rate来发送数据,取代了以前一窗数据突发出去的弊端。
Pacing背后的思想就是尽可能减小网络交换节点处队列的排队!经过上一节的最后,咱们知道,交换节点出口的速率恒定,而入口可能会面临突发,虽然在统计意义上,出入口的处理能力匹配便可,然而即使大多数时候到达速率都小于出口速率,只要有一瞬间的突发就可能冲击队列到爆满!事实上队列缓存存在的理由就是为了应对这种状况!
传统意义上,TCP拥塞控制逻辑仅仅计算一个拥塞窗口,TCP发送按照这个拥塞窗口发送适当大小的数据,但这些数据几乎是一次性突发出去的,Linux 3.9以后的patch出现了TCP Pacing rate的概念,能够将一窗数据按照必定的速率平滑发送出去,然而TCP自己并无实现实际的Pacing发送逻辑,Linux 3.12内核实现了FQ这个schedule,TCP能够依靠这个schedule来实现Pacing了。
为何不在TCP层实现这个Pacing,缘由在于TCP层并不能控制严格的发送时序,它是属于软件层的。Pacing必须在数据包被发送到链路以前进行才比较有效,由于这时的Pacing是真实的!切记,Pacing目前能够经过TC来配置,要想Pacing起做用,在其以后就不能再有别的队列,不然,FQ的Pacing Rate就可能会被后面的队列给冲掉!
...
咱们继续谈BBR算法的收敛特性。性能
BBR算法的收敛性与以前基于加性增乘性减的算法的收敛性彻底不一样,比以前的更加优美!欲知如何,我先展现加性增乘性减的收敛图:测试
如下是根据上图总结出来的一幅抽象图:ui
这个图以前贴过,这个图来自于控制论的理论,每一个链接是独立地向最终的收敛点去收敛,你们彼此不交互,只要都奔着平衡收敛点走就行。
当咱们认识BBR收敛性的时候,咱们要换一种思路。即BBR收敛过程并非独立的,它们是配合的,BBR算法根本就没有定义收敛点,只是你们互相配合,知足其带宽之和不超过第一类缓存的大小,即真正BDP的大小,在这个约束条件下,BBR最终本身找到了一个稳定的平衡点。
在展现图解以前,为了简单起见,咱们先假设BBR在PROBE_BW状态,讨论在该状态的收敛过程。咱们先看一下PROBE_BW状态的增益系数数组:
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... */ };
仔细看这个数组,就会发现,bbr_pacing_gain[0],bbr_pacing_gain[1]以及后面的元素安排很是巧妙!bbr_pacing_gain[0]代表,BBR有机会获取更多的带宽,而bbr_pacing_gain[1]则代表,在获取了足够的带宽后,在须要的状况下要出让部分带宽,而后在出让了部分带宽后,循环6个周期,等待其它链接获取出让的带宽。那么,BBR如何安排以上三类增益系数的使能周期长度呢?
很显然,BBR但愿链接尽量多的使用带宽,所以bbr_pacing_gain[0]的使能时间尽量久些,其退出条件是:
已经运行超过了一个最小RTT时间而且要么发生了丢包,要么本次ACK到来前的inflight的值已经等于窗口值了。
虽然BBR但愿一个链接尽量占用带宽,可是BBR的原则是不能排队或者起码减小排队,当另外一个链接发起时,额外的带宽占用会让处在正增益的链接inflight发生满载,所以bbr_pacing_gain[0]会让位给bbr_pacing_gain[1],进而出让带宽给新链接,随后进入长达6个RTT周期的平稳时期,等待出让的带宽被利用。总之,总结一点就是:
若是没有其它链接,一个链接会一直试图占满全部带宽,一旦有新链接,则老链接尽可能一次性或者很短期内出让部分带宽,而后在这些带宽被利用以前,老链接再也不抢带宽,若是超过6个RTT周期以后,老链接从新开始新一轮抢占,出让,等待被利用的过程,从而和其它的链接一块儿收敛到平衡点。
所以,和加性增乘性减的独立收敛方案不一样,BBR一开始就是考虑到对方存在的收敛方案。咱们看一个简单的例子,描述一下大体的收敛思想:
初始状态
链接1:10 & 链接2:0
1>.链接1在一个RTT出让1/4带宽,稳定6个RTT,带宽为7.5 & 链接2以4个RTT为一个PROBE周期分别的带宽为:1.25, 1.55,1.95,2.4
2>.链接1在bbr_pacing_gain[0]占据带宽失败,继续出让带宽,稳定在5.6 & 链接2以3个RTT为一个PROBE周期分别的带宽为3.0,3.75,4.6
3>.完成收敛。
最后,咱们能够看一下BBR的收敛图了:
根据Google的测试,其收敛效果以下图:
经过上图以及bbr_pacing_gain数组,咱们知道了是什么保证了收敛。假设链接1和链接2在网络中的RTT相同(这种假设是合理的,由于在BDP计算中,能够将RTT做为一种权值看待),那么根据PROBE_BW状态内部增益在bbr_pacing_gain数组中转换的规则:
1).BBR_UNIT * 5 / 4的增益要保持多个最小RTT(非滑动的时间内)的时间,直到填满带宽(但要避免排队!因此只要触及带宽便可,即上一次的inflight等于本次的窗口估值)
2).BBR_UNIT * 3 / 4的增益保持的时间要短,最多一个最短RTT时间就退出,在不到RTT时间内,若是排空了部分网络资源也退出。
3).BBR_UNIT的增益紧接在BBR_UNIT * 3 / 4以后,而且持续6个最小RTT的时间。
4).在1)和2)表示的获取和出让之间,保持等比例平衡,默认为当前带宽1/4的获取和出让。
咱们来根据以上规则描述上图中的收敛:
1).在上图的右下边,链接1带宽大于链接2,以上的规则使得链接1一次出让的带宽大于链接2一次获取,所以链接2多个RTT周期内维持在BBR_UNIT * 5 / 4增益,平衡点向沿着带宽线向左上收敛。
2).在上图的左上方,链接2的带宽大于链接1,链接1即便再出让带宽,即使链接2获取了,那链接2回赠的带宽仍是大于链接1的出让,使得链接1比较久维持在BBR_UNIT * 5 / 4增益,向右下收敛。
关于收敛和性能调优的话题,这里还要多说几句。
首先,虽然收敛是必然的,上文已经分析,可是收敛速度如何呢?BBR在Linux实现的目前是第一个版本,其带宽出让环节和带宽获取环节中,增益系数中有一个定值,即3/4和5/4,这两个值的算术平均值为1,这里的意义在于保持一个比例上的平衡。若是将出让系数3/4调大,好比调到1/2,那么获取带宽的增益系数就要调整为3/2,这会让收敛速度更快些。
我着手要作的就是参数化这个bbr_pacing_gain数组:
static const int bbr_pacing_gain[] = { BBR_UNIT * a, // 1<a<2 BBR_UNIT * (2-a), BBR_UNIT, BBR_UNIT, BBR_UNIT, BBR_UNIT, BBR_UNIT, BBR_UNIT, BBR_UNIT, BBR_UNIT, BBR_UNIT // 新增2个周期 };
这里列举几个典型的a值:6/5,5/4,4/3,3/2。所须要的就是将a参数化。
其次,因为以上的讨论都是基于PROBE_BW稳定状态这种理想化场景的,然而现实中你没法忽略STARTUP,DRAIN等状态,为了在测试中找到状态瓶颈,包括带宽利用最大化,收敛公平性,咱们把BBR的状态机看做是一个马尔科夫链:
之因此能够创建这样的模型,是由于BBR全权接管了全部计算窗口和带宽的逻辑,所以这个转换图是闭合且不受外部干扰的。在获得各类转换几率后,咱们基本就能够看出网络的行为了。
好比,调优的目标是尽量让BBR系统运行在PROBE_BW状态,且在PROBE_BW内部,也有一个状态机,咱们但愿尽量的稳定在系数为1的增益上运行。
获取了各类几率数据后,BBR的参数化调优方案就有了基调了。
在上一节,咱们描述了BBR算法在稳定的PROBE_BW状态的收敛性。如今,咱们来看一个异常的可是有广泛意义的场景,那就是发生拥塞的时候,BBR如何表现,如何收敛。因为BBR自己的宗旨就是消除队列,咱们假设某个或者某些链接刚启动时,在STARTUP状态填充了队列的一部分,此时在其进入DRAIN状态以前,队列一直是存在的,因为队列已经开始被填充,那么已有的链接会在至关长的时间内没法采集到更小的RTT,最终,它们几乎会同时进入PROBE_RTT状态!看BBR的PROBE_RTT实现,就知道在这个状态中,cwnd会被瞬间缩减到4个MSS的大小!这会致使大量的网络资源被腾出,而这些腾出的资源会被新链接共享,这就是STARTUP和PROBE_RTT状态的公平性!
可是,若是和CUBIC共享资源怎么办?!
很遗憾,BBR没法识别CUBIC的存在!当BBR将cwnd缩减的时候,CUBIC会继续填充第二类缓存,直到透支掉最后的那一个字节。随后,也许你会认为CUBIC会执行乘性减来缩减cwnd,是的,确实如此,然而即便这样,也不能期望它们会腾出带宽,由于CUBIC的行为是各自独立的,你没法假设它们会同时进入乘性减窗,所以几乎能够确定,共享链路上的缓存老是趋向与被填满的状态,这都是CUBIC的所为。然而怎能怪它呢,毕竟它的基础就是填满全部两类缓存为止,决不降速(不一样于BBR的发现排队以前毫不减速的特性)。所以,BBR和CUBIC共存的时候,颇有可能会出现全盘皆输的局面。
怎么缓解?!
事实上,BBR不必对CUBIC过度谦让。只要知足本身不排队便可(由于排队于人于己均无好处!)。所以大可没必要将窗口降到4个MSS,直接降到一半便可,这也是沿袭了传统乘性减的规则!此外,在PROBE_RTT阶段,也不要在这个状态运行太久,时间减半意思意思就好!为此,很容易更改代码:
0).定义一个新的fast_probe模块参数
1).bbr_set_cwnd的修改:
if (bbr->mode == BBR_PROBE_RTT) { /* drain queue, refresh min_rtt */ if (fast_probe) tp->snd_cwnd = max(tp->snd_cwnd >> 1, bbr_cwnd_min_target); // 取cwnd/2! else tp->snd_cwnd = min(tp->snd_cwnd, bbr_cwnd_min_target); }
2).bbr_update_min_rtt的修改:
if (!bbr->probe_rtt_done_stamp && tcp_packets_in_flight(tp) <= bbr_cwnd_min_target) { bbr->probe_rtt_done_stamp = tcp_time_stamp + msecs_to_jiffies(fast_probe?(bbr_probe_rtt_mode_ms>>1):bbr_probe_rtt_mode_ms); ... }
这样会再也不过度对CUBIC低头示弱。经过上一节描述的BROBE_BW状态的收敛过程,这种强势的行为并不影响多个同时运行BBR算法的TCP流之间公平性,它们之间的公平收敛,留到PROBE_BW状态慢慢玩吧。至于和CUBIC之间的竞争,你不仁,我便不义了!
最后,咱们看一下STARTUP和DRAIN的增益系数,它们互为倒数,怎么填充就怎么清空,完美回退。
/* We use a high_gain value of 2/ln(2) because it's the smallest pacing gain
* that will allow a smoothly increasing pacing rate that will double each RTT
* and send the same number of packets per RTT that an un-paced, slow-starting
* Reno or CUBIC flow would:
*/
static const int bbr_high_gain = BBR_UNIT * 2885 / 1000 + 1;
/* The pacing gain of 1/high_gain in BBR_DRAIN is calculated to typically drain
* the queue created in BBR_STARTUP in a single round:
*/
static const int bbr_drain_gain = BBR_UNIT * 1000 / 2885;
咱们来尝试调整这参数,能够是不对称的缓慢Drain,这是基于在降速排空队列的过程当中,可能已经有别的链接出让了带宽!
CUBIC还算是迄今比较伟大的算法,它不会轻易被BBR取代,可是它须要被改进。
首先,在没有AQM时,加性增乘性减自己并无错,通常的丢包都是尾部拥塞丢包,这对于TCP拥塞控制而言,基于丢包的拥塞探测太容易作了,可是尾部丢包会带来一系列的问题,为了解决这些问题,出现了AQM,好比RED之类的丢包算法,这样一来就没法区别RED丢包,尾部丢包,线路噪声丢包,乱序未丢包这几类现象了。问题的严重性是由拥塞算法对丢包的敏感性形成的,只要有丢包,或者说仅仅是按照本身的逻辑检测到了可能的丢包,就好像出了大事通常,窗口会大幅度降低!!然而,噪声丢包和乱序并非拥塞,因此若是能过滤掉这两类,CUBIC的效率必定会有大的提升!
事实上,CUBIC算法没有任何问题,问题出在Linux的TCP实现(其它系统估计也好不到哪去,你们都是仿照BSD实现的)的问题,形成不少时候,CUBIC爱莫能助,好不容易探测到一个合适的拥塞窗口,被dubious的ACK调用tcp_fastretrans_alert的PRR算法一会儿拉下去了,或者直接被定时器超时事件给拉下去...BBR之因此优秀,并非说其算法优秀,其独到之处在于一切都是它本身来决定的,没有谁能拉低BBR算出来的窗口和发送带宽,除了它本身!因此说,BBR的优秀更多的指的是其框架的优秀。
我不对CUBIC自己进行任何的改造,我只是解放它。首先要作的就是排除假拥塞丢包,要确保进入PRR逻辑的丢包都是因为致使第二类缓存被填满的拥塞避免引发的。至于说非拥塞丢包,继续进入CUBIC逻辑,让CUBIC获取更多的权力。
这里介绍的一种方法是NCL机制。详细文档请参见《TCP-NCL: A Unified Solution for TCP Packet Reordering and Random Loss》。其基本思想就是,将重传与拥塞控制Alert(即主动的拥塞控制逻辑调用,在Linux中表现为tcp_fastretrans_alert以及tcp_enter_loss)逻辑分离,在确认真的拥塞以前不进入拥塞控制Alert逻辑。NCL是怎么作到的呢?很简单:
其各个例程的示意图以下:
NCL的设计巧妙之处就是至关于为丢包的拥塞探测设置了一个时间窗口,在标准的Linux TCP拥塞控制实现中,只要程序逻辑断定发生了丢包,就会惊恐地呻吟着进入拥塞控制Alert阶段,退出Open状态,进入Recovery状态PRR降窗,或者进入LOSS状态窗口掉底。而NCL分离了丢包的性质判断和拥塞控制Alert的调用,在以往,重传定时器超时就意味着拥塞已经发生,然而NCL中却能够有一次过滤机会,即第一个RD定时器仅仅处理重传逻辑,重传后启动的CD定时器超时后才会判断为拥塞。其巧妙之处还在于RD,CD两个定时器的超时时间选择,其思想都在图示的注释里。