超时与重传
为什么要设定超时?
我们发送数据包的目的是为了让接收方收到并处理,而发送方发送的包在传输的过程中可能会丢失,丢失后接收方就接收不到了,此时我们需要重传这个包。
而我们怎么判断这个包丢失了呢?
首先这么想,发送方怎么确认这个包被成功接收到了呢?一定是发送发接收到了对这个包的ACK,那么如果没有接收到这个ACK,我们就认为这个包丢失了或者ACK丢失了,而为了保险,我们统一认为数据包丢失了。
那怎么才算我们没有收到ACK呢?
1年没有收到,1天没有收到,还是1分钟没有收到? 从我们发送这个包之后,在时间t内若没有收到ACK,我们就认为这个包丢失了,这个t就是重传超时RTO(计时器超时),这是我们设置超时的原因。
RTO怎么确定?
我们先来定义一个概念。如果包没有丢失,它从被发送到被接收方接收,再到发送方接收到对它的ACK,经过的时间我们称为往返时延RTT。
RTO一定不能比RTT小,否则一个正常传送的包会被认为丢失了。
那RTO要比RTT大多少呢?
我们再定义2个名词
往返时延的加权平均srtt ,我们每次得到RTT样本的时候,不直接使用它,因为可能有误差,所以我们用它的加权平均,即srtt = asrtt + (1-a)RTT,a一般取0.8~0.9。
往返时延加权平均srtt的平均偏差rttvar,它可以近似看作srtt的标准差,为什么要用这个近似值而不直接使用标准差呢?一个原因是标注差计算过程中需要开方,时间长,所以用平均偏差。
现在我们来考虑,对于一个正态分布,(-∞, μ+3σ]可以覆盖大约99.85%的概率,而正态分布是一个对称的曲线,RTT的概率密度曲线不是一个对称的曲线,它的平均值相较于正态分布的平均值要向左偏一点,所以我们采用u + 4σ作为RTO的值,这样就能基本覆盖所有的RTT,(为什么不加5倍的呢?再大的话就是明知丢包了却还在等待了,其实加4倍我认为并不能完全覆盖,但是对于那些超过了4倍的RTT样本,大概率不是一个好的RTT)
好了,现在我们知道RTO可以根据srtt和rttvar来确定,采用的计算公式如下:、
标准方法:
M为每次测量到的RTT样本值
srtt <-- (1-g)*srtt + g*M
rttvar <-- (1-h)*rttvar + h*(|M-srtt|)
RTO = srtt + 4*rttvar
g一般取1/8,h一般取1/4
看到这儿,你可能会发现一个问题,
在没有初始srtt和rttvar的情况下,RTO怎么确定?
根据[RFC2018],RTO的初始值设为1s,第一个SYN报文段采用的超时间隔为3s。当得到首个RTT样本值M后,来根据下面的式子初始化srtt和rttvar,然后算出RTO。
srtt <-- M
rttvar <-- M/2
一般情况的RTO确定值已经OK了,但是会不会有例外呢?有的
如果在测量RTT的过程中出现了重传,这时候接收到1个ACK,那这个ACK是对第一个包的确认还是对重传的包的确认?
有两种方法来处理这个问题。一个是Karn算法,另一个是时间戳选项。
1、Karn算法
它指出,当出现超时重传时,接收到的对重传数据的确认不能用来估计srtt。这是这个算法的第一部分。
它的第二部分是,如果这个包是就是对第一个数据报而不是对重传数据报的确认呢?
它是不是说明,在这样的情况下,网络的负载能力可能出现了问题,导致第一个包的往返时延RTT增加了呢,而网络此时如果出现问题的话,那么我们再重传数据包就更增加了网络的负担。
所以,我们不能简单地忽略这次重传,我们需要做一些事,做什么事呢?
我们刚才提到,是这个包的RTT增加了导致了这次重传,而不是包丢失了,所以,那我们增大RTO不就好了吗?RTO增大后,不就不会重传了吗?
Karn算法指出,我们需要采用一个回避系数,初始值为1。虽然我们不用这个RTT来估计RTO了,但是,每次重传我们需要将回避系数加倍,让RTO乘以这个回避系数,此过程一直持续到接收到非重传数据,然后重新将回避系数设为1。
2、时间戳选项
时间戳值TSV是这个数据要发送时发送方当前的时钟值,它携带于初始SYN报文段的时间戳选项TSOPT中,并在SYN + ACK的TSER部分返回,然后此时的时钟值减去TSER就能得到一个RTT样本,以此设定srtt、rttvar和RTO初始值。
乍一看这个方法现在没有什么问题,但是,因为TCP并非对其接收到的每个报文段都返回ACK,在处理大批量数据时,通常是两个数据报返回一个ACK。还有一些情况,比如,当数据出现丢失、失序或重传成功时,TCP的累积确认机制表明报文段与其ACK并非一一对应关系。
为解决上面的这些问题,基本采用如下方法来测量RTT样本值:
TCP端让它发送的数据报都携带一个TSV(发送这个数据报时的时钟值)。
接收端接收到一个数据报后,把TSV值记录在TsRecent中,并记录其上一个发送的ACK号LastACK。
执行步骤如下:
1、当一个新的报文段到达时,如果它的序列号和LastACK吻合,那么就将这个TSV写入到TsRecent中。
2、接收端发送ACK的时候,将TsRecent的值写入到数据报的TSOPT的TSER部分。
3、发送端接收到ACK的时候,将当前时钟值减去TSER,得到的差即为一个RTT样本。
Linux采用了怎样的计算超时的方法呢?
与标准方法稍微有点差别,因为标准方法发布的时候,时钟粒度普遍为500ms,而linux的时候,时钟粒度为1ms。linux采用更频繁的RTT测量与更细的时钟粒度,这会提高RTT的准确度。但是,
1、多次测量之后,由于积累了大量的RTT样本值,这会导致rttvar随时间减小趋近于0,基于大数定律。这是不好的,这样RTO就接近等于srtt了。
2、当某个RTT样本值显著低于srtt时,标准方法会增大rttvar,这会导致RTO不降反升,为什么?因为RTO = srtt +4*rttvar,rttvar是4倍,占的权重更大。这是不应该的,此时我们应该让RTO减小才对。
为了解决这两个问题。linux新增了两个新的变量,mdev和mdev_max,mdev记录瞬时平均偏差估计值,mdev_max记录测量过程中最大的mdev,它的下限是50ms。
rttvar需要定期更新保证其不小于mdeg_max。第1个问题解决了。
用标准方法得到初始srtt rttvar RTO后,采用下面的方法估计RTO:
m为得到的RTT。
// 计算srtt,与标准方法一样
srtt = (7/8)*srtt + (1/8)*m
// 若m小于RTT的下界,就减小它的权重。
if(m < (srtt - mdev)
mdev = (31/32)*mdev + (1/32)*|srtt-m|
else
mdev = (3/4)*mdev +(1/4)*|srtt-m|
mdev_max = max(mdev_max, mdev)
// 计算rttvar
rttvar = mdev_max
// 得到RTO,这里与标准方法一样。
RTO = srtt + 4*rttvar
至此,关于重传超时RTO的设置已经完成。那么对于重传呢?
重传有3种方法
1、基于计时器的重传
如其名,当计时器超时后启动重传,它会降低当前数据发送速率,通过两种方法。1、基于拥塞控制减小发送窗口;2、Karn算法的第二部分,让RTO每次都加倍。
2、快速重传
当收到3个重复ACK后,马上启动重传,不必等到重传计时器超时。
这里有一个概念:恢复点:发送端在执行重传前发送的最大序列号。当收到的ACK号大于或等于恢复点时,才会退出重传阶段。
3、带选择确认的重传
带选择确认的ACK(SACK)。n个块的长度是8*n + 2字节。因为一般要携带时间戳,所以40个字节能携带3个块。
发生丢包后就会生成SACK,第一个SACK块包含的是最近接收到的报文段的序列号。其余的SACK块包含的内容按照接收的先后顺序排序。
当发送端接收到SACK后,一种方法在其重传缓存中标记该报文段的选择重传成功。但是由于接收端可能在下一个SACK中做出变更(即标记接收到的数据报会变成没有接收到,我也不知道为啥),所以不能在重传缓存中马上把这个数据报清除。只有当接收到的普通ACK值大于重传前的序列号值之后才能清除。
另外,正在有SACK的情况下,收到两个块重复标记缺失部分就重传数据报,不必等待超时。
还有一个是伪超时
即某次RTT突然增大导致的。
通过Effiel检测算法或F-RTO检测算法来检测,Effiel检测算法就是重传后收到ACK后看收到的ACK的时间戳是第一个包的时间还是重传包的时间,如果是第一个包的,就代表是伪重传。
然后通过Effile响应算法来恢复,即超时后会通过如下算式保存现在的srtt和rttvar:
srtt_prev = srtt +2*G // G是时钟粒度,加两倍的原因是如果srtt超大,可能就不会超时。
rttvar_prev = rttvar
若为伪重传,就让srtt和rttvar分别等于上面的两个值,然后重新计算RTO。
一些知识:
目的度量:当一条TCP连接断开后,可以保存srtt和rttvar值一段时间,供以后使用。
重新组包:当需要重传时,TCP允许重传一个更大的包。
与TCP重传相关的攻击:
1、低速率DoS攻击。攻击者向网关或主机发送大量数据,使得受害系统持续处于重传超时的状态。
2、减慢受害TCP的发送,使得RTT估计值增大。这样受害者在丢包后不会立即重传。
3、伪造ACK,这样攻击者会使得受害TCP认为连接的RTT远小于实际值,导致过分发送,造成大量的无效传输。