RACK为TCP BBR提供动力源

先从需求谈起吧。
        在上一篇文章《 Google's BBR TCP拥塞控制算法的四个变速引擎》的最后,我提到bbr算法作为一个标称功率十足的引擎需要源源不断的能源供给,而这类能源就是数据包。又提到,TCP的快速重传机制几乎只会将判断为LOST的数据包重传一次,因此当重传数据包再次丢失,滑动窗口无法滑动的时候,将会无法提供数据包发送,bbr引擎就会失速,此时只能等待TCP的超时!当然,超时的代价有点大,不单单是对于拥塞控制而言,对于整个连接而言,都无异于异常劫难!

        因此,bbr需要源源不断的数据包供给它开大马力运行,bbr并不在乎这些数据包是新数据包,标记为LOST的数据包,重传过的数据包,甚至是构造的错包...只要有数据包即可!bbr真正彻底实现了拥塞控制与数据包标记/发送之间解耦合。


        说完了需求,再谈方案。
        Linux内核在bbr之前就已经引入了RACK机制,旨在快速发现并重传那些曾经重传后再次丢失的数据包,这些数据包的处理至关重要,如果不及时处理,便会陷入RTO的深渊。当然,RTO的回调在你自己手上,你也可以处理得不那么激进,然而对于已经失速的连接,继续勉强撑着也算是自欺欺人了...RACK解决了这个问题。然而bbr之前的RACK并没有获得最大收益,原因在于虽然RACK可以即时地探知哪些数据包丢失,特别是那些重传后再次丢失的数据包,但是由于此时拥塞窗口的计算已经固定地朝着ssthresh的方向PRR跌落下去,受限于拥塞窗口,即时准备再多的可发送数据也无法着实发送出去!bbr引擎可以即时消化掉RACK送上来的能源,二者配合就开启了一台动力十足的高端引擎。
        RACK(请先看 这个draft)从名字上看,Recent ACK,即最近的ACK,当然也包括SACK,因此正确的名字应该是Recent (s)ACK,RACK并不记录数据被(s)ACK的时间,而是在收到ACK的时候,记录被该ACK确认的数据包的发送时间,在这些发送时间中取Recent,即最晚发送的。RACK的思想是,记录这个Recent (s)ACK所确认数据包的发送时间T.rack,然后给定一个时间窗口twin,在时间T.rack-twin之前发送的未被确认的数据包均被标记为LOST,然后这些数据包会被交给发送逻辑去发送。这非常符合常理。
        RACK的代码超级简单,核心逻辑就一个文件两个函数,位于net/ipv4/tcp_recovery.c中的:
/*
 * 在一次(s)ACK的处理过程中更新被确认的数据包的最晚发送时间rack.mstamp。
 * xmit_time-当前处理的被确认的数据包的发送时间
 * sacked-当前处理的被确认的数据包的sacked标记,被选择确认过吗?被重传过吗?等等。
 */
void tcp_rack_advance(struct tcp_sock *tp, const struct skb_mstamp *xmit_time, u8 sacked);
/*
 * 根据tcp_rack_advance记录的最晚发送的被确认的数据包的发送时间rack.mstamp以及
 * 重传队列里未被选择确认的数据包的发送时间skb.mstamp的差,判断是否标记为LOST。
 * RACK内置有一个twin,凡是符合rack.mstamp-skb.mstamp>twin的数据包,均标记为LOST。
 * 如果该数据包被重传过,那么清除其被重传过的印记!
 */
int tcp_rack_mark_lost(struct sock *sk);

以上就是关于RACK的两个接口,TCP在处理ACK的时候会调用这两个接口:
1).处理ACK携带的信息(TCP头的ACK号或者选项中sACK块)时调用tcp_rack_advance;
2).在发送ACK并非顺序ACK时,进入异常Alert时,调用tcp_rack_mark_lost。

