14.1 引言

到目前为止,我们并没有过多的讨论效率和性能问题,我们主要关心的是运行的正确性。本章及接下来的两章,我们不仅关注TCP执行的基本任务,还会关注TCP执行效率。TCP使用其下面的网络层(IP层,可能会丢包、重复、包重排序)为两个应用程序间提供了可靠的数据传输服务。为了提供无误的数据交换,TCP重发它认为已经丢失的数据,为了确定哪数据需要重发,TCP依赖接收方到发送方的连续确认流,当数据段或确认丢失时,TCP重传未确认的数据。TCP有两种独立的机制来完成重传,一种基于时间,另一种基于确认信息的构成,第二种方法通常比第一种更高效。

TCP发送数据时会设置一个计时器,如果计时器到期时没有收到确认,就会触发超时重传或基于计时器的重传。超时发生在被称为重传超时(RTO)的时间间隔之后。还有另外一种重传称之为快速重传,快速重传不会有任何延迟。当TCP累积确认未能随着不断收到的ACK而增长,或者ACK携带选择确认信息(SACK)表明接收方有乱序报文时,快速重传基于上述规则推断出现丢包。通常,当发送方认为接收方丢失数据时,需要在发送新数据(未发送过的)和重传之间做出选择。本章将仔细研究TCP如何判断报文段丢失以及丢失时发送什么数据,发送多少数据的问题我们推迟到第16章再讨论,到时我们将讨论怀疑有丢包时常调通常调用的拥塞控制过程。这里,我们研究如何根据连接的往返时间(RTT)来设置RTO、基于计时器的重传机制以及TCP的快速重传机制如何工作的。我们还将讨论如何使用SACK来帮助TCP确定接收方丢失了哪些数据、IP包重排序及重复对TCP行为的影响以及TCP重传时改变包大小的方式。我们还将简要介绍一些可以用来欺骗TCP,使TCP过分积极和被动的攻击方法。

14.2 简单的超时与重传举例

我们已经见过了一些超时和重传的例子。(1)第8章的ICMP目标不可达(端口不可达)示例中,采用UDP的TFTP客户端使用一个简单的(且低效)超时和重传策略:TFTP认为5秒是一个合适的超时时间间隔,每5秒重传一次。(2)第13章[h1] 中尝试连接到一个不存在的主机时,TCP试图建立连接时,它在每次重传SYN报文段时使用越来越长的延迟。(3)第3章中,可以看到以太网遇到冲突时会发生什么。所有这些机制都是计时器到期引发的。

我们先看一下TCP使用的基于计时器的重传策略。先建立一个连接,发送一些数据来验证一切正常,然后断开连接的一端,再发送一些数据并观察TCP如何处理。这个案例中,我们使用Wireshark来查看连接的过程(如图 14‑1所示)。

tcpip必须知道的十个问题(TCPIP详解第14章超时与重传)(1)

图 14‑1 TCP超时和重传机制的一个简单示例。第1次重传发生在42.954,紧接着在43.374、44.215、45.895和49.255发生重传。连续重传的间隔分别为206ms、420ms、841ms、1.68s和3.36s,这些时间表明同一报文段连续重传的超时时间加倍。

报文段1、2、3为正常的TCP连接建立握手,当Web服务器完成连接建立后,它静静的等待Web请求。在发起请求之前,我们隔离(断开)服务器主机。客户端输入如下:

Linux% telnet 10.0.0.10 80

Trying 10.0.0.10...

Connected to 10.0.0.10.

Escape character is '^]'.

GET / HTTP/1.0

Connection closed by foreign host.

这个请求无法发送到服务器,所以它在客户端的TCP队列中保留了很长时间。在此期间,客户端的netstat命令输出表明队列不是空的:

Active Internet connections (w/o servers)

Proto Recv-Q Send-Q Local Address Foreign Address State

tcp 0 18 10.0.0.9:1043 10.0.0.10:www ESTABLISHED

可以看到发送队列中有18个字节等待被发送到Web服务器,这18个字节包括前面请求中显示的字符以及两组回车换行符。输出的其它细节(包括地址和状态信息)将在下面说明。

报文段4是客户端第一次发送Web请求,时间为42.748s。紧接着的下一次尝试在0.206s后即42.954s。然后0.420s后即43.374s又发起了一次尝试。其它的重试(重传)发生在44.215s、45.895s和49.255s,间隔分别为0.841s、1.680s和3.360s。

每次重传间隔时间加倍称为二进制指数退避(binary exponential backoff),我们在第13章TCP尝试建立连接失败时看到过,稍后我们会详细讨论它。如果我们计算初始请求到连接终止所用的时间,大约是15.5分钟,然后,客户端会显示如下信息:

Connection closed by foreign host.

逻辑上讲, TCP有两个阈值来确定重传同一个报文段持续多长时间,这2个阈值在主机需求RFC[RFC1122]中有描述,我们在第13章中简要地提到了它们。阈值R1表示TCP向IP层传递“消极建议”前(如重新评估正在使用的IP路径)尝试的次数(或等待时间),阈值R2(大于R1)表示TCP应该放弃连接的时机,建议R1和R2应分别至少设置为3次重传和100秒。对于连接建立(发送SYN段),阈值可能和数据报文段的设置不同,SYN报文段的R2值要求至少为3分钟。

在Linux中,正常数据段的R1和R2值可以由应用程序更改,也可以分别使用系统级的配置变量net.ipv4.tcp_retries1和net.ipv4.tcp_retries2更改,单位为重传次数,而不是以时间为单位。tcp_retries2的默认值是15,大约对应13-30分钟,具体取决于连接的RTO。tcp_retries1的默认值是3。对于SYN报文段,net.ipv4.tcp_syn_retries和net.ipv4.tcp_synack_retries限制了SYN报文段的重传次数,它们的默认值是5(大约180秒)。Windows也有许多影响TCP行为的变量,包括R1和R2的值,可以通过以下注册表项[WINREG]修改这些值:

HKLM\System\CurrentControlSet\Services\Tcpip\Parameters

HKLM\System\CurrentControlSet\Services\Tcpip6\Parameters

TcpMaxDataRetransmissions的值对应Linux中tcp_retries2的值,默认值为5。即使是目前我们看到的简单重传的例子,也需要TCP为重传计时器设置超时值,此值决定发送数据后等待ACK多长时间。若TCP只在静态环境中使用,为超时值选择一个合适的值是比较容易的。但TCP需要运行在各种各样的环境下,且环境可能随时时间的推移而变化,因此TCP需要根据当前的状况来确定超时值。例如,如果一个网络的链路故障,流量重新路由,RTT就会改变(可能变化很大),换句话说,TCP需要动态的确定RTO,我们接下来考虑这个问题。

14.3 设置RTO

TCP超时和重传过程的原理是如何根据给定连接的RTT测量值设置RTO。如果TCP早于RTT重传报文段,可能会将不必要的重复数据注入网络。相反,如果它延迟发送时间比RTT大得多,那么当数据丢失时,网络整体利用率(和单个连接的吞吐量)就会下降。RTT测量会使事情变得更加复杂,因为RTT会随着时间的推移而改变,因为路由和网络使用会发生变化,TCP必须追踪这些变化并相应地修改其超时时间以保持良好的性能。

因为TCP在收到数据后发送确认,所以可以发送一个具有特定序号的单字节,并测量收到能覆盖该序号的确认所需的时间,每一个这样的测量被称为RTT采样。TCP所面临的挑战是,根据给定一组随时间变化的样本,建立RTT取值范围的良好估计值,第二步是如何根据这些值设置RTO,得到这个“正确的RTO”对于TCP的性能非常重要。

每个TCP连接的RTT是独立估算的,并且重传计时器会为任何消耗序号(包括SYN和FIN报文段)的在途数据计时。多年来,人们一直在研究如何设置这个计时器,偶尔也会做出一些改进。本节将探讨计算RTO方法演进过程中一些重要里程碑。我们从第一个(“经典”)方法开始,详细介绍见[RFC0793]。

14.3.1 经典方法

最初的TCP规范[RFC0793]采用如下公式得到平滑的RTT估计(称为SRTT):

SRTT基于现有的SRTT值和新的样本值RTTs进行更新,常量 为平滑或比例因子,推荐值为0.8-0.9。每次得到新的样本值时SRTT就会更新, 使用推荐值的情况下,新的估计值80%-90%来自前一次的估计值,10%-20%来自新的样本值,这种类型的平均也称为指数加权移动平均(EWMA)或低通滤波器。该方法容易实现,因为它只需要保存先前的SRTT就可得到新的估计值。

SRTT随着RTT的变化而变化,[RFC0793]建议将RTO按如下方式设置:

其中 为延迟变化因子,推荐值为1.3-2.0。ubound是RTO的上边界(建议,如1分钟),lbound是RTO的下边界(建议,如1秒)。我们将此方法称为经典方法,它通常会将RTO设置为1秒或2倍的SRTT大小,对于相对稳定的RTT分布来说,此方法效果不错。然而,当TCP运行在RTT变化较大的网络(如早期的分组无线网络)上时,性能就没那么好了。

14.3.2 标准方法

