一、虚假重传
在一些情况下,TCP可能会在没有数据丢失的情况下初始化一个重传,这种重传就叫做虚假重传(Spurious retransmission)。发生虚假重传的原因可能是包传输中重排序、传输中发生包复制、ACK确认包传输中丢失等等。如果由于链路时延变化或者负载变化等因素导致RTT突然变大等原因,TCP的发送端可能还没收到ACK确认包就已经RTO超时而触发重传,这种重传就叫做虚假超时重传(Spurious retransmission timeouts)。虚假超时重传会降低网络性能,其主要由两方面的影响,一个是会导致重传已经发出的但是还没有收到ACK确认的数据,而这部分数据包的ACK报文可能只是延迟到达而已,因此对应的重传是不必要的,另一方面是虚假超时后进入慢启动阶段,每当收到一个ACK确认包的时候可以发出两个数据包,而原来初传的数据包实际上并没有丢失,因此增加了网络负载,违背了包守恒原则。慢启动和包守恒等内容在后续拥塞控制进行介绍。因此有必要进行检测处理。对于虚假超时有几种方法来处理,一般处理方法都是分为两部分一个分是探测(detection)算法,另外一部分是响应(response)算法。当探测算法探测到一个虚假超时后,再调用响应算法撤销之前RTO超时的影响。当前探测算法有FRTO、DSACK、Eifel探测算法等,我们这里主要介绍FRTO和DSACK,以对虚假重传的探测有一定的简单认识。Eifel探测算法则留到后面拥塞控制的时候进行介绍。响应算法一般主要有两部分操作,一个是修正RTO值,第二个是撤销拥塞控制的处理。linux中主要是撤销拥塞控制并不会去修正RTO。拥塞控制相关内容我们后面内容在进行介绍。
二、FRTO简介及linux实现介绍
其中FRTO(Forward RTO-Recovery ,也简称为F-RTO)就是一种发送端的无效RTO超时重传检测方法。在RTO超时重传了第一个数据包之后,FRTO会检测之后收到的ACK报文来判断刚刚的超时重传是否是虚假重传(即对端实际上已经接收到了对应的TCP报文,但是发送端仍然RTO超时进行了重传),然后根据判断结果来决定接下来是接着进行重传还是发送新的未发送数据。在传输链路RTT抖动比较大的场景下(例如无线网络),FRTO可以有效的避免减少后续的虚假重传从而提升TCP性能。相比于Eifel、DSACK探测算法,FRTO不需要任何额外的TCP选项支持,而且不需要修改接收端实现。协议中给出了两种算法一种是基础的FRTO算法,一种是SACK增强的FRTO算法,linux的FRTO代码重构后只实现了SACK增强的FRTO算法(增强算法也可以用于普通场景),当/proc/sys/net/ipv4/tcp_frto设置为非0值的时候表示打开FRTO算法。
我们简单介绍一下linux中FRTO的实现,想深入了解的话请参考RFC5682和linux中的相关代码。这里涉及到大量拥塞控制的相关状态(Open、Disorder、Recovery、Loss等等)可以看完拥塞控制再来看这块。然后我们通过wireshark示例来进一步理解FRTO
先介绍一个概念
RecoveryPoint:在进行RTO超时重传的时候,当前已发送的数据中的最高系列号,例如发送端发出P1(0-9)、P2(10-19)、P3(20-29)三个TCP报文后如果P1超时重传,那么此时RecoveryPoint就是29,实际linux内部使用high_seq状态变量维护记录的是(RecoveryPoint+1),也就是记录的30。
1、在RTO超时的时候,TCP发送端进入Loss状态,判断是否启动FRTO过程,如果tcp_frto开启,当前不是PMTU过程,且不是进入Loss状态前不是Loss状态或者Recovery状态,那么对这次RTO超时开启FRTO检验。接着发送RTO超时重传报文。
2、如果上一步判断对当前RTO超时启动了FRTO过程,那么对于收到的ACK,判断是否带有SACK信息,并且SACK确认了RecoveryPoint之前的数据且SACK确认的数据是没有在第一步重传的数据。那么认为RTO重传是虚假重传,通过两个操作尝试撤销Loss状态的部分影响,一个是尝试更改拥塞窗口cwnd,另外一个是标识先前的数据包为非丢失状态,重传完,进入Open状态,并退出FRTO过程,不在执行下面的其他步骤。因为先前的数据包标记为非lost状态了,接下来回到正常流程就会发送新的数据包。
3、如果上一步判断中没有认定虚假重传,那么如果这个ACK确认包确认的数据超过RecoveryPoint,那么退回到普通的重传恢复状态,不再执行下面的步骤。
4、如果收到的ACK确认包是RTO后的第一个ACK确认包,且ack number确认了新数据,那么把RecoveryPoint更新为当前发送的最高系列号并尝试发送新的未发送数据。如果没有新数据发送,则退出FRTO过程,执行普通的RTO超时重传处理,不在执行接下来的过程。
5、如果收到了第二个ACK确认包,如果这个确认包SACK了新的数据或者是dup ACK那么认为是真实重传,退出FRTO过程执行普通的RTO超时重传流程。
三、wireshark示例
1、设置tcp_frto=2,返回SACK确认部分未重传的数据
首先通过一个RTO超时重传(No7和No8)来缩减拥塞窗口,这样server端在收到No9反馈包的时候就只能发出两个数据包了,接着server端写入500bytes的数据,server发出两个数据包No10和No11(设置了MSS为62,减掉12bytp的TSOPT选项后,每个数据包大小最大为50bytes了)
client对No9回复一个ACK确认包,server端再次发出两个数据包(No13和No14)
No13数据包RTO超时后进行重传,对应No15数据包,此时判断满足启动FRTO的条件
client回复No16数据包Ack=109确认了No11数据包,同时通过SACK选项告诉server端client收到了No14报文
server端收到No16确认包后发现当前FRTO为启动状态,并且ACK报文满足上面FRTO流程中的第2步,即SACK确认了RecoveryPoint之前的数据且SACK确认的数据是没有在第一步重传的数据,因此认定这个RTO是虚假重传,尝试撤销Loss状态的部分影响,进入open状态(实际上进入open状态后又发现满足了进入Disorder的条件,因此最终实际上进入了Disorder状态),此时FRTO流程结束。
因为上一步已经对No13报文取消了lost标记,因此进入Disorder状态后并不会重传No13而是传输了新的未发送数据即No17报文,接着client回复ACK确认,整个传输过程结束
2、设置tcp_frto=0,返回SACK确认部分未重传的数据
我们重复上面的业务模型,但是这次选择关闭FRTO,如下图所示No1到No14包的流程与前面相同,不同点如下
No15包为RTO超时重传包,此时判断开关关闭不对这次RTO超时重传启动FRTO探测
server在收到No16包的时候发现当前没有启动对FRTO探测,因此server端仍然处于Loss状态,No16包触发一个慢启动重传(SlowStartRetrans实际上走的就是快速重传流程),接着没有了待重传的数据传输了一个新的未发送数据包No18
client端回复对应No17和No18的ACK确认包
3、设置tcp_frto=2,第一个ACK确认了新数据但是没有SACK选项,第二个ACK带有SACK选项。如下图所示
No1-No15数据包与示例1相似不再解释
client接收到到No15的重传之后,回复一个不带有SACK选项的partial ACK确认收到数据包No11,此时server端还有No13和No14两个数据包没有被ACK确认
server端的FRTO流程接收到No16确认包之后,发现满足上面描述的第4点,即“如果收到的ACK确认包是RTO后的第一个ACK确认包,且ack number确认了新数据,那么尝试发送新的未发送数据”,这时候缓存中正好还有一个数据包,因此FRTO流程发出了No17的新数据包。示例1中此处也是发出了新数据包,但是示例1是判断为虚假重传退出了Loss状态和FRTO流程。而这里发出的No17新数据包是在FRTO流程中发出去的,server端仍然处于Loss状态,FRTO流程还需要等待下一个ACK确认包
接着FRTO流程收到No18确认包,包含有确认了No17数据包的SACK信息,满足上面描述的第5点:“如果收到了第二个ACK确认包,如果这个确认包SACK了新的数据或者是dup ACK那么认为是真实重传,退出FRTO过程执行普通的RTO超时重传流程。”FRTO判断本次RTO超时为真实的RTO超时,退出FRTO流程执行普通的RTO超时慢启动流程
接着慢启动重传了No19和No20两个数据包,client回复对应的ACK报文。
补充说明:
1、细心的读者会发现这几个示例中client端的TSopt选项回复的TSECR是有问题的,正常的话实际上会触发Eifel虚假重传探测,后面拥塞控制部分会介绍Eifel探测相关内容。实际上所使用的client端TSopt的实现有两种方式,一种是直接echo最近接收到的TSval,另外一种是按照协议要求实现的。具体选择那种方式可以通过参数选择来设置,注意第一种直接echo最近接收到的TSval是不符合协议的错误的实现。除了后面会介绍到的Eifel探测client端选用了协议标准的实现方式外,其余示例大部分都是使用的第一种方式。