从上节课看到,出现了丢包的时候,tcp会出现重传,那重传的机制是什么,TCP拥有两套独立机制来完成重传。一是基于时间,二是基于确认消息(SACK)。第二种方法通常比第一种更高效。
4.1 简述
TCP在发送数据时会设置一个计时器,若到计时器超时仍未收到数据确认信息,则会引发相应的超时或基于计时器的重传操作,计时器超时称为重传超时(RTO)。另一种方式的重传称为快速重传,通常发生在没有延时的情况下。若TCP累积确认无法返回新的ACK,或者当ACK包含的选择确认信息(SACK)表明出现失序报文时,快速重传会推断出现丢包。
4.1.1 简单的超时与重传
《TCP详解》中举了个例子,非常不错,用telnet连接,连接完成之后再断开一遍,然后进行抓包,抓出来的包就可以体验出超时重传的机制,每一次重传都比前面一次晚2倍。重传了几次之后,就会提示连接失败。
TCP拥有两个阈值来决定如何重传同一个报文段。R1表示TCP在向IP层传递“”消极建议”(如重新评估当前IP路径)前,愿意尝试重传的次数(或等待时间)。R2(大于R1)指示TCP应放弃当前连接的时机。针对SYN报文段的R2应最少设为3分钟。
linux系统中,对一般数据段来说,R1和R2的值可以通过应用程序或使用系统配置变量net.ipv4.tcp_retries1和net.ipv4.tcp_retries2设置。tcp_retries1默认值为3。tcp_retries2默认值为15,对应约13~30分钟,根据具体连接的RTO而定。
由于TCP需要适应不同环境进行操作,可能随着时间不断变化,因此需基于当前状态设定超时值。例如,若某个网络连接失败,需要重新建立,RTT也会随之改变(可能变化很大)。也就是说,TCP需动态设置RTO(重传超时)。
4.1.2 简单计算RTO的方法
最初的TCP规范【RFC0793】采用如下公式计算得到平滑的RTT估计值(称为SRTT):
SRTT = α(SRTT) + (1-α)RTTs
这里的,SRTT是基于现存值和新的样本值的TRRs得到的结果。常量α为平滑因子,推荐值为0.8~0.9.
这种方法称为指数加权移动平均或低筒过滤器。
后来考虑到SRTT估计其得到的值会随RTT而变化,【RFC0793】推荐值如下公式设置RTO:
RTO = nin(ubound, max(lbound, (SRTT)β))
这里的β为时延离散因子,推荐值为1.3~2.0,ubound为RTO的上边界(可设为建议值1分钟),lbound为RTO的下边界(可设定建议值,如1秒)。RTO的取值范围,大于1秒,小于2倍的RTT。
这样子计算RTO就可以了么?
当RTT有大规模的变动的情况下,上述公式计算得出的结果是比较小的,因为上一次的结果占比较大,所以导致会大量重传,这样会导致网络过载。
为了解决上面所说的问题,可对原方法做出改进以适应RTT变动较大的情况。可通过记录RTT测量值的变化情况以及均值来得到较为准确的估计值。我们还可以同时测量均值和方差(或标准差)能更好地估计将来值。对RTT的可能值范围做出好的预测可以帮组TCP设定一个能适应大多数情况的RTO值。
平均偏差是对标准差的一种好的逼近,但计算起来却更容易,更快捷。所以接下来这个版本,我们要结合平均值和平均偏差来进行估算。可对每个RTT测量值M(前面称为RTT)采用如下算式:
srtt = (1-g)(srtt) + (g)M //算这个srtt跟前面是一样的
rttvar = (1-h)(rttvar) + (h)(|M| - srtt ) //计算平均偏差
RTO = srtt + 4(rttval)
如前面一样,srtt是平均值,rttval为绝对误差|err|的EWMA。err为测量值M与当前RTT估计值srtt之间的偏差。
增量g为新RTT样本M占srtt估计值的权重,取为1/8.增量h为新平均值偏差样本占偏差估计值rttval的权重,取值为1/4。当平均偏差越大的时候,(也就是大规模波动的时候),RTO会越大。
4.1.3 RTT的计算
上面的公式已经把RTO的计算说了挺清楚了,但是每个RTO的计算都需要测量RTT的值。
那怎么测量RTT的值呢?
在测量RTT的过程中,TCP的时钟都是在运转的。对于刚开始建立连接的的时候,实际TCP的时钟并非从0开始计时,也没有绝对精确的精度。只要TCP的时候,会随着系统的时钟增加就可以了,TCP时钟一个“滴答”的时间长度称为粒度。通常,该值相对较大(约500ms),但linux系统使用了更细的粒度1ms。
粒度会影响RTT的测量以及RTO的值,所以上面的公式又来了再次的更新:
RTO = max(srtt + max(G, 4(rttval)), 1000)
这里的G为计时器的粒度,1000ms为整个RTO的下界值。因此,RTO至少为1s,同时提供了可选上界值,假设为60s。
4.1.4 初始值
我们前面说的都是根据时间的变化而得出的值,那初始化的时候的值呢?
在首个SYN交换之前,TCP无法设置RTO的值。除非系统提供,否则也无法设置估计器的初始值。
根据【RFC6298】,RTO的初始值为1s,而初始SYN报文段采用的超时间隔为3s,当接收到首个RTT测量结果M,估计器按如下方法进行初始化:
srtt = M
rttval = M/2
4.1.5 linux采用的方法
之前看到一篇介绍linux的重传机制,当初没保存,今天没找到了,真是无线插柳柳成荫,下次碰到才仔细研读一波,今天就以自己的话写,写多了多多包涵,也请指出。
画了这个图还是挺辛苦的,大家也可以按照画画,加深印象。
初始阶段:
在上面介绍,SYN的超时时间设为3s,这个就不在这图里表示出来了,因为这是通信前的,现在是由客户端发起连接syn报文,顺带上TSV=0,这里取了相对值,具体的不会是0.这时候服务器接收到了syn,回复了一个ack,会把接收到的TSV的值写到TSER中,这时候客户端收到这个ack之后,这就是第一次接收到M,看上面可以得到:
srtt=m=16, rttval=m/2=16/2=8
等于linux中引入了一个变量mdev,mdev采用标准方法的瞬间平均偏差估计值,也就是mdev=m/2=16/2=8,也引进mdev_max记录在测量RTT样本过程中的最大mdev,这时候的rttval=mdev_max(mdev, TCP_RTO_MAX=50ms),所以这里的rttval=50,最后rto=srtt+4*rttval=216;
正常计算:
客户端接收到服务器返回一个ack之后,就再次给服务器发送ack,这是三次握手过程。三次握手结束后,正式进入了传输阶段,这时候客户端发送了两个1400长度的报文,因为两个间隔比较小,TSV没有增加。(这个也是一个问题)。服务器接收到两个报文之后,回复ack=2801,这个值怎么来的到滑动窗口的时候讲,这时候客户端接收到了这个ack,第二次计算RTO又开始了:
m=223-127=96
mdev=mdev*3/4+|m-srtt|*1/4=8*3/4+|96-16|*1/4=26ms
mdev_max=rttval=max(mdev_max, mdev)=max(50, 26)=50
srtt=srtt*7/8+m*1/8=16*7/8+96*1/8=26
RTO=26+4*50=226
完美,这是第二次计算的结果。
接收不过来的情况:
如果ACK不能及时返回,返回了后面的的报文的ACK,计算RTO的时候,也是要看ACK中带的TSER的时间戳计算,不要看客户端最新发送的时间戳。
linux使用这种机制有如下优点:
- 时间粒度使用了1ms
- 引入了瞬间平均偏差mdev,跟一段区间内的mdev的最大值mdev_max,保证rttval不会变的太小
- 减小rttval的比例
if(m < (srtt - mdev))
mdev = (31/32)mdev+(1/32)|srtt-m|
else
mdev = (3/4)mdev+(1/4)|srtt-m|
4.1.6 基于计时器重传
由上面的讲解,我们已经明白了,RTO的值的设置了。在设置计时器之前,需记录被计时的报文段的序列号,(要不然收到了ACK都不知道是谁的),若计时收到了该报文段的ACK,那么计时器被取消。之后发送端发送一个新的数据包时,都需设定一个新的计时器,并记录新的序列号。所以TCP连接的发送端不断的设定取消一个重传计时器,如果数据丢失了,超过了计时器的时候,就会启动重传机制。
4.2 快速重传
竟然是快速重传,那我们就快速说完。哈哈。
如果我们接受端已经知道我们丢失了哪个包,但是还要等待超时计时器再进行重传,这样效率低了很多,所以TCP就提出了一个基于接收端的反馈信息来引发的重传(不知道是不是这样的,我自己理解的),叫快速重传。
当网络中出现失序分组时——若接收端收到当前期盼序列号的后续分组时,当前期盼的包可能丢失,也可能仅为延时到达。通常我们无法得知是哪种情况,因为TCP等待一定数目的重复ACK(称为重复ACK阈值或dupthresh),来决定数据是否丢失并触发快速重传。通常dupthresh为常量(值为3),但是linux系统可基于当前的失序程度动态调节该值。
如果不采用SACK的话,在接收到有效的ACK前至多只能重传一个报文段。采用SACK,ACK可包含额外信息,使得发送端再每个RTT时间内可以填补多个空缺。
4.2.1 带选择确认的重传
我抓包的时候,没找到SACK的包,有点可惜,但是在SYN的包中,是打开了SACK的使能,以后遇到了再截图出来吧。
**每一个SACK块内包含的是最近接收到的报文段的序列号范围。**由于SACK空间有限,应尽可能确保向TCP发送端提供最新消息。其余的SACK块包含的内容也是按照接收的先后依次排列。
举个栗子:
没有图的例子是没有灵魂的,不过没办法,找不到包,那就直接写字了。
发送端发送了1-26601个数据,接收到接收到了1-23801和25201-26601。从上面可以看出接收端丢失了23801-25201的数据包,所以我们回复的ACK是这样的,ack=23801 SACK = [25201-26601],假设发送端接收到了3次重复ack,就会触发快速重传,然后把23801-25201的数据包发送过来。(希望我这个例子说对了)