在[J88]中,Jacobson进一步详细说明了经典方法存在的问题——按[RFC0793]设置的计时器不能适应RTT大的波动(特别是当实际RTT比预期大得多时,会导致不必要的重传),RTT样本值增大表明网络已经过载,不必要的重传会进一步加重网络负担。

为了解决这个问题,需要对计算RTO的方法进行改进以适应RTT的大的波动,这可以通过追踪RTT的变化和均值估计值来实现。与仅将RTO设置为均值的常数倍( )相比,基于均值估计值和RTT的变化得到的RTO能提供更好的超时响应。

[J88]中图5和图6展示了采用[RFC0793]方法得到的RTO值与考虑RTT变化得到的RTO的值的对比情况,如果将TCP的RTT采样看作一个统计过程,同时对均值和方差(或标准差)进行估计能更好的预测未来值。对RTT取值范围的良好预测有助于TCP在大多数情况下设置一个即不太大也不太小的RTO值。

如Jacobson所述,平均差(mean deviation,MD)是一种较好的对标准差的逼近,但平均差更容易、更快。计算标准差需要计算方差的平方根,对于快速TCP来说代价太大(这并不是全部,参见[G04]中关于“辩论”的有趣历史)。因此我们需要对均值和平均差同时进行估计,对于每个RTT测试值M(前面称为RTTs)采用如下公式计算均值和平均差[h2] :

srtt替代了前面的SRTT,rttvar为平均差的指数加权移动平均(EWMA),rttvar替代了 用于计算RTO。在计算机上实现时,这组方程可以写成另外一种操作步骤较少的形式:

如前所述,srtt为均值的指数加权移动平均(EWMA),rttvar为绝对误差 |Err| 的指数加权移动平均(EWMA),Err为测量值M和当前当前RTT估计值srtt的差值。srtt和rttvar都用于计算RTO,增益g为新的RTT样本M在均值srtt中所占的权重,设置为1/8。增益h为新的平均差样本(新样本M与当前均值srtt的绝对差)在平均差估计值rttvar中的权重,设置为1/4。偏差的增益越大,RTT变化时RTO上升的越快。g和h的值选择为2的(负)次幂,允许在计算机上使用带有移位和加法运算的定点整数算法来实现所有计算,而不用乘法和除法。

注意

[J88]在RTO计算中使用的2*rttvar,但经过进一步研究,[J90]将其改为了4*rttvar,BSD Net/1实现采用了该值并最终形成了标准[RFC6298]。

比较经典方法与Jacobson方法,RTT均值计算是类似的( )但使用了不同的增益。此外,Jacobson方法的RTO计算依赖平滑RTT和平滑偏差,而经典方法采用平滑RTT的倍数,这是今天许多TCP实现计算RTO的基础。由于[RFC6298]采用了Jacobson方法作为基础,我们称之为标准方法,在[RFC6298]有一些细微改进,我们现在讨论这些改进。

14.3.2.1 时钟粒度与RTO边界

在RTT测量过程中,TCP的“时钟”始终处于运行状态。和初始序号一样,实际的TCP连接的时钟不是从0开始且精度有限。TCP时钟通常是一个变量值并随着系统时间的前进而更新,但不一定是一对一的同步更新。TCP时钟“滴答”的长度称为粒度,通常,该值比较大(约500ms),但最近的实现使用了更细粒度的时钟(例如,Linux为1ms)。

时钟粒度影响RTT测量细节和RTO的设置,在[RFC6298]中,时钟粒度用于优化RTO的更新,并给RTO设置了一个下界,公式如下:

其中G为计时器粒度,1000ms为整个RTO的下界([RFC6298]中规则(2.4)的建议值),因此,RTO总是至少为1s。可以使用可选的上界,若使用上界则值至少为60s。

14.3.2.2 初始值

我们已看到估计器如何随着时间而更新的,但我们还需要知道如何设置初始值。第一次SYN交换前,TCP不知道RTO的值应设置为多少,也不知道估计器的初始值,除非是系统提供了此信息(有些系统在转发表中缓存这个信息,见14.9节)。根据[RFC6298],虽然初始SYN报文段的超时时间为3s,但RTO的初始值应该为1s,当接收到第一个RTT测试量M时,估计器按如下方式初始化:

现在我们已经知道了估计器如何初始化及更新的细节了,这个过程依赖获得RTT样本,这个过程看起来很简单,但并不总是这样。

14.3.2.3 重传二义性与Karn算法

当数据包重传时测量RTT样本会出现问题,假设传输了一个数据包,出现超时,该数据包被重传并收到了确认,这个ACK是第一次发送数据包的确认还是重传的数据包的确认?这是重传二义性的一个例子。除非使用了时间戳选项,否则ACK只提供确认号而无法指明对哪一个(第一次发送还是重传的)报文的确认。

论文[KP87]指出,当发生超时和重传时,最后收到重传数据的确认时,不能更新RTT估计值,这是Karn算法的“第一部分”。它通过排除二义性的数据来解决确认二义性问题,这是[RFC6298]中的要求。

如果在设置RTO时完全忽略重传段,很可能会将网络提供的一些有用信息忽略(例如,网络当前无法快速发送数据包)。在这种情况下,通过降低重传速率来降低网络负载是有益的,至少在数据包不再丢失之前是这样,这个推论是我们在图 14‑1中看到的指数退避行为的基础。

TCP计算RTO时使用一个退避系数,每当重传计时器超时时,退避系数翻倍,翻倍一直持续到收到未重传的段的确认为止,此时,退避系数被设置为1(即,二进制指数退避被取消),重传计时器回归到正常值。重传时退避系数翻倍是Karn算法的“第二部分”。注意,当TCP超时时,它还会调用拥塞控制过程改变其发送速率(拥塞控制将在第16章详细讨论)。Karn算法实际上包括两部分,引用1987年的论文[KP87]如下:

已发送多次(即至少重传一次)的数据包的确认到达时,忽略基于此数据包的任何RTT测量,从而避免重传二义性问题。此外,当前数据包的RTO会应用于下一个包,只有当数据包(或随后的数据包)被确认且中间没有重传时,才会根据SRTT重新计算RTO。

Karn算法作为TCP实现中必需的方法已经有一段时间了(自[RFC1122]起),但有一个例外,当使用TCP时间戳时(见第13章),可以避免确认二义性问题,此时Karn算法的第一部分不再适用。

14.3.2.4 使用时间戳选项进行RTT测量(RTTM)

时间戳选项(TSOPT)除了作为我们在第13章中看到的PAWS算法基础外,还可以用于RTT测量(RTTM)[RFC1323]。时间戳选项的基本格式已经在第13章做了描述,它允许发送方在TCP报文段中包含一个32位的数字,并在其对应的确认中返回。

初始SYN报文段的时间戳选项中携带时间戳值(TSV),并在SYN ACK报文段的时间戳选项TSER中返回此时间戳值,以此来设定srtt、rttvar和RTO的初始值。由于初始SYN报文段被认为是数据(即如果丢失会重传且占用一个序号),因此可以测量它的RTT,其它报文段中也携带有时间戳选项,所以可以持续估计连接的RTT。这看起来很简单,但由于TCP并不是对它接收到的每一个报文段进行一次确认,所以会变得比较复杂。例如,当传输大量数据时,TCP通常每两个报文段返回一个ACK(见第15章)。另外,当数据丢失、重排序或成功重传时,TCP的累积确认机制意味着一个报文段和其ACK间并没有固定的对应关系。为了处理这些问题,使用时间戳选项的TCP(目前大多数,包括Linux和Windows)使用如下算法获取RTT样本:

1. TCP发送方在它发送的每个TCP段的时间戳选项的TSV部分携带一个32位的时间戳,这个值是报文发送时的TCP时钟值。

2. TCP接收方记录(通常保存在TsRecent变量中)收到的TSV并在它生成的下一个ACK中发送,并记录它发送的最后一个确认的确认号(通常在LastACK变量中)。回想一下,确认号表示接收方(即ACK的发送方)期望接收的下一个序号。

3. 当一个新报文段到达时,如果它包含的序号与LastACK相匹配(即此报文段是期望的报文段),则将新报文段的TSV值保存在TsRecent中。

4. 接收方无论什么时候发送ACK都要包含时间戳选项,并将TsRecent变量值放入时间戳选项TSER部分。

5. 发送方收到使窗口移动的ACK时,使用当前TCP时钟减去TSER的值,得到的差用来更新RTT估计值。

FreeBSD、Linux以及更高版本的Windows默认启用时间戳选项。在Linux中,系统配置变量net.ipv4.tcp_timestamps标识是否使用时间戳(0代表不使用,1代表使用)。在Windows中,由注册表Tcp1323Opts的值来控制,如果值为0,时间戳禁用;如果值为2,时间戳启用,该键没有默认值(默认注册表中不存在),默认如果对方发起连接时使用时间戳则就使用时间戳选项。

14.3.3 Linux采用的方法

Linux的RTT估计过程与标准方法略有不同,它采用的时钟粒度为1ms,时间戳的精度也为1ms,与其它实现相比,其时间粒度更细。采用更频繁的RTT测量及更细的时钟粒度有助于更准确的估计RTT,但rttvar的值也会随着时间的推移而变小[LS00],因为当累积了足够多的均差样本时,它们会相互抵消,这是设置与标准方法不同的RTO的一个考虑因素。另一个与标准方法在RTT样本明显小于现有RTT的估计srtt时增加rttvar的方式有关。

