0%

TCP - 超时与重传

TCP基础 - 连接与状态一文中,我们提到TCP会利用Sequence Number与Acknowledgment Number,对发出的数据进行标记,并且对接收到的数据进行应答,从而确定数据从发送端成功的送达到了接收端。

现在假设有这么一种场景,发送端一共发送了1,2,3,4,5一共五个数据包,并且陆续接收到了1,2,4,5这四个数据包的ACK,但是迟迟没有收到数据3的ACK。由于TCP是可靠传输协议,必须要确保所有的数据包都能成功送达到对端,这种情况下,发送端没法确认接收端到底有没有收到数据包3(有可能在发送过程中失败了;有可能收到了,但是ACK传输失败了),为了保证数据传输的可靠性,就必须对数据包3进行重传。

上述过程中涉及到两个关键的知识点:

  • 多长时间内没有收到ACK才会触发重传;
  • 触发重传之后,重传哪些数据包。

超时时间(RTO,Retransmission TimeOut)

RTT(Round Trip Time)

RTT即指一个数据包从发送出去,到接收到ACK回复的时长,计算RTT的方式比较简单,TCP协议中有一个TSOPT(timestamp option)选项,它包含两个时间戳,发送端会将发送时的32bit时间戳放到第一部分(TSval)中,而ACK报文会将其原封不动的填入ACK报文中,TSOPT选项的第二部分(TSecr)中(ACK包会将当前时间戳放入TSval中),发送端通过将当前时间戳减去ACK报文中TSOPT选项中的TSecr就可得到精确的RTT值。

固定的RTO

最简单的办法,是设置一个固定值$t$,来作为超时时间,数据包从发送之后开始计时,如果过了$t$之后,还没有收到ACK,则重传数据包。

这样做无疑是最简单,但是也最笨的方法,如果$t < RTT$肯定不行,正常情况下数据包都还没有收到ACK回复就被判定为超时了;如果$t > RTT$,那么有在正常可能数据包早就已经超时了,但是仍然要等到$t$之后,才会重传,会造成网络利用率不高。同时,在真实的网络环境下,$RTT$会受到网络拥塞,传输距离等等很多因素的影响,也是一个不确定的值,因此也没有办法固定一个合适的$t$值。

Jacobson / Karels算法

既然固定值可行性不高,那就优化一下。由于RTT能够反映当前网络的情况,因此还是采用RTT值做参考,不过只看一次的RTT波动性很大,不一定能代表整体情况,因此采用多次采样,然后进行平滑处理。

算法步骤如下:

  • $RTT_s = RTT_s + α * (RTT_n - RTT_s)$

    其中:

    • $RTT_s$:Smoothed RTT,表示平滑之后的RTT值;
    • $RTT_n$:表示当前最新一次的RTT值;
    • $α$:0.125。

    假设最近一个$RTT_s$为100ms,$RTT_n$为200ms,则$RTT_s =100 + 0.125 * (200 - 100)$,即是112.5ms。

  • $RTT_d = (1 - β) RTT_d + β (|RTT_n - RTT_s|)$

    其中:

    • $RTT_d$:Deviation RTT;
    • $β$:0.25。
  • $RTO = µ RTT_s + ∂ RTT_d$

    其中:

    • $µ$:1;
    • $∂$:4。

RTO的设置,一直都是研究的热点,从最早的平滑经典算法,到Karn / Partridge算法,再到Jacobson / Karels算法,都在不断的优化演变,中间的演变过程也很有意思,都在解决上一个算法中的短板,可以阅读TCP 的那些事儿(下)一文中的TCP的RTT算法章节来了解。

Exponential backoff

通过上面的算法,我们得到一个RTO,作为数据包需要重传的超时判定时间。现在假设网络出现拥堵,所有数据包的ACK时间都比预期的要长很多,如果依然使用RTO作为超时判定时间的话,这样可能导致所有数据包都会被认定为超时,于是便有了Exponential backoff(退避指数),此时会将RTO的时间翻倍,即是$RTO’ = 2 * RTO$,如果连续发生n次重传,那么$RTO_n = 2^{(n - 1)} * RTO_1$。

各个操作系统都会限制重传次数,当超过一定值之后,仍然重传失败,则会释放TCP连接。

重传机制

快速重传(Fast Retransmit)

通过超时算法,得到RTO之后,就可以判断什么时候需要重传数据包了。不过我们考虑下面的一个场景:

1

发送端连续发送1,2,3,4,5一共五个数据包,但是由于某些原因,接收端并没有收到数据包2,于是数据包1和2送达之后,接收端依次回复了ACK 1,ACK 2,数据包4,5一次到达之后,接收端依然回复两次ACK 2,一直要等到RTO时间过去之后,发送端才能确认数据包3丢失,然后进行重传。

Acknowledgment Number是以字节数为单位,来告诉发送端已经收到的字节数,所以ACK的时候不能跳着确认,只能回复已经收到的连续的包的字节数,不然发送端会以为之前数据包都收到了。

为了节省不必要的等待时间,TCP引入了快速重传的机制,简单来说,如果发送端连续收到n次相同的ACK回复,就触发重传,而不用一直等到超时。在上面的例子中,发送端连续收到了三次ACK 2,则直接触发数据包3的重传:

2

Selective Acknowledgment(SACK)

使用快速重传有个小问题,例如上述的例子中,当发送端连续收到3个ACK 2的时候,可以确定需要重传数据包3,但是数据包3之后的数据包的送达情况是不可知的,发送端无法确定数据包4,5,6是否收到了。更复杂一点,假设数据包3之后,又发送了4到10这7个数据包,但是只收到了4次ACK 2,那么这7个数据包到底是哪4个包收到了呢?

简单来看的话,有两种办法:

  • 先重传数据包3,然后等ACK,就能知道下一个连续收到的数据包;
  • 将数据包3之后的数据包全部重传一遍。

两种简单的方式都有弊端,第一种比较占用时间,需要等待收到重传的数据包的ACK之后,才能知晓接收端的情况;第二种比较占用带宽,会重复发送已经送达了的数据包。因此TCP中定义了一种稍微复杂一点的方式,称之为SACK,来解决上面的问题。

简单描述SACK,就是在ACK数据包中,告知发送端目前缺失的数据包碎片,如下图:

3

有了SACK之后,发送端就能判断出哪些数据包发送成功了,哪些数据包发送失败了,从而既不用等待,又不用重传所有的数据包。

参考



-=全文完=-