TCP超时重传
当TCP发出报文之后一段时间后,没能收到对方的确认包,那么可以认为时数据包已经超时,这时就需要重传。 在数据的发送方与接收方之间,需要协调发送与接收速率:发送太快接收太慢,那么接收方数据来不及处理缓冲区不足,数据包可能会被丢弃;发送太慢,就白白浪费带宽,占用发送方的时间,这就是流量控制(Stream Control)。 但网络中并不止我们自己建立的一个TCP连接,还有大量其它的连接以及中间设备,如果所有TCP连接都自行其是,那么网络将会发生阻塞。如何避免阻塞,发现阻塞时如何退避缓解阻塞,如何避免阻塞的同时充分利用网络,这就是所谓拥塞控制(Congestion Control)。
超时重传
一个报文段从发送方发出, 再到从接收方得到ACK
,这一轮经过的时间称为一个RTT
,即Round Trip Time
,依据通信通常的RTT
,我们可以推断通信RTT
的上限,当超出这个上限就认为数据包已经超时,需要重传, 这个时间就是RTO
,即Retransimission Timeout
.
当数据包需要重传时,很可能是由于对面已经意外关闭,网络连接断开,或者中间网络恶化等等。重试1次不一定成功,因此需要每隔一段时间重试一次。TCP一般实现,每次重传间隔时间加倍,这被称为二进制指数退避(binary exponential backoff),多次重试后就应当考虑放弃连接。
在建立连接时,我们可能遇到发起SYN
失败,或对SYN
返回ACK
失败:
net.ipv4.tcp_syn_retries
用于控制SYN
发送失败多少次应该放弃连接
net.ipv4.tcp_asynack_retries
用于控制对SYN
的ACK
发送失败多少次应该放弃连接
TCP定义了R1
和R2
两个阈值来决定如何重传同一个报文段,当达到R1
时,IP层将需要考虑重新评估当前的IP传输路径;当达到R2
时,则放弃当前连接。在Linux中,他们分别是:
net.ipv4.tcp_retries1
以及 net.ipv4.tcp_retries2
.
RTO估值的传统方法
这个估值方法没有被现在的Linux协议栈采用,但是其思路简单,有一定的参考意义,因此做一下记录。
对于RTT的估计值,根据历史取样加权计算出的平滑的RTT称为SRTT
:
$$SRTT\longleftarrow\alpha(SRTT)+(1-\alpha)RTT_{s}$$
这里SRTT
基于现存值和新的样本值$SRTT_{s}$得到更新结果。常量$\alpha$为平滑因子,推荐取值 0.8~0.9,这样估算SRTT
时,一部分来自现存值,一部分来自新测量值,这种估算方式被称为加权移动平均或者低通过滤器.
计算RTO
时, 如果直接使用RTT
,会导致RTO
随着RTT
不断变化,因此可以采用建议公式计算:
$$RTO=min(ubound, max(lbound, (SRTT)\beta))$$
其中$\beta$为离散因子, 推荐值为 1.3~2.0, ubound为上界,如1分钟,lbound为RTO
下界,如1秒,这称为经典方法,在RTT
稳定的网络中可以获得比较好的性能,但在RTT
变化巨大的网络中,无法获得预期的效果。
现在Linux采用的是一种通过采样获得srtt
,以及绝对值偏差rttvar
,偏差越大,srtt
波动越剧烈,其计算比较复杂,在此不表,我也不打算掌握。在Linux 5.9内核,相关代码放在net/sctp/transport.c:sctp_transport_update_rto
函数中。
重传二义性
在理想情况下,只要我们记录数据包发出的时间T1
,然后记录返回ACK
的时间T2
,计算T2-T1
就得到了RTT
。但现实中,如果发生了数据包重传,那么发送端得到ACK
时,我们不能确定它是重传的ACK
,我们也不能确定它是第一个ACK
, 因为重传时,两个ACK
可能经过不同的路径先后到达发送端。这就是重传二义性。
当发生超时时,通过一个退避系数(backoff factor)加倍,直到收到正常的非重传数据为止重置退避系数为1。这是Karn算法
的重要部分,在发生超时时,同时会引发拥塞控制机制。
基于时间戳的RTT测量
在Linux中开启sysctl选项net.ipv4.tcp_timestamps=1
即可开启TCP的时间戳选项。Linux通过精度为1ms
的时间戳来估计RTT,这同时也可以规避上面的重传二义性问题。
在通信两端都开启时间戳的情况下, 发起方将会在TCP Option中加入一个Timestamps选项,设定一个32位数TSval
(Timestamp value)为发送时的系统时间戳, 如果服务端也支持,那么会在Option中加入TSecr
(Timestamp echo reply)将这个值原封不动放入,并放入自己的TSval
。这样任意一方收到数据包时,就可以知道对方的ACK
是针对自己什么时候发送的数据包做出的回应,从而精确到计算出RTT
值。
Linux RTO估计行为
Linux中RTO的上下限分别为TCP_RTO_MAX
和TCP_RTO_MIN
,分别为120s和200ms。在特殊网络环境下,如同一机房内的集群,机器间通信RTT可能低于1ms。由于rttvar
在默认情况下权重过大,当RTT减少可能反而导致RTO
升高,以200ms作为下限。此时,如果网络中出现丢包,由于RTO远远超过实际RTT,重传将会严重降低网络性能。在Linux中,当出现这种情况时,可以通过削弱rttvar
的比重来降低影响。在RKS07中,作者发现将Linux的TCP_RTO_MIN
从200ms调整到100ms的效果几乎可以忽略,但是调整rttvar
在计算RTO
时的比重可以有效的改变效率。
快速重传
快速重传与超时重传在Linux网络实现中同时存在。当观察到有至少dupthreshold
个重复ACK
后,不必等到超时计时器生效,就可以重传数据。当然,也可以同时发送新的数据。这种方式的优点是不必等到超时再重传数据包,缺点是如果网络只是偶发的出现了重传,可能会导致不必要的重传。
带选择确认的重传
SACK
选项允许TCP选择重传哪些数据包。当发送端收到一个ACK
时,这个ACK
与缓冲区其它数据(还没有收到ACK的)之间就形成了一个“空缺”,通过报告这个空缺,可以让对方有效的进行选择重传。
SACK
在接收端,通过报告缓存中的“空缺”,从而让发送端知道该重传哪些数据。 SACK
在发送端,在接收到了SACK
或重复ACK
时,可以推断需要重传的空缺数据。 由于这些行为在两端都是“建议性”的,因此可能出现变更,导致不必要的重传。
伪超时与伪重传
很多时候,即使没有出现数据包丢失也可能出现重传,这种不必要的重传被称为伪重传(spurious retransmission),其主要造成的原因是伪超时(spurious timeout),即过早的判定超时。在RTT显著增长超过当前RTO,或者出现包失序,丢失时可能出现这种伪重传。
在接收端发现伪重传时,发送DSACK
,从而告知发送方发生了伪重传,但这需要等到接收端发现并返回。也可以通过Eifel响应算法来提前检测出伪超时。
Linux下查看当前的TCP连接RTT状态
ip tcp_metrics
202.89.233.100 age 840.896sec cwnd 10 rtt 105537us rttvar 63949us source 10.8.4.106