为了更好地理解第二个问题,回想一下RTO通常设置为srtt 4(rttvar),因此,无论新的RTT样本是大于还是小于srtt,任何导致rttvar变大的因素都会导致RTO变大。这有点违反直觉——如果实际RTT显著下降,则RTO就不应该增加。Linux通过限制显著下降RTT对rttvar的影响来解决这一问题。现在,我们详细介绍Linux设置RTO的方法,这个方法解决了刚才所说的两个问题。

与标准方法一样,Linux使用了srtt和rttvar变量,同时还使用了两个新变量mdev和mdev_max。值mdev使用前面描述的rttvar的标准算法记录平均差的运行估计,mdev_max记录了最后一次测量的RTT中所看到的mdev的最大值且不允许小于50ms。此外,rttvar会定期更新以确保它至少于mdev_max一样大,因此,RTO永远不会低于200ms。

注意

可以修改RTO最小值,TCP_RTO_MIN是一个内核配置常量,可以在重新编译和安装内核前更改。一些Linux版本允许使用ip route命令更改此常量。在数据中心网络使用TCP时,RTT可能只有几微秒,若RTO最小为200ms会导致性能严重下降,因为本地交换机丢包后TCP恢复缓慢,这就是所谓的TCP incast问题。针对这个问题有多种解决方案,包括修改TCP计时器粒度和将RTO最小值设为微秒级[V09],不建议在全球因特网上使用这样小的RTO最小值。

当最大值mdev_max增加时,Linux使用mdev_max的值更新rttvar,它总是将RTO设置为srtt 4(rttvar),并且确保RTO的值不会超过TCP_RTO_MAX,后者的默认值为120秒,详见[SK02]。可以在图 14‑2中看到这一过程细节,该图还展示了时间戳选项的使用方式。

tcpip必须知道的十个问题(TCPIP详解第14章超时与重传)(2)

图 14‑2 TCP时间戳选项携带发送方时钟副本,ACK会将此副本返回给发送方,发送方使用两者差值(当前时钟减去返回的时间戳)来更新它的srtt和rttvar的估计值。为了清晰起见,这里只描述了一组时间戳。在Linux系统中,rttvar的值被限制为至少50ms,RTO的下界为200ms。

在图 14‑2中,TCP连接在启动时使用了时间戳选项。发送方系统为Linux 2.6,接收方系统为FreeBSD 5.4。为清晰起见,序号和时间戳取的相对值。此外,只显示了发送方的时间戳,了为使数字易读,图中也没有严格按照时间比例来画。根据本例中的初始RTT测量值,Linux进行如下更新:

• srtt = 16ms

• mdev = (16/2)ms = 8ms

• rttvar = mdev_max = max(mdev, TCP_RTO_MIN) = max(8, 50) = 50ms

• RTO = srtt 4(rttvar) = 16 4(50) = 216ms

初始SYN报文段交换后,发送方对接收方的SYN返回了ACK,接收方用一个窗口更新作为响应。由于这些包不包含数据[h3] (SYN或FIN位被置位的包算作数据),它们不会被计时。当发送方收到窗口更新时也不会执行RTT估计更新。不包含数据的段不能被TCP可靠的传输,这意味着如果包丢失不会被重传,这些类型的段不需要设置重传计时器,因为它们永远不会被重传。

注意

值得一提的是,TCP选项本身不会被重传或可靠传输,只有当选项专门放到数据段(包括SYN和FIN报文段)时丢失时才会重传,这只是一个副作用。

应用程序第一次写操作时,发送方发送了2个报文段,每个段的TSV值为127,因为这两个段的发送间隔小于1ms(发送方TCP时钟粒度),所以这两个段的TSV值是相同的。当发送方以这种方式连续发送多个报文段时,很容易看到TCP时钟不变或改变很小的情况。

接收方LastACK变量保存着接收方最后发送的ACK号,本例中,LastACK从1开始,因为最后发送的ACK是连接建立期间发送的SYN ACK包。当第一个完整的(full-size)段到达接收方时,它的序号与LastACK匹配,所以TsRecent变量值更新为收到的报文段的TSV值127,第二个报文段的序号与LastACK不匹配,所以不会更新TsRecent变量值。接收方对到达的包进行响应发送ACK,其时间戳选项TSER部分包含了TsRecent的值,并且ACK的发送也导致了接收方更新LastACK变量的值为确认号2801。

当这个ACK到达发送方时,TCP可以进行第二个RTT样本测量,拿当前TCP时钟减去到达包的TSER值得到测量值m:m=223-127=96,根据该测量值,TCP更新连接变量如下:

• mdev = mdev (3/4) |m-srtt|(1/4) = 8(3/4) |96-16|(1/4) = 26ms

• mdev_max = max(mdev_max, mdev) = max(50, 26) = 50ms

• srtt = srtt (7/8) m(1/8) = 16(7/8) 96(1/8) = 14 12 = 26ms

• rttvar = mdev_max = 50ms

• RTO = srtt 4(rttvar) = 26 4(50) = 226ms

如前所述,Linux TCP对经典的RTT估计算法有几处改进。经典算法提出时,TCP时钟的典型粒度是500ms并且时间戳也没有广泛使用,它只是每个窗口取一个RTT样本并更新相应的估计值,如果时间戳不可用或未启用则依然采用此方法。

如果每个窗口只采集一个RTT样本,则rttvar项的变化相对较慢。利用时间戳和对每个包的测量,可以得到更多的样本值。同一个窗口内的数据每个包的RTT通常都是不一样的,在较短的时间内(例如,当窗口很大时)进行如此多的测量会导致均差估计变小(根据大数定律[F68]接近于0)。为了解决这个问题,Linux维护mdev变量作为平均差估计,但基于rttvar来设置RTO,rttvar为数据窗口期间mdev的最大值且至少为50ms,仅当进入下个窗口时,rttvar才允许减小一次。

标准方法中rttvar项使用了一个较大的权重(系数4),因此即使当RTT减小时,RTO也倾向于变大。时钟粒度较粗时(例如500ms),这可能影响相对较小,因为RTO可以取的值非常少。然而,当时钟粒度较细时,比如Linux使用的1毫秒,就可能会出现问题。为了解决这个问题,Linux会处理RTT下降的情况,如果新样本小于估计的RTT的范围的下界(srtt – mdev),那么新样本会被赋予更小的权重。完整的关系式如下:

if(m < (srtt - mdev))

mdev = (31/32) * mdev (1/32) * |srtt - m|

else

mdev= (3/4) * mdev (1/4) * |srtt - m|

该条件判断新的RTT样本是否比预期的RTT测量范围下界还要小,如果是,则表明该连接的RTT正在显著减小。为了避免mdev(以及rttvar和RTO)在这种情况下增大,新的平均差样本|srtt - m|权重减少到原来的权重的1/8,总的来说,这避免了在RTT减少时RTO变大的问题。有关这些问题的深入讨论请参见[LS00]和[SK02]。在[RKS07]中,作者使用280万个TCP流评估了不同操作系统的TCP RTT估计算法,结论表明,Linux估计器是所有研究对象中最有效的,主要是因为它的收敛速度相对较快,通过减少RTT方差对RTO的影响来更有效地调整它。

现在回到图 14‑2,当接收端生成ACK 7001时,它的TSER包含的TSV的副本不是来自最近到达的段,而是最前面尚未确认过的段。当返给发送方时,此ACK导致RTT样本是从两个报文段中的第一个段开始计算的,而不是从最后发送的报文段开始计算的,这就是时间戳算法在延迟的(否则不稳定)ACK下的工作情况。当从最前面尚未确认的包开始测量RTT样本时,RTT样本是发送方等待ACK的时间,而不是实际的网络RTT。这点很重要,因为发送方要基于接收方期望的ACK速率来设置RTO,该速率可能小于包的发送速率。

14.3.4 RTT估计器行为

正如我们看到的,如何设置TCP的RTO及如何估计RTT已投入了大量的创新和设计, 图 14‑3显示了将标准算法的和Linux的算法应用于人造数据集上,更流行的估计器是如何工作的。为了演示,标准方法[RFC6298]推荐的RTO最小为1s已删除,目前大多数TCP实现都已不再采用此推荐值[RKS07]。

tcpip必须知道的十个问题(TCPIP详解第14章超时与重传)(3)

图 14‑3 Linux和标准方法的RTO设置及RTT估计算法应用于人造(伪随机)样本点。前100个点服从N(200,50)分布,后100点服务N(50,50)分布,并对负值做了取正处理。Linux避免了样本100后均值下降引起的RTO变大。在Linux中,最小的RTO实际上设置为了200ms,所以样本120后,标准方法更接近样本。对于本例,Linux在所有情况下都避免将RTO设置的太小,标准方法在样本78和样本191遇到了潜在的问题。

图中显示了由两个高斯概率分布N(200,50)和N(50,50)绘制的200个人造数据时间序列图,第一个分布对应前100个点,第二个分布对应后100个点,任何负的样本通过改变符号变为正值(仅针对第二个分布)。每个加号( )代表一个特定的样本值,在样本100之后,样本值明显下降,很容易看出Linux方法在样本100后立即减小了RTO,而标准方法在样本120后才开始减小。

