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 用于控制对SYNACK发送失败多少次应该放弃连接

TCP定义了R1R2两个阈值来决定如何重传同一个报文段,当达到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_MAXTCP_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