一、零窗口与TCP持续计时器
- 我们了解到,TCP是通过接收端的通告窗口来实现流量控制的。通告窗口指示了接收端可接收的数据量。当窗口值变为0时,可以有效阻止发送端继续发送,直到窗口大小恢复为非零值。当接收端重新获得可用空间时,会给发送端传输一个“窗口更新”,告知其可继续发送数据。这样的窗口更新通常都不包含数据(为“纯ACK”),不能保证其传输的可靠性,因此TCP必须有相应措施能处理这类丢包
窗口更新丢失导致的死锁
- 如果一个包含窗口更新的ACK丢失,通信双方就会一直处于等待状态:接收方等待接收数据(已将窗口设为非零值),发送方等待收到窗口更新告知其可继续发送。
- 为防止这种死锁的发生,发送端会采用一个“持续计时器”间歇性地查询接收端,看其窗口是否已增长。持续 计时器会触发“窗口探测”的传输,强制要求接收端返回ACK(其中包含了窗口大小字段)
- 主机需求RFC建议在一个RTO之后发送第一个窗口探测,随后以指数时间间隔发送(与前面讨论的karn算法中的“第二部分”类似)
窗口探测
- 窗口探测包含一个字节的数据,采用TCP可靠传输(丢失重传),因此可以避免由窗口更新丢失导致的死锁
- 当TCP持续计时器超时,就会触发窗口探测的发送。其中包含的一个字节的数据是否能被接收,取决于接收端的可用缓存空间大小。与TCP重传计时器类似,可以采用指数时间退避来计算持续计时器的超时。而不同之处在于,通常TCP不会停止发送窗口探测,由此可能会放弃执行重传操作。这种情况可能导致某种程度的资源耗尽(见前面“与窗口管理相关的攻击”的文章)
演示案例
- 为了说明TCP的动态窗口调节和流量控制机制,我们建立了一个TCP连接,并使其在处理接收到的数据之前暂停接收。本实验采用了Mac OS X 10.6发送端和Windows 7接收端。在接收端运行带-P选项的sock程序:
- 该命令使得接收端在处理接收到的数据前暂停20s。这样就导致接收端的通告窗口在125号包处开始关闭,如下图所示
- 从下图中可以看到,在接收了100多个包后,窗口大小仍然维持在64KB。这是由于自动窗口调节算法(见后面的“大容量缓存与自动调优”)默认分配了TCP接收端的缓存。然而,随着可用缓存 的减少,可以看到在125号包之后,窗口开始减小。随着大量的ACK到达,窗口进一步减小,每个到达的ACK号都增大2896字节。这表明接收端在存储这些数据,但应用程序并没有处理。如果我们进一步观察,会发现最终接收端已经没有更多空间来存储到达的数据(见下下图)
- 从下图中可以看到,151号包耗尽了327字节大小的窗口,Wireshark显示“TCP窗口满”。约200ms后,在4.979s时刻,零窗口通告产生,表明无法接收新的数据。窗口最后的可用空间已满,接收端应用程序暂停处理数据,直到20.143s时刻
- 收到零窗口通告后,发送端每隔5s共发送了三次窗口探测以查看窗日是否打开。在20s时刻,接收端开始处理TCP队列中的数据。因此有两个窗口更新传送至发送端,表明可以继续传输数据(64kB)。窗口更新并不是对新数据的确认,而只是将窗口右边界右移。这时,发送端可以恢复正常的数据传输
- 从上面两张图可以总结出以下几点:
- 1.发送端不必传输整个窗口大小的数据
- 2.接收到返回的ACK的同时可将窗口右移。这是由于通告窗口是和该报文段中的ACK号相关的
- 3.窗口大小可能减小,如上面第一张图所示,但窗口右边界不会左移,以此避免窗口收缩
- 4.接收端不必等到窗门满才发送ACK
- 此外,还可通过观察吞吐量随时间的变化函数得到一些启发。使用Wireshark的“统计|TCP流图|吞吐量图”功能,可以得到下图所示的时间序列
- 这里我们看到 一个有趣的现象。即使在接收端处理任何数据前,连接依然能达到约1.3MB/s的吞吐量。这种状况一直持续到约0.10s时刻。之后,直到接收端开始处理数据前(在20s时刻后),吞吐量基本上都为0
二、糊涂窗口综合征(SWS)
什么是糊涂窗口综合征
- 基于窗口的流量控制机制,尤其是不使用大小固定的报文段的情况(如TCP),可能会出现称为糊涂窗口综合征(SWS)的缺陷
- 当出现该问题时,交换数据段大小不是全长的而是一些较小的数据段。由于每个报文段中有用数据相对于头部信息的比例较小,因此耗费的资源也更多,相应的传输效率也更低
- TCP连接的两端都可能导致SWS的出现:
- 接收端的通告窗口较小(没有等到窗口变大才通告),或者发送端发送的数据段较小(没有等待将其他数据组合成一个更大的报文段)
- 要避免SWS问题,必须在发送端或接收端实现相关规则。TCP无法提前预知某一端的行为。见下面的接收端/发送端规则
接收端规则:
- 对于接收端来说不应,不应该通告小的窗口值。描述的接收算法中,在窗口可增至一个全长的报文段(即接收端MSS)或者接收端缓存空间的一半(取两者中较小者)之前,不能通告比当前窗口(可能为0)更大的值
- 注意到可能有两种情况会用到改规则:
- 当应用程序处理接收到的数据后使得可用缓存增大
- TCP接收端需要强制返回对窗口探测的相应
发送端规则:
- 对于发送端来说,不应发送小的报文段,而且需要Nagle算法控制何时发送。为避免SWS问题,只有至少满足以下条件之一时才能传输报文段:
- (a)全长(发送MSS字节)的报文段可以发送
- (b)数据段长度>=接收端通告过的最大窗口值的一半的,可以发送
- (c)满足以下任一条件的都可以发送:
- (i)某一ACK不是目前期盼的(即没有未经确认的在传数据)
- (ii)该连接禁用Nagle算法
- 条件(a)最直接地避免了高耗费的报文段传输问题。条件(b)针对通知窗口值较小,可能小于要传输的报文段的情况。条件(c)防止TCP在数据需要被确认以及Nagle算法启用的情况下发送小报文段。若发送端应用在执行某些较小的写操作(如小于报文段大小),条件(c)可以有效避免SWS
- 上述3个条件也让我们回答了以下问题:
- 当有未经确认的在传数据时,若使用Nagle算法阻止发送小的报文段,究竟多小才算小?从条件(a)可以看出,“小”意味着字节数要小于SMSS(即不超过PMTU或接收端MSS的最大包大小)。条件(b)只用于比较旧的原始主机,或者因接收端缓存有限而使用较小通知窗口时
- 条件(b)要求发送端记录接收通告窗口的最大值。发送端以此猜测接收端缓存大小。尽管当前建立时缓存大小可能减小,但实际这种情况很少见。另外,前面也提过,TCP需要避免窗口收缩
演示案例
- 下面通过一个具体的例子来观察SWS避免的行为;本例也包含持久计时器。这里使用我们的sock程序,发送端主机为Windows XP系统,接收端为FreeeBSD,执行三次2048字节的写操作传输。发送端命令如下:
- 接收端相应的螟蛉畏:
- 该命令将接收端缓存设为3000字节,在首次读取数据前有15s的初始延时,之后每次读都会引入2s的延时,买次读的数据量为256字节。设置初始延时是为使接收端缓存占满,最终迫使传输停止。这是通过使接收端执行小的读操作,我们期望看到它执行SWS避免。利用wireshark可以得到下图
- 整个连接的传输内容如下图所示。包长度是根据每个报文段中携带的TCP有效荷载数据描述的。在连接建立过程中,接收端通告窗口为3000字节,MSS为1460字节。发送端在0.052s时刻发送了一个1460字节的包(包4),在0.053s时刻发送了588字节的包(包5)。两者综合为2048字节,为应用写操作的大小。包6是对这两个包的确认,并提供了一个952字节的窗口通告(3000 - 1460 - 588=952)
- 952字节的窗口(包6)并没有一个MSS大,所以Nagle算法阻止了发送端的立即发送。相反,发送端等待了5s,直到持续计时器超时,才发送了一个窗口探测。考虑到无论如何都要发送一个包,因此发送端发送了允许的952字节数据填满了可用窗口,因此包8返回 了零窗口通告
- 下一个事件发生在6.970s时刻,TCP发送了一个窗口探测,即在接收到首个零窗口通告约2s后。探测包本身包含一个字节的数据,图中Wireshark显示为“TCPZeroWindowProbe” ,但对该探测包的ACK号却没有增大(Wireshark将其标记为“ TCP ZeroWindowProbeAck”),因此这一个字节的数据并没有被接收端保存。在10.782s时刻又产生了一个探测包(约4s后),接着18.408s时刻又产生一个(约8s以后),表明其发送间隔随时间呈指数增长。注意到最后 一次窗口探测中包含的一个字节的数据已被接收端确认
- 在25.061s时刻,在上层应用执行了6次256字节的读数据操作后(每次间隔2s),窗口更新表明现在接收端缓存中有1535字节(ACK号加1)的可用空间。根据接收端SWS避 免规则,该数值已“足够大”。发送端开始继续传送数据,在25.064s时刻发送了1460字节的包,在25.161s时刻得到了对4462字节数据的ACK,这时通告窗口大小只有75字节(包17)。该通告似乎违背了我们之前的规则,即窗口值应至少为一个MSS (对FreeBSD来说)或总缓存空间的四分之一。出现这种情况的原因在于避免窗口收缩。最后一个窗日更新中(包15),接收端通告窗口右边界为(3002 + 1535)=45370如果当前ACk (包17)通告的窗口小于75字节,像接收端SWS避免要求的那样,窗口右边界就会左移, TCP是不允许出现 这种情况的。因此这75字节的通告窗口代表一种更高的优先级:避免窗口收缩优先于避免SWS
- 通过包17和包18之间的5s的延时,我们再次看到发送端的SWS避免。发送端被强制要求发送一个75字节的包,接收端返回一个零窗口通告响应。在1s之后的包20是再次的窗口探测,得到了767字节的可用窗口。又一轮的发送端SWS避免导致了5s的延时;发送端填满窗口后,再次返回零窗口通告;这种状况一直重复。最终发送端没有新的数据发送而终止。包30代表发送的最后一个包,在20s后连接终止(由于接收端应用每次读数据的间隔为2s)
- 为了理解上层应用行为、通告窗口和SWS避免之间的关系,我们将连接的动态传输以表格的形式展现出来。下图给出了发送端和接收端的行为,以及接收端应用执行读操作的估计时间
- 在上图中,第1列表示图中出现的每个传输行为的相对时刻,带三位小数的数值是Wireshark显示的时间值(参见下面“三”中演示案例的第二张图片)。而不带小数的则是接收端主机行为的估计时刻,图中并没有显示
- 接收端缓存中的数据(表中标记为“已存数据”)随着新数据的到达而增加,随着上层 一应用的读取而减少。我们想了解的是接收端返回给发送端的窗口通告中包含的内容。这样就能知道接收端是怎样避免SWS的
- 如前所述,第一次SWS避免是包6和包7之间的5s延时,由于窗口大小只有952字 节,发送端一直避免传输直到被强制要求发送数据。传输完成后,接收端缓存满,之后产生 了一系列的零窗口通告和窗口探测交换。我们可以看到持续计时器指示的时间间隔呈指数增长:探测包的发送时刻为6.970s,10.782s和18.408s。这些时刻与发送端首次接收到零窗口通告的时刻5.160s的间隔约为2s、 4s、 8s
- 尽管上层应用在15s和17s时刻读数据,但至18.408s时刻为止只读了512字节。根据 接收端SWS避免规则要求,由于512字节的可用缓存既小于总缓存空间(3000字节)的一 半,也没有达到一个MSS (1460字节),因此不能提供窗口更新。发送端在18.408s时刻发送了一个窗口探测(报文段13)。该探测包被接收,由于缓存有一定的可用空间,因此其中 包含的一个字节数据也被保存,报文段12和14之间的ACR号的增长验证了这一点
- 尽管有511字节的可用空间,但接收端再次实施了SWS避免。接收端FreeBSD在实现SWS避免时区分了何时发送窗口更新与怎样响应窗口探测。它遵循[RFCl122]中的规则, 只在通告窗口至少为总接收缓存的一半(或一个MSS)时才发送窗口更新,并且只有当窗口 至少为一个MSS或超过总接收缓存的四分之一才响应窗口探测。但在这里,511字节小于一 个MSS且不到3000/4 = 750字节,因此接收端只好对报文段13的ACK中包含的通告窗口设为0
- 直到25s时刻为止,上层应用完成了6次读操作,接收端缓存有1535字节空闲(大于总的3000字节的一半),因此发送了一个窗口更新(报文段15)。发送的数据为全长报文段 (报文段16),接收到的ACK中包含的通告窗口仅为75字节。在接下来的5s内,两端都执行SWS避免。发送端需要等待一个更大的通告窗口,上层应用在27s时刻和29s时刻执行 读操作,但只有587字节的空间,不足以发送窗口更新。因此,发送端持续等待了5s并最终发送了剩余的75字节,迫使接收端再次进人SwS避免状态
- 接收端没有提供窗口更新,直到31.548s时刻发送端的持续计时器超时,发送了一个窗 口探测。接收端响应了一个非零窗口,为767字节(大于总接收缓存的四分之一)。该窗口值 对发送端并非足够大,因此继续执行发送端的SWS避免。发送端等待了5s,之后一直重复上述过程。最终,在43.486s时刻,最后的71字节发送完并得到确认。该ACk中包含696字节的窗口通告。尽管小于总接收缓存的四分之一,为了避免窗日收缩,通告窗日并没有设为0
- 从报文段32开始,不再包含数据,连接开始关闭。随即得到的确认中窗口大小为695 字节(接收端的FIN消耗了一个序列号)。在上层应用再次完成6次读操作后,接收端提供 了一个窗口更新,但发送端已经完成所有数据的发送,并保持空闲状态。上层应用又执行了4次读操作,其中3次返回256字节,最后1次没有返回,表明已经无数据到达。此时,接 收端关闭连接并发送FIN。发送端返回了最后一个ACK,双向连接结束
- 由于发送端应用在执行3次2048字节的写操作后开始关闭连接,在发送完报文段32后,发送端从ESTABLISHED状态变为FIN_WAIT状态。接着在接收到报文段33后,进人FIN_WAIT_2状态。尽管在这时接收到了窗日更新,但发送端没有任何动作,因为它已经发送了FIN并经确认(这一阶段没有计时器)。相反,在接收到对方的FIN 前,它只是静静等待。这就是我们没有看到更多的传输直至接收到FIN的原因(报文段35)
三、大容量缓存与自动调优
- 从前面的章节可以看到,在相似的环境下,使用较小接收缓存的TCP应用的吞吐性能更差。即使接收端指定一个足够大的缓存,发送端也可能指定一个很小的缓存,最终导致性能变差。这个问题非常严重,因此很多TCP协议栈中上层应用不能指定接收缓存大小。在多数情况下,上层应用指定的缓存会被忽视,而由操作系统来指定一个较大的固定值或者动态变化的计算值
- 在较新的Windows版本(Vista/7)和Linux中,支持接收窗口自动调优[S98]。有了自动调优,该连接的在传数据值(连接的带宽延时积——一个重要概念,将在后面“TCP拥塞控制”讲解)需要不断被估算,通告窗口值不能小于这个值(假如剩余缓存空间足够)。这种方法使得TCP达到其最大可用吞吐率(受限于网络可用容量),而不必提前在发送端或接收端设置过大的缓存
Windows的设置
- 在Windows系统中,默认自动设置接收端缓存大小
- 然而,也可以通过netsh命令更改默认值:
- 这里X可设置为disabled、 highlyrestricted、 restricted、 normal或experimentalo不同的设置值会影响接收端通告窗口的自动选择。在disabled状态下,禁用自动调优,窗口大小使 用默认值。restricted模式限制窗口增长,normal允许其相对快速增长。而experimental模式允许窗口积极增长,但通常并不推荐normal模式,因为许多因特网站点及某些防火墙会干扰,或没有很好地实现TCP窗口缩放(Window Scale)选项
Linux的设置
- 对于Linux 2.4及以后的版本,支持发送端自动调优。2.6.7及之后的版本,两端都支持该功能。然而,自动调优受制于缓存大小
- 下面的Linux sysctl变量控制发送端和接收端的最大缓存。等号之后的值为默认值(根据不同的Linux版本可能会有不同),如果系统用于高带宽延时积的环境下,上述值需要增大
- 另外,通过下面的变量设定自动调优参数:
- 每个变量包含三个值:自动调优使用的缓存的最小值、默认值和最大值
演示案例
- 为演示接收端自动调优行为,这里采用Windows XP发送端(设为使用大容量窗口和窗口缩放)和Linux2.6.11接收端(支持自动调优)。在发送端运行如下命令:
- 对接收端,我们不对接收缓存做任何设置,但在上层应用读取数据前设置20s的初始延时:
- 为描述接收端通告窗口的增长,可以利用Wireshark来显示包传输情况,并根据接收端地址来进行分类(参见下图)。在连接建立阶段,接收端初始窗口值为1460字节,初始 MSS为1412字节。由于其采用了窗日缩放,会有2倍的变动(图中没有显示),使得最大可用窗口为256KB。可以看到在完成第一个包传输后,窗口有了增长,相应地发送端也提高了发送速率。在“TCP拥塞控制”中我们将会讨论TCP拥塞控制的发送速率。现在,我们只需要知道发送端何时开始发送,通常情况下其首先发送一个包,接着每收到一个ACK,发包数就增加 一个MSS。因此,每接收一个ACK,就会发送两个包(每个包长度为一个MSS)
- 观察窗口通告值10712、 13536、 16360、 19184……可以发现,每接收一个ACK,窗口就增长两个MSS,这与发送端拥塞控制操作相一致(后面“TCP拥塞控制”讨论)。假设接收端存储空间足够大,根据拥塞控制局限性,通告窗口总是大于允许发送的数据量。这种方式是最优的——在保持发送端最大发送速率的情况下,接收端通告和使用的缓存空间最小
- 当接收端缓存资源耗尽时,自动调优也会受影响。在本例中,0.678s时刻窗日达到最大值33304字节,接着开始减小。这是由于上层应用停止读取数据,导致缓存被占满。当20s后上层应用继续读操作时,窗口再次增大,超过了之前的最大值(见下图)
- 零窗口通告(包117)使得发送端执行了一系列的窗口探测,但返回的仍是一系列的零窗口。在20.043s时刻恢复读数据操作时,发送端接收到了窗口更新。每接收到一个ACK,窗口就增大两个MSS。随着数据的发送、接收和处理,通告窗口达到了最大值678080该版本的Linux也测量相邻两次读操作完成的时间,并与估计RTT值相比较。如果RTT估计值增大,那么缓存也将增大(但不会因RTT的减小而减小缓存)。这样即使在连接的带宽延时积增大的情况下,自动调优也保持接收端通告窗口优先于发送端窗口
- 随着广域网络连接速度的增长,TCP应用使用的缓存太小已成为严重局限。在美国,全国范围内的RTT约为100ms,在一个1Gb/s的网络中使用64KB的窗口将TCP吞吐量限制在约640KB/s,而计算的最大值可以达到约130MB/s (99%的带宽都浪费了)。实际上,如果在相同网络环境下使用较大容量的缓存,吞吐性能将提升100倍。Web100工程[W100]应得到更多关注和信心。它开发了一系列工具和改进软件,致力于使应用从众多的TCP实现中获得最优的吞吐性能