我们看下Linux的rttvar线,可以看到它基本保持不变,这是因为mdev_max的最小值为50ms(因此rttvar也是如此),这使得Linux的RTO值总是至少为200ms,并避免了所有不必要的重传(尽管计时器触发的没有那么快,导致丢包时性能下降)。标准方法在样本78和样本191会遇到潜在的问题,可能会发生伪重传,我们以后再讨论这个问题。

14.3.5 丢包和包重排序时RTTM的鲁棒性

当没有丢包时,不论接收端是否延迟发送ACK,时间戳选项都可以正常工作。该算法在以下情况也能正常工作:

l 乱序报文段:接收端收到一个乱序报文段,通常是因为前一个报文段丢失了,应立即生成一个ACK以启动快速重传算法(见14.5节),这个ACK所携带的TSER值为最近有序到达接收端的报文段(即最近使窗口向前移动的报文段,通常不会是刚到达的乱序报文段)的TSV值,这会使得发送方RTT样本值变大,进而导致发送方RTO变大。包重排序后是有好处的,它能让发送方在重传前有更多的时间意识到包重排序了而不是丢失了。

l 成功重传:接收方收到一个可以填充接收方缓冲区“空洞”的报文段时(例如,由于成功重传),窗口通常向前移动,在这种情况下,对应的ACK的TSER值来自最近到达的段,这是有用的,因为如果使用较早的报文的TSV的值,可能导致发送方RTT估计出现大的偏差。

图 14‑4中的示例说明了这些要点,假设有3个报文段,每个报文段包含1024字节数据,接收到的顺序如下:包含第1-2024字节的报文段1、包含第2049-3072字节的报文段3、包含第1025-2048字节的报文段2。

tcpip必须知道的十个问题(TCPIP详解第14章超时与重传)(4)

图 14‑4 报文段重排序时,返回的时间戳是最后一个使接收方窗口向前移动的报文段的时间戳(不是到达接收方的最大的时间戳),这使得发送方RTO在包重排序期间高估RTT并降低重传积极性。

图 14‑4中返回的ACK 1025携带的报文段1的时间戳(正常的数据确认),然后另一个ACK 1025携带的报文段1的时间戳(重复确认,是对在窗口中但乱序的报文段的响应),紧接着是ACK 3073携带的报文段2的时间戳(而不是报文段3的时间戳)。当报文段重排序(或丢失)时会高估RTT,较大的RTT估计导致较大的RTO,使发送方不那么急于重传,这正是出现报文重排序情况时所需要的的,因为过分积极的重传可能是伪重传。

可以看到,时间戳选项可以让发送方即使在包延迟、丢失、重排序情况下也能进行RTT估计。发送方可以在时间戳选项中使用任何值来进行RTT测量,只要其单位与实际时间成比例且粒度合理,并能与TCP序号及链路速率相匹配(参见[RFC1323]以获得更多细节)。对于任何可信的RTT,TCP时钟至少要“滴答”一次。另一方面,它的变化速度不能快于59ns,否则保存TCP时钟的32位TSV值就可能在IP层允许单个包存在的最大时间内(255s [ID1323b])发生回绕。如果上述条件都没问题的话,RTO的值就可以用来触发重传。

14.4 基于定时器的重传

一旦发送方根据时变的RTT得到RTO后,只要发送报文段,它就能确保重传定时器设置合理。设置定时器时需要记录所谓的定时报文段序号,如果及时收到了该报文段的确认则重传定时器取消。发送方再次发送数据包时,会设置一个新的重传定时器,并记录新的定时报文段序号,旧的重传定时器已经取消,因此连接的发送方不断的设置及取消重传定时器,如果没有数据丢失,就不会出现定时器超时。

注意

这个结果有点让主机操作系统设计者有点惊讶,在典型的操作系统中,定时器用于通知各种各样的事件,定时器一般是设定后并等待超时(通过调用系统函数),而TCP要求设置定时器,然后重新设置或取消定时器,如果TCP工作良好,定时器永远不会超时。

在RTO时间内,TCP未接收到已经定时的段的ACK时,就会执行基于定时器的重传,我们已经在图 14‑1看到了这一点。TCP认为基于定时器的重传是相当重要的事件,当发生这种情况时,它迅速降低向网络中注入数据的速率,反应非常谨慎。通过两种方法可以做到这一点,第一种是根据拥塞控制机制(见第16章)减少发送窗口大小,第二种是重传的报文再次重传时成倍增大退避因子,即前面提到的Karn算法的“第二部分”,特别是同一报文段多次重传时,RTO值(临时的)乘以 得到新的退避超时值。

一般情况下, 值为1,连续重传时, 翻倍:2、4、8等等。通常 不能超过最大退避因子(Linux会确保所使用的RTO不会超过TCP_RTO_MAX值,默认为120s),一旦收到有效的ACK, 被重置为1。

14.4.1 例子

通过建立一个与图 14‑1和图 14‑2类似的连接可以看到重传定时器的动作,我们故意将序号为1401的段丢弃两次(见图 14‑5)。

tcpip必须知道的十个问题(TCPIP详解第14章超时与重传)(5)

图 14‑5 报文段1401被强制丢弃2次,导致发送方进行基于定时器的重传。只有让发送方窗口向前移动的ACK才更新srtt、rttvar、RTO。带星号(*)的ACK携带SACK信息。

本例中,我们使用一个特殊的函数发送TCP报文段,此函数可以将指定的TCP序号的报文段丢弃指定的次数。与图 14‑2相比,RTT增加了一些延迟。连接开始和之前一样,但当发送序号为1和1401的包时,丢弃第二个包。假设第一个包到达了接收方,但接收方并没有立即响应而是进行了延迟确认,接收方由于在225ms内没有确认,所以发送方重传定时器超时,导致序号为1的包重新发送(这次的TSV值为577),当重传的包到达接收方时使得接收方向发送方返回了ACK,由于这个ACK确认了数据并使发送方窗口前移,所以它的TSER值被用来更新srtt及RTO值,分别更新成了34和234。

接下来的三个ACK是对到达接收方的数据包的响应,带星号(*)的ACK都是重复确认并包含了SACK信息,我们在第14.5节和第14.6节讨论重复确认和SACK的影响,目前,由于这些确认没有使发送方窗口前移,所以TSER的值没有被使用。

最终报文段1401重传(TCP时钟为911时)并到达接收方,修复阶段完成,接收方使用确认号7001响应,表明所有数据已经接收。

重传定时器为TCP连接中数据丢失提供了“重新开始的最后手段”。在大多数情况下,让重传定时器触发重传是没必要的(也不是期望的),所以RTO通常比典型的RTT大(大约2倍或更大)。基于定时器重传常常导致网络不能被充分利用,幸运的是,TCP有另外一种方法来检测和修复丢失的包,通常这种方法总是比基于定时器的重传更高效,因为这种方法不需要重传定时器超时所以被称为快速重传。

14.5 快速重传

快速重传[RFC5681]是TCP的一种机制,它可以根据接收方的反馈而不是重传定时器超时来进行数据包重传。因此,使用快速重传与使用基于定时器的重传相比能更快、更有效的修复丢包,典型的TCP实现既会支持快速重传也会支持基于定时器的重传。在详细讨论快速重传前,需要知道当接收方收到一个乱序报文段时,TCP要求接收方立即生成确认(“重复ACK”),出现丢包但后续数据到达了接收方意味着接收方缓存出现了空洞(hole)、数据出现了乱序,发送方的工作变成了尽可能快的、高效的填补接收方的空洞。

当乱序数据到达时需要立即发送重复ACK,不能延迟发送,这样做的原因是让发送方知道收到了乱序的报文段并指明期望的序号(即空洞序号是多少)。当使用SACK选项时,重复ACK通常会包含SACK块,这些SACK块可以提供多个空洞的信息。

到达发送方的重复ACK(不论带不带SACK块)表明先前发送的包可能丢失。正如我们在14.8节详细讨论的那样,当包在网络中重排序时也会出现重复ACK——如果接收方收到一个超过了期望的序号的包时,期望的包可能丢失了或仅仅延迟到达了而已,通常不知道是哪一种情况。TCP在认定丢包触发快速重传前会等待一定数量的ACK(称为重复ACK阈值或dupthresh),通常dupthresh是个常量(值为3),但一些非标准实现(包括Linux)会根据当前重排序的程度改变这个值(参见14.8节)。

TCP发送方至少收到dupthresh个重复ACK时才会重传一个或多个可能已经丢失的包,而不会等到重传定时器超时,发送方也可能会多发送一些未发送过的数据,这就是快速重传算法的精髓。由重复ACK推断出的丢包被认为与网络拥塞有关,拥塞控制过程(第16章讨论)会与快速重传一起被调用。不使用SACK选项时,在收到有效的ACK前通常只能重传一个报文段,使用SACK选项时,ACK包含的额外信息允许发送方在每个RTT时间填充多个空洞,在演示了基本的快速重传算法后我们再探讨SACK在快速重传中的使用。

14.5.1 例子

在下面的示例中,我们创建一个类似图 14‑5的TCP连接,只是这次我们丢弃报文段23801[h4] 和报文段26601[h5] 并禁用SACK。我们看下TCP如何使用基本的快速重传算法修复这些空洞,发送方系统为Linux 2.6,接收方系统为FreeBSD 5.4,图 14‑6由Wireshark的Statistics | TCP Stream Graph | TimeSequence Graph (tcptrace)菜单得到,显示了快速重传的行为。