这个RACK机制的简单性就在于,它不再区分正常的顺序ACK以及SACK,它只比较时间戳,不管发送顺序如何,只基于确认携带的信息来决定一个数据包是不是要被重传。以下面的序列为例:
1|2|3|4|5|6|7|8|
假设一个ACK确认了4,那么UNA则是5,假设这个ACK没有携带SACK信息,只是确认了4,那么rack.mstamp就是4发送的时间了,现在的问题是,4之后的5,6,7,8怎么可能在4之前发送呢?它明明是位于4之后的啊!RACK的简单性就体现在这里!5,6,7,8虽然在4后面但是谁也不能保证它们就一定是按照序列号顺序发送的,更加合理的做法是记录发送时间序列!典型的场景是,4,5,6,7,8均是重传过的数据包,首先重传了7,8,然后重传了6,然后重传4,最后重传5,这样发送的时间序列就是:
1|2|3|7|8|6|4|5|
现在4被确认了,按照上述时间序,我有理由继续等待5的确认到来,因为5是在4之后发送的,然而7,8,6却都是在4之前发送的,到底等不等呢?这里7是最先发送的,要判断skb7.mstamp与rack.mstamp之间的差值了,如果大于twin,说明继续等待7的选择确认是不可忍受的,反之,如果在twin之内,那么便有理由继续等待了,可能是乱序了!同样的策略处理8和6。
        这样处理是不是更加简单些呢?只需要按照时间序比较数据包最近一次的发送时间与rack.mstamp之间的差即可!完全忽略这个数据包是不是曾经被重传过,这样就解决了按照序列号进行LOST判断的复杂性问题。
        然而,在乱序的情况下,你可能会认为RACK机制可能会误传很多并未丢失(实际上是乱序到达或者ACK乱序反馈)的数据包,事实上这里就体现了twin时间窗口的作用,RACK的时间序并非严格的时间序,它是一个带有缓冲的准时间序机制。就算你认为twin也没用,再不济你也可以将RACK关掉的!
        注意twin的选择,一般而言是最小RTT的1/4,这里的最小RTT是与SRTT无关的,它是真实测量出来的离散RTT的win_minmax(请在《 Google's BBR TCP拥塞控制算法的四个变速引擎》看win_minmax详情)最小值,基于一个自动向后滑动的时间窗口采样的最小RTT,这里的主要目的不是平滑掉噪点(这有点像鸵鸟策略...),而是过滤掉非拥塞导致的抖动,这是一种主动发现噪点的行为,可以将 BufferBloat的影响最小化。


有了RACK机制,bbr再也不用为无包可发这种事发愁了。
        只要bbr可以根据(s)ACK采集到带宽和RTT,那么bbr就可以根据这些带宽,RTT的反馈全速率运行,而能让带宽和RTT反馈回来的,正是发出去的包,再次重申,无论是新包还是重传包,只要发送去,它们都可以反馈回结果,不管是ACK,SACK,还是DSACK...我们又绕回来了,RACK机制即使在已经发生丢包/乱序等事件时也能提供源源不断的可发送的数据包(即标记为LOST的数据包)。这个过程平滑的运行,不必再等待RTO超时!
        在bbr之前,一旦发生丢包或者严重乱序,TCP就会接管拥塞控制算法,但现在不了!以前的做法是错误的,所谓的丢包,乱序,这些都是TCP的拥塞控制状态机逻辑自己猜的,相当大程度是不真实的,任何算法都无法准确猜出是不是真的发生了丢包,想欺骗一只蝙蝠撞墙是很容易的,同样,TCP也是一个瞎子!所以,bbr的做法是正确的:
bbr算法本身:计算发送速率和窗口。
TCP拥塞控制状态机:准备新数据,标记LOST(传统方式以及RACK方式),即提供可传输的数据包,灌入bbr算法提供的食道。
TCP传输逻辑:实际传输任何可以传输的数据包,新的数据包,所有标记为LOST的数据包。
以上3者相互配合,回答了 ”传输多少?“,”传输什么?“,”怎么传输?“等问题,并且三者之间完全基于当下的(s)ACK反馈来交互,彼此之间则完全独立。
...
接下来干什么?
接下来我来吐个槽,事情要从工业革命后,人们企图将蒸汽机装在马车上开始...
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值