在TCP中,超时重传机制是和应答确认机制一样组成TCP可靠传输的关键设计。而超时重传机制中最最重要的就是超时计时器的时间选择的了,很明显,在工程上,在数据发送的过程中,如果用一个固定的值一直作为超时计时器的时长是非常不经济也非常不准确的方法,所以这一篇就来说说TCP中的超时计时器的设计哲学。

太短不行,太长也不行

超时超时,首先你得定义什么是正常的时间,才能知道有没有超过正常的时间。先假设一个非常理想的环境,这个环境理想到和以前很多物理题一样,不考虑摩擦力。我们假设网络很通畅且速率稳定,而且处理包的速度忽略不计,这样一个包发送到对端的时间永远都是一样的,将这个时间记为t。那么很明显,如果超过两倍的t还没有收到对端的回复,我们就可以肯定超时了。所以在这种情况下超时计时器只要设置的比两倍t长就行了。只要过了这个时间,发送端就会重新发送这个包。

那么这个时间是不是越长越好呢?答案很明显不是,因为太长会人为的减少通信的速率,对于通信这种有时候一点点速率的提高都让人欣喜若狂了,如果你还人为的浪费时间那真是暴殄天物了。

那么如果这个时间设置的太短会怎样呢?在这个理想情况下就是小于2倍的t,这会导致太多不必要的重传。也许ACK正在路上,你却错误的认为是丢失了,那么网络中就会增加很多本来就不必要的包。

而且,要知道,现实的网络环境是十分复杂多变的,有时候可能突然的抽风,有时候可能突然的又很顺畅,所以说如果只用一个一直不变的时间作为重传计时器的时长是完全不现实,不可用的。所以很多计算重传时间的算法就被设计出来。

调的一手好参数

TCP把一个包从发送端发送出去到接收到这个包的回复这段时间称之为RTT,学名round-trip time。如果在一个包发送出去以后,超过了RTT还没有接受到回复确认,那么很明显,这个包超时了。如果你还记得前面的关于的PING那一篇,里面就有一个time指示了来回时间,但是这个是ICMP的来回时间,和TCP的这个RTT是完全不一样的概念。通过wireshark你可以看到每一次的RTT的值。

 TCP的智慧—RTT的计算  - 图1

这个RTT的计算很简单,只要把收到确认包的时间减去发送包的时间就得到了这个答案。

现在开始对于重传计时的第一次思考,上面说了这样一个来回就说明包是成功的接收了并且没有发生任何异常,那么可不可以简单的用这个值作为标准来作为判断超时的依据呢?也就是如果超过了0.285s没有收到ack就开始重传,很明显,不能。原因是这个RTT是过去完成时了,是上一次成功的时候的时间,和下一次网络会不会突然抽风,还是会突然变得更通畅没有太大的必然关系,最愚蠢的一种思维就是简单的用过去代表未来。但是数学给我们提供了一种用已知大概去逼近未知的方法,那就是用概率统计的思维。所以最简单的一个办法是用过去的几次平均值来作为这一次重传计时器的时长,毕竟这是初中学过的理论。不过这个方法明显太过于幼稚,缺乏灵活的控制,所以说,第一次设计的尝试就出现了。

为了能够用更加灵活的方法来估算出重传时间,一个叫SRTT的概念被引进,SRTT学名是Smoothed RTT。估算重传时间(以后称之为RTO,Retransmission Timeout)的算法如下:

SRTT = (α SRTT) + ( (1 -α) RTT)

其中这个奇妙莫测的阿尔法取值在0.8到0.9之间,为什么这样取,我也不知道,我至今也没有找到原因。对于这第一个公式,具体实际中的做法是这样的,首先采样几次RTT的值,然后在第一次迭代的时候SRTT的初始值为RTT,后面就是根据每次计算出来的SRTT来计算就行了。这个公式有个你应该比较熟悉的中文名字,叫做加权移动平均。

在计算出SRTT之后,就使用这个值来计算我们需要的RTO,其方法如下:

RTO = min[UBOUND, max[LBOUND,(β * SRTT)]]

这其中UBOUND是一个上限时间,比如1分钟,LBOUND是一个下限时间,比如1秒钟,β,哈哈,又是一个神奇的参数,取值在1.3到2.0之间,叫做延迟方差因子,到底取啥,为什么取这个值,我,还是不知道。

这个方法有什么问题呢?问题就在这个RTT的计算上,前面说过RTT的计算是接收到ACK的时间和包发送出去的时间的差值,在正常情况下还好,如果是在采样的过程中发生了重传,那么到减去的时间是第一次发送的时间还是重传发送的时间呢?

如果是减去第一次发送的时间,那么很明显,这个RTT计算大了。那你可能会说了,从直观上说,用第二次发送的时间计算才是合理的。但是有一种情况,假设本来应该到达的ACK不是丢失了,只是延迟到达了,也就是说你刚重传,这个迷路的ACK就到了,那么你用这个时间减去第二次发送的时间,明显就小了。

这个时候两个叫Phil Karn和Craig Partridge的人就针对这个问题提出了一个算法,其解决方案十分简单,既然重传情况这么复杂,那么在采用RTT的时候直接忽略重传不就行了。你先收起你的吐槽说尼玛这样我要早出生几年也能想出这个办法啊,人家论文里还写了很多其他的东西,这个只是其中之一,而且这个算法也有很大的问题,Karn针对这个问题还提出了一个可行的解决方案,至少在工程上有了个可行的路子。

这个问题是什么呢?假设在某个时间,网络极度的抽风,突然由快变得很慢,导致所有的包都要重传。这下好了,因为前面一直很通畅,所以必然RTO很小,那么你又说重传的包不参与RTT的采样,这下完了,RTO永远不会更新,只会不断的重传,情况会越来越糟。而Karn针对这个提出了一个解决方案,只要重传,那么RTO就翻倍,这样就保证了在极端情况下不会导致越来越糟。

Karn的算法解决了初代算法的问题并且有了个可行的方案,但是RTO粗暴翻倍的做法感觉还是比较浪费。所以,在一年之后又有两个人Jacobson 和 Karels 针对这种加权移动平均的算法对RTT波动handle能力不强的弊端做了修正。其原理是用最新采样的RTT和平滑过的SRTT的差距来作为另一个影响因子。

SRTT = SRTT + α * (RTT - SRTT)

DevRTT = (1-β) DevRTT + β (|RTT - SRTT|)

RTO = μ SRTT + δ DevRTT

这三个公式就是现在TCP协议中真正运用的算法,关于这些参数,α是取0.125,β是0.25,μ 是1,δ是4,这就是linux中的取值,至于为什么,没有人知道,但是在实际效果中,果真就很有效,在编程过程中,我们称这种玄学叫做调的一手好参数。