tcpip必须知道的十个问题(TCPIP详解第14章超时与重传)(6)

图 14‑6 图中x轴为时间轴,y轴为TCP序号,发送的段以黑色线段显示,收到的ACK以浅灰色显示。快速重传由0.993秒到达的第三个重复ACK触发。该连接不使用SACK,所以每个RTT最多只能修复一个空洞,第三个重复ACK后的其它ACK使得发送方发送新报文段(不是重传),1.32秒到达的“部分确认”(partial ACK)再次触发了重传。

该图x轴表示时间,y轴表示发送的报文段序号(相对序号),黑色垂直[h6] 的I型线段表示发送报文段的序号范围,Wireshark中棕色的线(图 14‑6下部浅灰色的线)表示返回报文中的ACK号。大约1s时,根据快速重传算法序号为23801的报文段被重传(位于发送方TCP协议层下方的进程丢弃了此报文段,所以最开始传输时没有传输此报文段),重传由第三个到达的重复ACK触发,见图中较低的黑色垂直I型线段,使用Wireshark的基础分析界面也可以看到重传(见图 14‑7)。

tcpip必须知道的十个问题(TCPIP详解第14章超时与重传)(7)

图 14‑7 以相对序号显示的TCP数据交换。包50及包66为重传包,包50是由三个重复ACK触发快速重传算法而重传的。因为不需要重传定时器,所以恢复相对较快。

图 14‑7第1行(40号包)为第一次收到ACK23801。Wireshark会高亮(黑背景红色字,图 14‑7看起来是黑背景白色字)显示“需要关注”的包,这些高亮的包与TCP期望接收到的正常包有些不同,我们关注其中的窗口更新、重复ACK和重传包。0.853s的窗口更新包是一个具有重复序号的ACK包(因为未携带数据)并改变了TCP流量控制窗口,窗口大小从231616字节变为了233016字节,因此它并不算触发快速重传所需三次重复ACK中的其中一次,窗口更新仅仅只是通知窗口大小已改变而已,我们在第15章再详细讨论这些内容。

第0.890s、0.926s和0.964s到达的包都是序号23801的重复ACK,这些重复ACK中的第三个重复ACK到达时触发了0.993s的报文段23801的重传,通过Wireshark的Statistics | Flow Graph功能也可以看出来(见图 14‑8)。

tcpip必须知道的十个问题(TCPIP详解第14章超时与重传)(8)

图 14‑8 在0.890s、0.926s和0.964s收到重复ACK后,在0.993秒由快速重传算法触发重传,0.853s的ACK因为包含窗口更新不被认为是重复ACK

这里我们用一种稍微不同的方式看到了0.993s的快速重传,第二次重传由于1.322s到达的ACK触发的。

第二次重传与第一次有些不同,第一次重传时,TCP发送方重传前最大序号为(43401 1400=44801),这个最大序号被称为恢复点(recovery point),重传后TCP收到的ACK只有与恢复点相等或超过恢复点才认为从丢包中恢复过来。在本例中,1.321s和1.322s的ACK不是44801而是26601,这个值比之前看到的最大ACK(23801)大,但不等于或超过恢复点(44801),因此这种ACK被称为部分确认(partial ACK)。当收到部分确认(partial ACK)时,TCP立即发送可能丢失的段(本例中为26601),并持续这种方式直到收到的ACK等于或大于恢复点,如果拥塞控制过程允许(见第16章),它也可以发送未发送过的新数据。

这个例子展示了TCP在未使用SACK选项、基于“NewReno”发送算法[RFC3782]进行恢复的快速重传过程。因为没有使用SACK,每个RTT时间发送方通过返回包中逐渐增加的ACK最多只能知道接收方一个空洞,一次重传只能填充一个接收方最低序号的空洞。

恢复过程的具体行为取决于发送方和接收方的类型及配置,本例展示了一个不支持SACK、使用NewReno算法的发送方常见的做法。根据NewReno算法,部分确认(partial ACK)使发送方处于如上所述的恢复阶段,对于旧的TCP变体(普通Reno算法)没有这样的概念,任何有效的ACK都能结束恢复阶段。这样做可能会给TCP带来一些性能问题,这些问题在第16章中详细讨论。我们下面要讨论的NewReno和SACK有时被称为“高级丢包恢复”技术,以区别于旧的方法。

14.6 使用SACK的重传

随着[RFC2018]中SACK选项的标准化,支持SACK的TCP接收方能够描述收到的这样的数据(序号超出返回给发送方的ACK号的数据)。正如前面提到的,ACK号与接收方缓存中窗口内其它数据间的空隙称为空洞,序号超过空洞的数据被称为乱序数据,因为这些数据的序号与之前已接收到的数据序号不连续。

TCP发送方的任务是重传丢失的数据来填充接收方的空洞,同时也要尽量不重传接收方已有的数据。在大部分情况下,支持SACK的发送方比不支持SACK的发送方能用更少的重传更快的填充空洞,因为它不需要再等一个RTT就能知道其它的空洞。当使用SACK选项时,ACK可以包含3个或4个SACK块来说明接收方乱序数据的信息,每个SACK块包含2个32位的序号来表示接收方每个乱序数据块的第一个和最后一个序号(加1)。

指定了n个块的SACK选项所用字节为8n 2,所以用来保存TCP选项的40个字节最多可以指定4个块。SACK选项通常与占10个字节的时间戳选项(外加2字节填充,共12字节)一起使用,也就是说每个ACK通常只能包含3个SACK块。

三个不同的块可以向发送方报告三个空洞,如果不受拥塞控制的限制(见第16章),使用支持SACK的发送方在一个RTT内填充三个空洞。包含一个或多个SACK块的ACK有时也简单地称为“SACK”。

14.6.1 SACK接收方行为

支持SACK的接收方在TCP连接建立期间(见第13章)收到允许SACK(SACK-Permitted)选项才允许生成SACK。一般来说,只要接收方缓冲区中有任何乱序数据就会生成SACK,发生这种情况的原因可能是数据传输时丢失了,或者数据重排序了,或者新数据比旧数据先到达了接收方。我们先考虑第一种情况,稍后再讨论第二种情况。

接收方将最近接收到的段的序号范围放在第一个SACK块中,因为SACK选项空间有限,所以如果可能的话最好确保总是向TCP发送方提供最新的信息,其它SACK块按照它们以前在SACK选项中作为第一个块时的顺序列出,也就是说,它会通过重复最近发送的SACK块(在其它报文段中)来填充,且正在构造的选项的SACK块不是另一个块的子集[h7] 。

在SACK选项中包含多个SACK块并在多个SACK中重复这些块的原因是为SACK丢失提供一些冗余。[RFC2018]指出如果SACK不会丢失,每个SACK只需要一个SACK块即可实现SACK的全部功能。不幸的是,SACK和常规的ACK有时会丢失,他们只有包含数据(或者是SYN或FIN控制位打开)时TCP才会重传它们。

14.6.2 SACK发送方行为

TCP连接想从SACK中受益,除了支持SACK的接收方为了充分利用SACK生成合适的SACK信息还不够,还必须要有支持SACK的发送方,它能够对SACK进行适当的处理,并能通过只发送接收方丢失的段进行选择性重传(selective retransmission),这个过程也被称为选择性重传(selective repeat)。支持SACK的发送方跟踪收到的累积确认信息(和普通TCP发送方一样)及收到的SACK信息,它使用收到的SACK信息(接收方在ACK中生成)来避免重传接收方已有的数据。有种方法可以做到这一点,当发送方收到的SACK中序号范围与重传缓冲区报文段对应时,将对应报文段的“已确认”标志置为1。

支持SACK的发送方有机会执行重传时,通常是因为它收到了一个SACK或已收到了多个重复ACK,它可以选择是发送新数据或重传旧数据。SACK信息提供了接收方当前的序号范围信息,所以发送方可以推断出需要重传哪些报文段来填充接收方空洞。最简单的方法是让发送方先填充接收方空洞,如果拥塞控制过程允许的话再发送新数据[RFC3517],这是最常见的方法。

这种行为有一种例外,SACK选项的当前规范[RFC2018]中,SACK块只是咨询的(advisory),这意味着接收方可以向发送方提供SACK表明它已经成功接收了某些序号的数据,然后又更改主意(“食言”)。因此,发送方在只收到了SACK的情况下不能释放重传缓冲区,只有接收方的普通ACK号大于缓冲数据的最大序号时,发送方才能释放对应的数据块。该规则还会影响当重传定时器超时时TCP应该做什么,当发送方发起基于定时器的重传时,根据SACK推断出的接收方的乱序数据信息会被忽略,如果接收方仍有乱序数据,那么重传报文段的ACK应该包含可供发送端使用的SACK块信息。幸运的是,食言情况比较罕见也不鼓励使用。

14.6.3 例子

为了理解使用SACK如何改变发送方和接收方的行为,我们使用相同的设置重复前面的快速重传实验(丢弃序号23801和26601),但这一次发送方和接收方都使用SACK。为了直观地看到发生了什么,我们再次使用Wireshark的TCP sequence number (tcptrace)图表功能(见图 14‑9)。

tcpip必须知道的十个问题(TCPIP详解第14章超时与重传)(9)

图 14‑9第1个包含SACK信息的重复ACK触发了快速重传,下一个到达的ACK让发送方知道第二个丢失的报文段并在同一个RTT内重传了该报文段

图 14‑9与图 14‑6类似,但是发送方在重传完段23801后不需要等待一个RTT再重传丢失的报文段26601,这是收到的ACK包含SACK信息的结果,我们稍后再讨论这些细节,我们现在先查看下连接建立期间SACK-Permitted选项协商情况,如图 14‑10所示。

tcpip必须知道的十个问题(TCPIP详解第14章超时与重传)(10)

图 14‑10 SYN报文段中交换的SACK-Permitted选项表明可以生成和处理SACK信息。大多数现代TCP在连接建立期间支持MSS、时间戳、窗口缩放和SACK-Permitted选项。

如预期的那样,接收方使用SACK-Permitted选项表明它能够使用SACK,发送方的SYN报文段(第一个包)也包含了相同的选项。这些选项只在连接建立时出现,因此它们仅会出现在SYN报文段中。

一旦连接允许使用SACK,丢包通常就会导致接收端生成SACK。例如,当单击第一个SACK时,Wireshark会显示SACK选项的内容(见图 14‑11)。

tcpip必须知道的十个问题(TCPIP详解第14章超时与重传)(11)

图 14‑11 第一个ACK包含的SACK信息表明乱序块的序号范围为25201到26601

收到第一个SACK后的一系列事件如图 14‑11所示,Wireshark通过SACK的left edge和right edge来标识SACK信息,我们看到ACK 23801包含了一个SACK块[25201,26601],这表明接收方有一个空洞,接收方缺失的序号范围为[23801,25200],对应于从序号23801开始的1400字节的包。注意,和前面所说的原因一样,这个SACK是一个窗口更新而不会算作重复ACK,所以它不会触发快速重传。

0.967s到达的SACK包含了2个SACK块:[28001,29401]和[25201,26601],回想一下,前一个SACK的第1个SACK块会在后面的SACK中稍后的位置重复出现,提高对ACK丢失时的鲁棒性。这个SACK是对序号23801重复ACK,它说明了接收方现在需要两个分别以序号23801和序号26601开始的全长报文段,发送方立即响应发起快速重传,但由于拥塞控制过程(见第16章),发送方只重传了报文段23801,随着另外两个ACK的到达,发送方被允许发送第二个重传报文段26601。

发送方使用了NewReno算法引入的恢复点的思想。在本例中,重传前发送方发送的最大序号是43400,比图 14‑7中的NewReno示例要小。对于SACK快速重传的这种实现,不需要三个重复ACK,TCP会较早的发起重传,不过退出恢复过程是一样的,在1.3953s一收到ACK 43401,恢复就完成了。

有意思的是,使用SACK控制发送方并不能保证一定能提高总体吞吐量,通过我们所看到的两个例子可以说明这一事实。NewReno(未使用SACK)完成131074字节传输需要3.592s,使用SACK时发送方使用了3.674s。尽管条件大体相似,然而这两个测量结果并没有直接的可比性,因为它们所面对的网络条件并不完全相同(这不是模拟,而是实际测试)。当RTT较大且丢包严重时,SACK的好处更为明显,在这种情况下,每个RTT时间可以填充多个空洞可能更明显。

14.7 伪超时与重传

许多情况下,即使没有数据丢失,TCP也可能发起重传,这种不必要的重传称为伪重传,是由伪超时(过早触发超时)和其它原因造成的,诸如包重排序、包复制或ACK丢失等。当实际RTT突然显著增加并超过RTO时就会出现伪超时,在底层协议的性能变化较大的环境中(如无线),这种情况更常见,[KP87]中也提到了这个问题。这里我们主要关注伪超时引起的伪重传,TCP重排序和包复制的影响下一节再讨论。

已经提出了很多方法来处理伪超时,这些方法通常包含检测算法及处理算法。检测算法用于检测超时或基于定时器的超时是否是伪超时,一旦认定超时或基于定时器的超时是伪超时就会调用处理算法,用于撤销或减缓重传定时器超时时TCP执行的一些动作。本章我们只讨论报文段重传的行为。处理算法通常也涉及拥塞控制,这些将在第16章讨论。

tcpip必须知道的十个问题(TCPIP详解第14章超时与重传)(12)

图 14‑12 数据包8传输之后出现了延迟尖峰,导致了伪重传超时及数据包5的重传,重传后,收到了首次传输的数据包5的ACK,数据包5的重传使得接收方收到了一个重复包,紧接着出现了不期望的“回退N”行为,即数据包6、7和8尽管在接收方已存在还是被重传了

图 14‑12展示了一个高度简化的交换过程,数据包8发送后ACK路径上由于延迟尖峰出现伪重传,由于超时数据包5重传后,最先传输的数据包5到8的ACK仍为在途状态。为了简单起见,本例中的序号和ACK号指的是数据包,并且ACK表示已收到的包而不是期望的下一个包。当5-8的确认陆续到达时,TCP已经开始重传其它接收方已收到的数据包了,这导致TCP出现了不期望的“回退N”行为,进而导致接收方生成许多重复ACK并返回给发送方,这些重复ACK可能会触发快速重传。已经提出了几种技术来缓解这些问题,现在我们来看一些比较流行的方法。

14.7.1 重复SACK (DSACK)扩展

对于不支持SACK的TCP,ACK只能向发送方告知最大有序报文段。若使用SACK,它还可以告知其它报文(乱序)段。我们前面讨论基本的SACK机制时并没有说明当接收者接收到重复数据段时会发生什么,这些重复报文段可能是由于伪重传、网络内的复制或其它原因造成的。

DSACK或D-SACK(代表duplicate SACK [RFC2883])是一个规则,应用于SACK接收方,并与传统的SACK发送方互操作,它使用第一个SACK块指示已经到达接收方的重复段的序列号。DSACK的主要目的是确定何时不需要重传,并了解有关网络的其它事实。有了它,发送方至少有可能推断出是否发生了包重排序、ack丢失、包复制和/或虚假的重传输。

DSACK的实现与传统SACK兼容,因为使用它不需要单独的协商。为使其正常工作,需要对接收方发送的SACK内容做些变化,并且发送方的逻辑也要进行相应的变化。如果一个非DSACK TCP与一个DSACK TCP共享一个连接,它们可以互操作,但无法获得DSACK带来的任何好处。

SACK接收方的变化是允许包含确认号小于(或等于)累积确认号的SACK块,虽然这不是SACK的初衷,但它的功能与我们的目的非常匹配。(它同样适用于DSACK大于累积确认号的情况,这种情况发生在重复的乱序报文段)。DSACK信息只出现在ACK中,这样的ACK称为DSACK。与传统SACK信息不同,DSACK信息不会在多个SACK之间重复,因此,DSACK对ACK丢失的鲁棒性不如常规的SACK。

[RFC2883]并没有规定发送方收到DSACK时应该如何处理,[RFC3708]给出了一种利用DSACK检测伪重传的实验算法,但没有提供任何响应算法,文中提到一种选择是使用Eifel响应算法。介绍一些其它检测算法之后,我们将在第14.7.4节研究该算法。

14.7.2 Eifel检测算法

在本章的开头,我们讨论了重传二义性问题,实验性的Eifel检测算法[RFC3522]利用TCP时间戳选项检测伪重传来解决这一问题。发生重传超时后,Eifel算法等待下一个可接受的ACK,如果下一个可接受的ACK表明此ACK是由重传数据包的第一个副本(即原始传输)产生的,则认为该重传是伪重传。

Eifel检测算法比仅采用DSACK能更早检测到伪重传行为,因为它依赖于数据包到达后产生的ACK,这在启动丢失恢复之前。相反,DSACK只有在重复报文段到达接收方后才能发送,且在DSACK返回给发送方后才能起作用。早些检测到伪重传较有利,因为它能使发送方避免大部分的“回退N”行为。

Eifel检测算法的机制很简单,它需要使用TCP时间戳选项。当发送一个重传(基于计时器的重传或快速重传)后,保存其TSV值,当收到第一个能覆盖重传报文段序号的ACK后,检查此ACK的TSER值,如果比保存的TSV值小,则此ACK对应的是原始数据包而不是对应的重传数据包,这意味着重传为伪重传。这种方法对于ACK丢失也有很好的鲁棒性,如果ACK丢失,任何后续ACK的TSER值仍然小于保存的重传段的TSV值。因此窗口内任一ACK的到达就可以判断出重传是伪重传,因此单个ACK的丢失不会有太大问题。

Eifel检测算法可以与DSACK相结合,在整个窗口的ACK都丢失但原始传输和重传都到达了接收方时仍然有用。在这种特殊情况下,到达的重传导致生成一个DSACK,Eifel检测算法默认认为重传是伪重传。然而,有人认为,如果丢失了这么多的ACK,允许TCP相信重传不是伪重传是有好处的(例如,让它开始慢发送——这是我们将在第16章讨论的拥塞控制过程的后果),因此,到达的DSACK会导致Eifel检测算法得出相应的重传不是伪重传的结论。

14.7.3 前移RTO恢复(F-RTO)

前移RTO恢复(F-RTO)[RFC5682]是检测伪重传的标准算法。它不需要任何TCP选项,发送方实现此算法后,即使接收方不支持TCP时间戳选项也能有效地工作。该算法只检测由重传计时器超时引发的伪重传,它不处理前面提到的其它原因导致的伪重传。

F-RTO通常在基于计时器的重传后对TCP行为进行修改。这些重传针对的是还未收到ACK的最小序号的数据包。通常,TCP会在其它ACK到达时继续发送相邻的数据包,这就是前面描述的“回退N”行为。

F-RTO会改变TCP平常的行为,在超时重传后第一个ACK到达时,让TCP发送新数据(到目前为止尚未发送过),然后检查第二个到达的ACK,若重传后先到达的这两个ACK中只要有一个是重复确认,则认为重传是正常的;如果这两个ACK都不是重复确认且使发送方的窗口向前移动了,则认为重传是伪重传。这种方法相当直观,如果新数据的传输导致可接受的ACK到达,说明新数据的到达使接收方的窗口向前移动了;如果新数据导致了重复确认,则说明接收方有一个或多个“空洞”,不论是哪种情况,接收方接收新数据不会影响整体数据传输性能(假设接收方有足够的缓存)。

14.7.4 Eifel响应算法

Eifel响应算法[RFC4015]是一组标准的操作,一旦重传被认定为伪重传时TCP就会执行这些操作。因为响应算法与Eifel检测算法逻辑上是解耦的,所以它可以与我们刚才讨论的任何检测算法一起使用。Eifel响应算法原计划用于基于计时器和快速重传的伪重传,但目前仅用于基于计时器的重传。

尽管Eifel响应算法可结合任意检测算法使用,但其行为根据较早的(如通过Eifel或F-RTO检测算法)还是较晚的(如通过DSACK)检测出伪超时而有所不同,前一种情况称为伪超时,通过检查原始传输的ACK来实现;后一种情况称为迟伪超时(late spurious timeout),基于(伪)超时引起的重传所返回的ACK来实现。

响应算法仅处理第一个重传计时器事件,在恢复完成前如果又发生超时则不会处理。重传计时器超时后,它会获取srtt和rttvar变量值的快照,并将其记录在新变量srtt_prev和rttvar_prev中,如下所示:

这些变量在任一计时器超时时分配,但仅在超时被判定为伪超时使用,用来设置新的RTO。公式中的G代表TCP时钟粒度,基于以下推理,srtt_prev被设置为srtt加上两倍的时钟粒度:伪超时可能是由于srtt的值小了一点点而引起的,如果稍微再大一点就可能不会超时,将srtt_prev略微增加一些(加上2(G))就是处理这种情况,稍后使用srtt_prev设置RTO。

保存srtt_prev和rttvar_prev值后,调用一种检测算法,运行该算法会输出一个值并赋给名为SpusiousRecovery的特殊变量。如果算法检测到伪超时,将SpurousRecovery设置为SPUR_TO;如果它检测到迟伪超时,将SpurousRecovery设置为LATE_SPUR_TO;否则,超时不是伪超时,继续正常的TCP超时处理。

如果SpuriousRecovery值为SPUR_TO, TCP可以在恢复完成之前采取动作,它通过调整将要发送的下一个报文段(SND.NXT)的序列号来实现这一点,改为发送最新的未发送过的报文段(SND.MAX),这避免了前面讨论的初始重传后不希望的“回退N”行为。如果检测算法检测出是迟伪超时,此时对初始重传已经确认过了,所以SND.NXT不变。无论是上述所说哪种情况,拥塞控制状态都会被重置(参见第16章),另外,重传定时器超时后一旦收到一个可接受的ACK,srtt、rttvar、RTO的值作如下更新:

其中,m是连接的RTT样本,此样本基于超时后发送的数据的第一个可接受的ACK计算得到。对标准TCP行为做这样修改的目的是,真实的RTT可能已经发生了重大变化,以至于当前估计器中的RTT历史已不再适合用于设置RTO,如果真实路径上RTT突然增加(例如由于无线切换到一个新的基站),当前的srtt和rttvar值有可能太小,所以应该重新初始化;另一方面,如果路径RTT的增加只是暂时的,这时如果重新初始化srtt和rttvar可能不太合适,因为原有值可能更接近正确值。

只有当新的RTT样本比较大时,这些等式通过对srtt和rttvar的移动平均重新赋值以尽量平衡这两种情况,这样做有效地排除了RTT以前的历史(和RTT方差)。srtt和rttvar的值只能由响应算法调大,若RTT没有增加,则运行估计器保持不变——本质上是忽略了发生超时的事实。无论哪种情况,TCP都按原来的方式重新给RTO赋值,并用此超时值设置一个新的重传计时器。

14.8 包重排序与包重复

到目前为止讨论的大多数问题都与TCP如何处理包丢失有关,这是一个相当常见的问题,针对丢包人们已经做了大量的工作来使TCP更加健壮。正如我们在上一节中所看到的,其它数据包传输问题(如重复、重排序)也会影响TCP的操作,在这些情况下,我们希望TCP能够区分出包重排序了还是包重复了或者是丢失了,正如我们将要看到的,这有时并不那么简单。

14.8.1 重排序

在IP网络中可能会发生包重排序,因为IP在发送期间不保证保持包之间的相对排序,这是有好处的(至少对IP来说),因为IP可以为流量选择另一条链路(例如,更快的链路),而不用担心新注入网络的流量比之前的流量先到达,导致接收端到达数据包的顺序与发送端发送数据包的顺序不一样。包重新排序可能还有其他原因,例如,一些高性能路由器在内部[BPS99]采用多个并行数据链路,不同数据包之间的处理延迟可能导致包离开顺序与到达顺序不一样。

TCP连接的正向路径或反向路径都可能会发生重排序(或在某些情况下两者都发生)。数据段的重排序对TCP的影响与ACK的重新排序略有不同,由于路由不对称,ACK在网络链路中的传输路径(和通过不同的路由器)与正向路径上的数据包传输路径通常是不同的。

流量出现重排序时,TCP可能会在某些方面受到影响。如果重排序发生在反(ACK)方向,发送方TCP会接收到一些使窗口显著向前移动的ACK,然后是一些明显冗余并应该丢掉的ACK,这可能导致TCP的发送模式出现不期望的突发(瞬时高速发送)行为,在利用可用的网络带宽方面也会造成麻烦(由于TCP的拥塞控制(参见第16章))。

如果在发送方向正方向发生重排序,接收方TCP可能很难区分这出是重排序还是丢包,两者都会导致接收方收到乱序的包,导致接收方形成“空洞”。轻微的重排序(例如,两个相邻的包交换了顺序)可以迅速地被处理。当重排序较严重时,即使数据并没有丢失TCP也可能认为数据已经丢失,这可能导致伪重传(伪重传主要来自于快速重传算法)。

回想一下之前的讨论,快速重传算法根据收到的接收方的重复确认推断出丢包,进而在不用等待重传计时器超时的情况下启动重传。因为TCP接收方会对其收到任何乱序数据立即确认以帮助丢包时触发快速重传,所以网络中任何重排序的包都会导致接收端产生重复ACK。如果发送方收到任何重复ACK就立即发起快速重传,那么网络上(有少量的包重排序很常见)就会有大量不必要的重传,为解决这一问题,快速重传只有在重复ACK个数达到一定的阈值(dupthresh)后被触发。

tcpip必须知道的十个问题(TCPIP详解第14章超时与重传)(13)

图 14‑13 通过忽略少量的重复ACK可以解决轻微的重排序(左)。当重排序较严重时(右),本例中包4传输用了传输3个包的时间,触发了快速重传。

如图14-13所示,左边的图显示了轻微重排序时TCP的行为,其中dupthresh的值设置为3。在这种情况下,单个重复的ACK不会对TCP有影响,实际上此重复ACK会被忽略,TCP解决了重新排序。右边的图显示了一个数据包重排序较严重时的情况,因为错了三个位置,所以生成了三个重复的ACK,触发了发送方的快速重传,接收方将收到一个重复的段(段4)。

区分丢包还是重排序比较棘手,解决这个问题涉及接收方应该等待足够长的时间还是应该填补接收方出现的“空洞”。幸运的是,Internet上严重的重排序并不常见[J03],因此将dupthresh设置为一个相对较小的数字(例如默认的3)可以处理大多数情况。即便如此,还是有很多研究调整TCP行为以处理严重重排序[LLY07],包括像Linux TCP实现一样动态地调整dupthresh。

14.8.2 包重复

尽管很少,但IP协议也可能会将一个数据包发送多次。例如,当链路层网络协议执行重传并创建同一数据包的两个副本时,就会发生这种情况。创建副本后,在某些方面TCP可能会变得混乱。考虑图14-14中的情况,3号报文重复出现了3次。

tcpip必须知道的十个问题(TCPIP详解第14章超时与重传)(14)

图 14‑14 网络中的包重复导致重复确认,引发了伪快速重传。

如我们所见,报文3被复制的影响是接收方生成一系列重复确认,这足以触发伪快速重传,因为不支持SACK的发送方会错误地认为报文5和6早已到达。发送方使用SACK(特别是DSACK)很容易诊断出这一点。采用DSACK,A3的每个重复确认都包含了DSACK信息——报文3已经收到的。而且每个重复确认都不包含任何乱序数据的信息,这意味着到达的数据包(或它们的ACK)一定是重复的,在这种情况下,TCP通常可以防止伪重传。

14.9 目的地度量值

正如我们所看到的,随着时间的推移TCP “知悉”了发送方和接收方之间网络路径特征,结果保存在发送方的状态变量中,如srtt和rttvar。一些TCP实现还记录路径上最近发生的包重排序的估计,但一旦连接关闭,这些结果就会丢失。也就是说,和同一个接收方重新建立一个新的TCP连接后必须从头开始确定状态变量的值。

较新的TCP实现在路由表项(或转发表项)或其它系统级的数据结构中维护了我们在本章中描述的许多度量值,即使在TCP连接关闭后仍然存在。当创建一个新连接时,TCP会查看这些数据结构,看看是否有与之通信的目标主机的路径信息。如果有,可以根据以前的时间相对比较近的值将srtt、rttvar等度量值初始化为某个值。当TCP连接关闭时,可以替换或更新这些统计信息。在Linux 2.6中,这些值被更新为TCP最新测量的现有值的最大值,可以使用iproute2工具套件[IPR2]中的ip程序检查这些值:

Linux% ip route show cache 132.239.50.184

132.239.50.184 from 10.0.0.9 tos 0x10 via 10.0.0.1 dev eth0

cache mtu 1500 rtt 29ms rttvar 29ms cwnd 2 advmss 1460 hoplimit 64

这个命令输出了以前连接缓存的信息,在本地系统和132.239.50.184之间使用IPv4并使用了特定的DSCP值(16,表示CS2,但使用较旧的“ToS”字节术语表示,值为0x10),下一跳10.0.0.1,使用网络设备eth0访问。我们可以看到包大小信息(通过PMTUD得到的路径MTU、接收方通告的MSS)、使用的最大跳数(针对IPv6,这里不适用)、srtt和rttvar的值,以及拥塞控制信息(例如我们将在第十六章讨论的cwnd)。

14.10 重新分组

TCP超时重传时,它不必重传完全相同的段,相反,允许TCP重新分组——发送一个更大的段提高性能(当然,这个更大的段不能超过接收方通告的MSS也不能超过路径MTU)。这在TCP协议中是允许的,因为TCP通过字节号(而不是段或包号)标识发送和确认的数据。

TCP能够重传与原报文段大小不同的段为解决重传二义性提供了另外一种方法,这是STODER [TZZ05]思想的基础,该思想使用重新分组来检测伪超时。

我们可以很容易地看到重新分组的行为。使用sock程序作为服务器,并通过Telnet连接到服务器。我们先输入一行hello there,这会生成一个13个数据字节的报文段(包括按下Enter键时产生的回车和换行符),断开网络并输入line number 2(15个字节,包括换行符),然后等待45秒输入and 3并终止连接:

Linux% telnet 169.229.62.97 6666

hello there 第一行发送没问题后,拔掉网线

line number 2 这一行会重传

and 3 输入后重新连上网

^] telnet> quit

通过tcpdump查看结果:

1

19:51:47.674418 IP 10.0.0.7.1029 > 169.229.62.97.6666:

P 1:14(13)[h8] ack 1 win 5840

<nop,nop,timestamp 2343578137 596377728>

2

19:51:47.788992 IP 169.229.62.97.6666 > 10.0.0.7.1029:

. ack 14 win 58254 <nop,nop,timestamp 596378252 2343578137>

3

19:52:35.130837 IP 10.0.0.7.1029 > 169.229.62.97.6666:

FP 29:36(7)[h9] ack 1 win 5840

<nop,nop,timestamp 2343602439 596378252>

4

19:52:35.146358 IP 169.229.62.97.6666 > 10.0.0.7.1029:

. ack 14 win 58254

<nop,nop,timestamp 596382987 2343578137,nop,nop,

sack sack 1 {29:36}>

5

19:52:39.414253 IP 10.0.0.7.1029 > 169.229.62.97.6666:

FP 14:36(22)[h10] ack 1 win 5840

<nop,nop,timestamp 2343604633 596382987>

6

19:52:39.429228 IP 169.229.62.97.6666 > 10.0.0.7.1029:

. ack 37 win 58248 <nop,nop,timestamp 596383416 2343604633>

7

19:52:39.429696 IP 169.229.62.97.6666 > 10.0.0.7.1029:

F 1:1(0) ack 37 win 58254

<nop,nop,timestamp 596383416 2343604633>

8

19:52:39.430119 IP 10.0.0.7.1029 > 169.229.62.97.6666:

. ack 2 win 5840 <nop,nop,timestamp 2343604641 596383416>

追踪结果中删除了初始的SYN交换,前两个报文段为数据字符串hello there和它的确认。追踪结果中的下一个包没有按顺序来:它以序号29开始并包含字符串and 3(7个字节),此包的确认包ack为14,并包含一个相对序号为{29,36}的SACK块,中间序号的字符已经丢失。TCP重传了这些字符,但使用了一个更大的包,此包序列为14:36。从此过程可以看到序号14包重传如何重新分组形成一个更大的包(大小为22字节)。有趣的是,这个包重复了SACK块中的数据,FIN位字段也已被置位,表示这是连接的最后一个数据。

14.11 与TCP重传相关的攻击

有一类DoS攻击被称为低速率DoS攻击[KK03]。在这种攻击中,攻击者向网关或主机发送大量流量,导致被攻击系统重传超时。若攻击者能够预测TCP何时会尝试重传,那么在TCP每次重传尝试时发送大量流量。因此,被攻击的TCP认为网络拥塞,根据Karn算法TCP不断退避其RTO,限制其发送速率直到接近于零,并且只能接收到很少的有效的网络流量。解决这种类型攻击的方案是添加RTO的随机性,使攻击者难以预测重传发生的准确时间。

一种与DoS攻击相关但不同的攻击是放慢被攻击者报文段的发送,从而造成过高估计RTT,这样做会使被攻击TCP在数据包丢失时不那么积极地进行重传。与此相反的攻击也是可能的:当数据已经发送但实际上还未到达接收方时,攻击者伪造ACK进行确认,在这种情况下,被攻击TCP认为连接RTT比实际情况小得多,导致TCP过分积极生成大量不必要的重传。

14.12 总结

本章详细介绍了TCP的超时和重传策略。第一个示例展示了当TCP有数据包要发送时拔掉网线导致重传计时器发起基于超时的重传的情况,每次重传等待的时间间隔是前一次的两倍,这是Karn的算法第二部分二进制指数退避算法的结果。

TCP测量RTT,使用这些测量值跟踪平滑RTT估计和平均差估计,并使用这两个估计计算新的重传超时值。如果没有时间戳选项,TCP在每个数据窗口只能测量一个RTT。Karn的算法通过不使用已丢失报文段的RTT测量值来消除重传二义性问题。今天,大多数TCP使用时间戳选项,它允许对每个报文段分别计时,即使是在包重排序或包重复的情况下时间戳选项也能正常工作。

我们研究了快速重传算法,它可以在计时器未超时的情况下被触发。对于TCP来说,这是填补接收方空洞(由于丢包造成)的最有效的方法(也是最常用的方法)。使用SACK可以改善快速重传,这些ACK中携带的额外信息允许支持SACK的TCP发送方在每个RTT时间内填补多个空洞,这样做在某些情况下可以提高性能。

如果RTT的估计值低于连接的实际值,就可能发生伪重传。在这种情况下,如果TCP等待的时间稍微长一些,就不会发生(不必要的)重传。已经提出了很多算法来检测TCP的伪超时,DSACK方法要求重复的报文段到达接收方才可以使用。Eifel检测算法依赖于TCP时间戳,但比DSACK响应更快,因为它根据超时前发送的报文段返回的ACK来检测伪超时。F-RTO是另一种与Eifel类似的算法,但它不需要时间戳,它使得发送方检测出伪超时后发送新数据。所有检测算法都可以与响应算法相结合,Eifel响应算法是到目前为止描述的一个主要算法,如果延迟大幅增加,它可以重新设置RTT和RTT方差估计(或者“撤销”TCP将在超时时执行的任何更改)。

我们还研究了如何缓存不同连接间TCP状态、TCP如何对数据重新分组、以及一些欺骗TCP使其行为出现过分被动或过分积极的攻击,在第16章(我们将研究拥塞控制过程)将看到更多关于这些攻击造成的影响。


[h1]第13.2.5节

[h2]srtt为均值估计值,rttvar为平均差估计值

[h3]SYN交换后的包,第3个和第4个包

[h4][23801,25201),1400字节

[h5][26601,28001) ,1400字节

[h6]Wireshark中为天蓝色

[h7]如第一个报文段中发送SACK块为[a,b],其中[a,b]为最近收到的报文段序号范围。第二个报文段中发送SACK块为[c,d]、[a,b],其中[c,d] 为最近收到的报文段序号范围。第三个报文段中发送SACK块为[e,f]、[c,d]、[a,b],其中[e,f] 为最近收到的报文段序号范围。

[h8]hello there\r\n

[h9]and 3\r\n

[h10]line number 2\r\nand 3\r\n

,