TCP 接收窗口自动调节

https://technet.microsoft.com/zh-cn/magazine/2007.01.cableguy.aspx

欢迎来到 TechNet 杂志“网络专家”的第一部分。TechNet 网站上的专栏爱好者都知道我们探讨各种网络问题,我们每个月都将继续保持这个传统。如果您是新手,要查找以前专栏的存档,请访问网络专家站点
现在开始介绍我们的第一个主题 - TCP 接收窗口。
TCP 连接的吞吐量可以通过发送和接收应用程序、发送和接收 TCP 的实现以及 TCP 对等方之间的传输路径来限制。在本专栏中,我将介绍 TCP 接收窗口及其对 TCP 吞吐量的影响、TCP 窗口缩放的使用以及 Windows Vista™ 中的“接收窗口自动调节”新功能(可优化 TCP 接收数据吞吐量)。

 

TCP 接收窗口
TCP 连接具有许多重要的特点。首先,它们是两个应用层协议之间的逻辑点对点通信。TCP 不提供一对多的传递服务,它仅提供一对一的传递服务。
其次,TCP 连接是面向连接的。在数据可以传输之前,两个应用层过程必须通过 TCP 连接建立过程正式协商一个 TCP 连接。同样,TCP 连接在通过 TCP 连接终止过程协商之后正式关闭。
再次,在 TCP 连接中发送的可靠数据按顺序排列,且期望得到接收端的肯定确认。如果没有接收到肯定确认,则重传这个段。接收端一端会放弃重复的段,并按照正确顺序排列到达时失序的段。
最后,TCP 连接是全双工的。对于每个 TCP 对等方,TCP 连接都由两个逻辑管道组成:一个传出管道和一个传入管道。TCP 报头包含传出数据的序列号和传入数据的确认 (ACK)。
此外,TCP 将通过传入和传出逻辑管道发送的数据视为连续的字节流。每个 TCP 报头中的序列号和确认号都根据字节边界定义。TCP 并不会考虑字节流中的记录或消息边界。应用层协议必须正确地分析传入的字节流。
为了限制任一时刻可发送的数据量,并为接收端提供流量控制,TCP 对等方使用窗口实现这些目的。该窗口是接收端允许发送端发送的字节流的数据范围。发送端只能发送位于窗口内的字节流中的字节。该窗口随着发送端的出站字节流和接收端的入站字节流而滑动。
对于给定的逻辑管道(全双工 TCP 连接的一个方向),发送端维护一个发送窗口,接收端维护一个接收窗口。当传输中没有数据或 ACK 段时,逻辑管道的发送和接收窗口相互匹配。换句话说,发送端允许发送的出站字节流中的数据范围与接收端能够接收的入站字节流中的数据范围相匹配。图 1说明了这种发送和接收关系。
图 1 匹配发送和接收窗口 (单击该图像获得较大视图)
为了表示接收窗口的大小,TCP 报头包含了一个 16 位的“窗口”字段。当接收端收到数据时,它把 ACK 发送回发送端以表明成功接收到这些字节。在每个 ACK 中,“窗口”字段表示接收窗口中剩余的字节数。当应用程序发送、确认和检索数据后,发送窗口和接收窗口都会滑动到右侧。接收窗口是用于控制可从发送端传送给接收端的未确认数据数量的窗口。
由于接收窗口中可能会有应用程序未检索到的数据以及已接收但尚未确认的数据,因此 TCP 接收窗口具有一些其他的结构,如图 2 所示。
图 2 TCP 接收窗口中的数据类型 (单击该图像获得较大视图)
请注意最大接收窗口和当前接收窗口的区别。最大接收窗口的大小是固定的。当前接收窗口的大小是可变的,并对应于接收端允许发送端发送的剩余数据量。当前接收窗口大小是发送回发送端的 ACK 中通告的“窗口”字段值,等于最大接收窗口大小与已接收和确认但尚未被应用程序检索的数据量之间的差值。

 

TCP 接收窗口和 TCP 吞吐量
为了优化 TCP 吞吐量(假设为合理的无差错传输路径),发送端应该发送足够的数据包以填满发送端和接收端之间的逻辑管道。逻辑管道的容量可由以下公式计算:
 
Capacity in bits = path bandwidth in bits per second * round-trip time (RTT) in seconds
容量称为带宽延迟乘积 (BDP)。管道可以用粗(高带宽)和细(低带宽)或者长(高 RTT)和短(低 RTT)来表示。粗而长的管道的 BDP 最高。使用高 BDP 传输路径的示例包括卫星链接或带有洲际光缆链接的企业广域网 (WAN)。
增强高 BDP 传输的发送端性能
新的“接收窗口自动调节”功能增强了通过高 BDP 链接接收数据的性能,但是发送端的性能如何呢?
避免发送 TCP 对等方拥塞整个网络的现有算法被称为“慢启动”和“拥塞避免”。在连接最初发送数据和还原丢失段时,这些算法可以增大发送窗口,即发送端可以发送的段数量。
对于每个接收到的确认段(Windows XP 和 Windows Server 2003 中的 TCP)或每个已经确认的段(Windows Vista 和 Windows Server“Longhorn”中的 TCP),“慢启动”算法会以一个完整的 TCP 段增大发送窗口。对于每个已经确认的完整窗口的数据,“拥塞避免”算法以一个完整的 TCP 段增大发送窗口。
这些算法很好地适应较小的 BDP 和较小的接收窗口大小。然而,当面对一个具有较大接收窗口大小和较大 BDP 的连接时,例如在位于高速 WAN 链接(往返时间为 100 毫秒)上的两个服务器之间复制数据,利用这些算法增大发送窗口的速度就不足以充分利用连接带宽。
为了在上述情形下更好地利用 TCP 连接的带宽,下一代 TCP/IP 堆栈包括了复合 TCP (CTCP)。CTCP 可以更快地增大发送窗口,适用于拥有较大的接收窗口大小和 BDP 的连接。CTCP 试图通过监控延迟变化和损失来使这类连接的吞吐量最大化。此外,CTCP 确保其行为不会对其他 TCP 连接产生负面影响。
在 Microsoft 内部执行的测试中,对于 RTT 为 50 毫秒、传输速率为每秒 1 Gb 的连接,大型文件的备份时间几乎缩短了一半。对于具有更大 BDP 的连接,性能的改善更为明显。CTCP 和“接收窗口自动调节”配合使用,可以提高链接利用率,最终可以显著提高具有较大 BDP 的连接的性能。
默认情况下,运行 Windows Server“Longhorn”的计算机上启用 CTCP,而运行 Windows Vista 的计算机上则禁用 CTCP。可以使用“netsh interface tcp set global congestionprovider=ctcp”命令启用 CTCP,还可以使用“netsh interface tcp set global congestionprovider=none”命令禁用 CTCP。
TCP 报头中的“窗口”字段大小为 16 位,允许 TCP 对等方通告最大为 65,535 字节的接收窗口。您可以根据以下公式计算给定 TCP 窗口的近似吞吐量:
 
Throughput = TCP maximum receive windowsize / RTT
例如,对于 65,535 字节的接收窗口,在 RTT 为 100 毫秒的路径上只能达到速度大约为每秒 5.24 兆字节 (Mbps) 的吞吐量,而不管传输路径的实际带宽是多少。对于目前的高 BDP 传输路径,最初设计的 TCP 窗口大小即使达到最大值,仍然是吞吐量的瓶颈。

 

TCP 窗口缩放
为了提供可适应高速传输路径的更大窗口尺寸,RFC 1323 (ietf.org/rfc/rfc1323.txt) 定义了允许接收端通告大于 65,535 字节的窗口大小的窗口缩放。“TCP 窗口缩放”选项包括一个窗口缩放因子,该因子与 TCP 报头中的 16 位窗口字段结合时,可以将接收窗口大小最大增加到 1GB。“TCP 窗口缩放”选项只用于在连接建立过程的同步 (SYN) 段中发送数据。TCP 对等方都可以为接收窗口大小指定不同的窗口缩放因子。TCP 窗口缩放允许发送端通过一个连接发送更多的数据,可以使 TCP 节点更好地利用一些具有高 BDP 的传输路径类型。
尽管接收窗口大小对于 TCP 吞吐量而言非常重要,但确定最佳 TCP 吞吐量还有一个重要的因素,那就是应用程序检索接收窗口中累积数据的速度(应用程序检索速率)。如果应用程序不能检索数据,接收窗口就可以开始填充,导致接收端通告了更小的当前窗口大小。在极个别的情况下,整个最大接收窗口都会填满,导致接收端通告了 0 字节的窗口大小。在这种情况下,发送端必须停止发送数据,直到接收窗口已经清除为止。因此,要优化 TCP 吞吐量,必须将连接的 TCP 接收窗口设定为可同时反映该连接传输路径的 BDP 和应用程序检索速率的值。
即使您可以正确地确定 BDP 和应用程序检索速率,它们仍会随时间而变化。BDP 速率可以根据传输路径中的阻塞情况而变化,而应用程序检索速率会根据应用程序检索数据所在的连接数量而变化。

 

Windows XP 中的接收窗口
对于 Windows XP(和 Windows Server® 2003)中的 TCP/IP 堆栈,最大接收窗口的大小具有许多重要的属性。首先,默认值基于发送界面的链接速度。实际值自动调整为 TCP 连接建立过程中协商的最大段大小 (MSS) 的偶数增量。
其次,最大接收窗口的大小可以手动配置。可将注册表项 HKLM\System\CurrentControlSet\Services\Tcpip\Parameters\TCPWindowSize 和 HKLM\System\CurrentControlSet\Services\Tcpip\Parameters\Interface\InterfaceGUID\TCPWindowSize 的值设置为最大 65,535 字节(不带窗口缩放)或 1,073,741,823 字节(带窗口缩放)。
再次,最大接收窗口的大小可以使用窗口缩放。可通过将注册表项 HKLM\System\CurrentControlSet\Services\Tcpip\Parameters\Tcp1323Opts 的值设置为 1 或 3 来启用窗口缩放。默认情况下,仅当接收的同步 (SYN) 段包含“窗口缩放”选项时,才在连接上使用窗口缩放。
最后,启动连接时,可使用应用程序的“SO_RCVBUF Windows Sockets”选项,指定连接的最大接收窗口大小。使用窗口缩放时,应用程序所指定的窗口大小必须大于 65,535 字节。
尽管 Windows XP 支持可缩放窗口,但其中的最大接收窗口大小仍然会限制吞吐量,因为它是针对所有 TCP 连接(除非由应用程序指定)的一个固定的最大大小,它可以增加某些连接的吞吐量,同时减少其他连接的吞吐量。另外,TCP 连接的最大接收窗口大小固定,不随应用程序检索速率的变化或传输路径中的阻塞而变化。

 

Windows Vista 中的接收窗口自动调节
为了优化 TCP 吞吐量,特别是具有高 BDP 的传输路径,Windows Vista(和代码名为“Longhorn”的 Windows Server 下一版本)中的下一代 TCP/IP 堆栈支持接收窗口自动调节功能。该功能通过测量 BDP 和应用程序检索速率,以及调整当前的传输路径和应用程序状况的窗口大小,来确定最合适的接收窗口大小。
默认情况下,“接收窗口自动调节”会启用 TCP 窗口缩放,它所允许的最大窗口大小为 16 MB。数据流通过连接时,下一代 TCP/IP 堆栈会监控连接,测量该连接当前的 BDP 和应用程序检索速率,并调整接收窗口大小以优化吞吐量。下一代 TCP/IP 堆栈不再使用 TCPWindowSize 注册表值。
“接收窗口自动调节”功能具有许多优点。它可以根据每个连接自动确定最佳的接收窗口大小。在 Windows XP 中,TCPWindowSize 注册表值适用于所有的连接。应用程序无需再通过“Windows Socket”选项指定 TCP 窗口大小。并且 IT 管理员也无需再为特定的计算机手动配置 TCP 接收窗口的大小。
使用“接收窗口自动调节”功能后,基于 Windows Vista 的 TCP 对等方通常会比基于 Windows XP 的 TCP 对等方通告更大的接收窗口大小。这使得其他 TCP 对等方可以通过发送更多的 TCP 数据段来将管道填入基于 Windows Vista 的 TCP 对等方,而无需等待 ACK(服从 TCP 拥塞控制)。对于如网页或电子邮件等典型的基于客户端的网络通信,Web 服务器或电子邮件服务器可以向客户端计算机更快更多地发送 TCP 数据,从而全面提高网络性能。连接的 BDP 和应用程序检索速率越高,性能的提高就越明显。
在数据传输中,将通常以较慢的速度发送的 TCP 数据包流快速发送,从而导致网络利用率出现更大的高峰,这就是该方法对网络造成的影响。基于 Windows XP 和 Windows Vista 的计算机在长而粗的管道上执行相同的数据传输时,传输的数据量相同。然而,基于 Windows Vista 的客户端计算机的数据传输更快,因为其具有更大的接收窗口大小,并且服务器能够填充从自身到客户端的管道。
由于“接收窗口自动调节”会增加高 BDP 传输路径的网络利用率,因此对于达到或接近最大容量的传输路径,限制使用服务质量 (QoS) 或应用程序发送速率可能会变得非常重要。为了满足此潜在的需要,Windows Vista 支持基于组策略的 QoS 设置,可以利用该设置为基于 IP 地址或 TCP 端口的信息流发送速率定义限制速率。有关详细信息,请参阅基于策略的 QoS 资源
增加高损失网络的 TCP 吞吐量
在高损失网络中,频繁的超时和重传可能会大大降低 TCP 吞吐量。高损失网络的一个例子就是无线网络,如基于 IEEE 802.11、通用分组无线业务 (GPRS) 或通用移动通信系统 (UMTS) 的网络,由于网络状况、信号衰减、电磁干扰和计算机的位置变化,可能会有较高的数据包丢失率。
下一代 TCP/IP 堆栈支持以下四种 RFC,可以优化高丢失率环境中的吞吐量:

RFC 2582:The NewReno Modification to TCP's Fast Recovery Algorithm(TCP 快速恢复算法 NewReno 修正)
RFC 2001 中定义的快速恢复算法基于 Reno 算法,由于快速重传事件而重传段时,Reno 算法可增加发送端能发送的数据量。Reno 算法适用于单个丢失的段,有多个丢失段时就不适用了。当数据窗口中的多个段丢失且发送端收到部分确认(仅对成功接收的那部分数据的确认)时,NewReno 算法通过更改快速恢复过程中发送端可以用来提高发送速率的方法,提供更大的吞吐量。

RFC 2883:An Extension to the Selective Acknowledgment (SACK) Option for TCP(TCP 选择确认 (SACK) 选项扩展)
RFC 2018 中定义的 SACK,允许接收端通过使用 SACK TCP 选项指示最多四个接收数据的非邻接块。RFC 2883 定义用于确认重复的数据包的 SACK TCP 选项中的字段的额外使用。发送端可以通过此操作确定何时重传了不必要的段并调整其行为,以防今后不必要的重传。发送的重传越少,整体吞吐量越合理。

RFC 3517:A Conservative Selective Acknowledgment-based Loss Recovery Algorithm for TCP(TCP 的基于保守选择确认的丢失恢复算法)
Windows Server 2003 和 Windows XP 中的 TCP/IP 当前实现只使用 SACK 信息确定未到达目标的 TCP 段。RFC 3517 定义了收到重复确认后使用 SACK 信息执行丢失恢复的方法,以便在连接上启用 SACK 时替代原来的快速恢复算法。下一代 TCP/IP 堆栈基于每个连接跟踪 SACK 信息并监控传入确认和重复确认,以便在目标未收到多个段时更快进行恢复。

RFC 4138:Forward RTO-Recovery (F-RTO):An Algorithm for Detecting Spurious Retransmission Timeouts with TCP and the Stream Control Transmission Protocol (SCTP)(使用 TCP 和流控制传输协议 (SCTP) 探测伪重传超时设定的算法)
RTT 突然增加时,可能会出现 TCP 段的伪重传现象,导致先前发送的段的重传超时设定 (RTO) 逐渐到期,TCP 开始重传它们。如果 RTT 增加正好发生在发送整个窗口的数据前,发送端可能会重传整个窗口的数据。F-RTO 算法通过以下行为防止 TCP 段的伪重传。
当多个段的 RTO 到期时,TCP 只重传第一个段。收到第一个确认后,TCP 开始发送新段(如果通告的窗口大小允许)。如果下一个确认确认超时但未重传的其他段,则 TCP 确定超时是伪超时,不重传超时的其他段。
这样的结果是,对于 RTT 突然和临时增加的环境(例如,当无线客户端从一个入口点漫游到另一个时),F-RTO 可防止不必要的段重传并更快地恢复到正常发送速率。基于 SACK 的丢失恢复和 F-RTO 最适用于使用 GPRS 链接的连接
 
http://blog.chinaunix.net/xmlrpc.php?r=blog/article&uid=28387257&id=3595033

一、RFC关于增大TCP初始窗口的讨论

    http://tools.ietf.org/html/rfc2414
    详尽的讨论参见原文,这里做一部分摘录,加上自己的理解
    增大起始窗口,和MTU探测等方面有关系【注1】。

1.1 增大初始窗口的好处

    1.1.1 可以及时回复ack

        这里牵涉到TCP的delay ack机制,普通情况下是每两个段一个ack。
        如果起始窗口只有一个段大小,就需要等待delay ack机制的定时器超时,大约为40ms。【注2】
            关于delay ack详细内容,参见另一篇博文《TCP delay ack机制和实现》(整理中)。    
        如果起始窗口大于等于两个数据段,发送端就有足够的数据段来触发ack,防止了RTO。

    1.1.2 节省小文件传输时间

        如果连接传输少量数据,较大的初始窗口可以节省时间。
        例如邮件、网页等较小的文件,传输时间甚至可以节省到一个RTT。
        在丢包率较高的情况下,也可以用大初始窗口来传输小文件节省时间。【注3】      

    1.1.3 节省大BDP网络传输时间

        对于能使用大拥塞窗口的链路,这种改进可以在慢启动阶段节省多个RTT,并避免delay ack超时。
        这对于带宽时延积(BDP)较大的链路,比如卫星链路,有特别的好处。

1.2 增大初始窗口的坏处

    1.2.1 对于用户的坏处

        在高拥塞环境,特别对于路由器厌弃突发流的情况。【注4】
            TCP初始窗口为1,有时会比较好。
            例如,某些场景下,初始窗口为1的TCP包没有被丢弃,窗口为4的遭遇了丢包。
            这是因为路由器能力有限,无法应对数据流的小突发。【注5】
            同时可能导致不必要的RTO,对于没有对TCP流造成超时的情况,也会导致连接
            过早地从慢启动转移到拥塞避免阶段。【注6】
        这种丢包和过早转移在较低的拥塞环境,路由器buffer足够的情况下不易发生。
            在平和拥塞环境下,如果路由器采用主动队列管理【注7】,也不会发生。
        有时,即便有丢包和过早转移,用户依然能从较大的初始窗口中获益
            1> 如果TCP连接没有RTO,及时恢复
            2> TCP窗口被网络拥塞或接收方通告窗口限制在一个较小范围内。 【注8】      

     1.2.2 对于网络的坏处

            就网络拥塞崩溃方面的因素而言,可以考虑两个潜在的危险场景。
            第一个场景是对端已经收到包,但链路极度拥塞,发送端不断发送重复包,不断超时。
            第二个场景是拥塞链路中的大量包在它们到达链路之前就会丢掉。
            增大拥塞窗口的潜在威胁是,加重了网络总体的丢包率。三个问题讨论:
             1> 重复段。较大的初始窗口可能引起丢包,但实验表明,即便在这种情况下,也没有大量重传。
             2> 必然会被丢弃的段。在只有一段拥塞链路的网络中,这些由较大初始窗口发送的多余段不会
                    浪费带宽,也不会带来拥塞崩溃。在有多条拥塞链路时,才可能浪费带宽。但是,这种浪费
                    带宽的行为对于初始窗口为1或4来说,都是相同的。【注9】
             3> 丢包率增长。较大的初始窗口在路由器执行队尾丢包的情况下,会导致丢包率的进一步增加。
                   但并不会导致过大的丢包率增长。
    实验结果参见原文。
            按个人理解来讲,过大的初始窗口坏处在于:不公平,且在丢包恢复方面有负担。
            在web浏览中,发起多个具有大窗口的连接是十分违反规则的。

二、Google建议将TCP初始窗口改为10

        sigcomm 2010
        《An Argument for Increasing TCP ’s Initial Congestion window》
        常见TCP初始窗口设置为3或4,大约4KB。
        在长流服务中,这种较小的初始窗口设定没有什么问题。
        但是,如今常见的许多web服务数据流较短,比如一个页面,可能只有4K-6K。
        在慢启动阶段,整个数据流的传输可能就会结束。
        此时,初始窗口的大小对于传输时间的长短起决定性作用。
        现有的chrome firefox等浏览器并发连接数为6,可以减少网页的下载时间。
        http://www.iefans.net/bingfa-lianjieshu-sudu-ceshi/
        如果使用较大的初始窗口,速度改进将更加显著。

2.1 提高初始窗口的好处

    2.1.1 减少发送延迟

        延迟计算公式如下,其中,S是传输块大小,C是链路带宽。
        
        不必在意公式细节,从直观上理解,初始窗口越大,需要的总传输次数越少。
        比如初始窗口为10,当然要比初始窗口为1节省时间。

    2.1.2 跟上页面的增长

        现有的很多web页面大小有几百KB,增大初始窗口能保持与时俱进。

    2.1.3 与长流竞争时,短流可以更加公平

        长流一般拥塞窗口已经很大,提高初始窗口可以让短流更加公平。
        在TCP性能评估中,公平性是一个重要指标。

    2.1.4 提升丢包恢复速度

        这是理所当然的,初始窗口大了,丢包重传时花的时间也就少了。

2.2 为什么是10

         根据Google的研究,90%的HTTP请求数据都在16KB以内,约为10个TCP段。

       对于较大数据量传输,初始窗口设定为10也能提升reponses的反应速度。
       将初始窗口设置为3、6、10、16、26、42,实验结果表明,init_cwnd=16可以最大限度减少延迟。
       论文版权原因,不贴图了,感兴趣的请自己搜索下实验图表。
       如果窗口进一步提升,延迟会再增加,可能是因为丢包方面的负担。
       详细参见https://datatracker.ietf.org/doc/draft-ietf-tcpm-initcwnd/?include_text=1

          关于初始窗口为什么不取16,原文作者说道,他们发现在全球网络环境实验中,
            将初始窗口设置为16,在某些地区可能引起较差的反应。例如在东南亚,
            初始窗口取为16引起了明显的丢包,这可能是过于拥塞的网络环境和浏览器发起同步连接引起的。
            而初始窗口为10和16,性能差距不大,因此取10作为一个安全的值。  【注 10】

 

三、增大初始窗口代码注意

        因为某些原因,不便详述代码方面的细节。
        可以提示一下:初始拥塞窗口过大时,会收到初始接收窗口的限制。

转载请注明出处。
http://blog.chinaunix.net/uid-28387257-id-3595033.html
注:
【1】没看太仔细,有兴趣的同学可以看一下,欢迎讨论
【2】原文写的是RTO等待,这里有一个问题。
        接收端发送ack的过程是与delay ack超时相关的。
        RTO超时是指发送端等不到ack,重复发送数据段的过程。
【3】原文是:a larger initial window reduces the transmission time
         (assuming at most moderate segment drop rates).
        在稳健的高丢包情况下。存疑。
        这一点难以理解:丢包率固定的情况下,
        丢包数和总包数比例是一定的,并不会因为初始窗口的原因减少。
【4】对于高拥塞网络,如果有突发流,路由器很可能丢弃。
        因为队列满了,路由器是drop tail原则。
【5】原文为due to the inability of the router to handle small bursts.
        称“small”,是因为突然的流不大,与之相对,也有大突发。
【6】这两个阶段,发送窗口的增长速率是很不同的,慢启动阶段窗口
        指数增长,如果过早的转移到拥塞避免阶段,此时窗口不够大,
        又是线性增长,会在很长一段时间内拖慢吞吐率。
【7】例如,http://tools.ietf.org/html/rfc2309 的主动队列管理算法。
【8】这种情况下,起始窗口较大,而增长被限制,初始发送不会丢包,
        也提高了速率。存疑。   
【9】“浪费”带宽的说法没有完全理解,既然是丢弃,无论有多少段拥塞链路,
        都是一样的吧。
【10】来自于作者的邮件交流。
 
http://blog.csdn.net/zhangskd/article/details/8200048

引言 

 

TCP中有拥塞控制,也有流控制,它们各自有什么作用呢?

拥塞控制(Congestion Control) — A mechanism to prevent a TCP sender from overwhelming the network.

流控制(Flow Control) — A mechanism to prevent a TCP sender from overwhelming a TCP receiver.

 

下面是一段关于流控制原理的简要描述。

“The basic flow control algorithm works as follows: The receiver communicates to the sender the maximum

amount of data it can accept using the rwnd protocol field. This is called the receive window. The TCP sender

then sends no more than this amount of data across the network. The TCP sender then stops and waits for

acknowledgements back from the receiver. When acknowledgement of the previously sent data is returned to

the sender, the sender then resumes sending new data. It's essentially the old maxim hurry up and wait. ”

由于发送速度可能大于接收速度、接收端的应用程序未能及时从接收缓冲区读取数据、接收缓冲区不够大不能

缓存所有接收到的报文等原因,TCP接收端的接收缓冲区很快就会被塞满,从而导致不能接收后续的数据,发送端

此后发送数据是无效的,因此需要流控制。TCP流控制主要用于匹配发送端和接收端的速度,即根据接收端当前的

接收能力来调整发送端的发送速度。

 

TCP流控制中一个很重要的地方就是,TCP接收缓存大小是如何动态调整的,即TCP确认窗口上限是如何动态调整的?

本文主要分析TCP接收缓存大小动态调整的原理和实现。

 

原理

 

早期的TCP实现中,TCP接收缓存的大小是固定的。随着网络的发展,固定的TCP接收缓存值就不适应了,

成为TCP性能的瓶颈之一。这时候就需要手动去调整,因为不同的网络需要不同大小的TCP接收缓存,手动调整不仅

费时费力,还会引起一些问题。TCP接收缓存设置小了,就不能充分利用网络。而TCP缓存设置大了,又浪费了内存。

如果把TCP接收缓存设置为无穷大,那就更糟糕了,因为某些应用可能会耗尽内存,使其它应用的连接陷入饥饿。

所以TCP接收缓存的大小需要动态调整,才能达到最佳的效果。

动态调整TCP接收缓存大小,就是使TCP接收缓存按需分配,同时要确保TCP接收缓存大小不会成为传输的限制。

linux采用Dynamic Right-Sizing方法来动态调整TCP的接收缓存大小,其基本思想就是:通过估算发送方的拥塞窗口

的大小,来动态设置TCP接收缓存的大小。

 

It has been demomstrated that this method can successfully grow the receiver's advertised window at a pace

sufficient to avoid constraining the sender's throughput. As a result, systems can avoid the network performance

problems that result from either the under-utilization or over-utilization of buffer space.

 

实现

 

下文代码基于3.2.12内核,主要源文件为:net/ipv4/tcp_input.c。

[java] view plaincopy
 
  1. struct tcp_sock {  
  2.     ...  
  3.     u32 rcv_nxt; /* What we want to receive next,希望接收的下一个序列号 */  
  4.     u32 rcv_wnd; /* Current receiver window,当前接收窗口的大小*/  
  5.     u32 copied_seq; /* Head of yet unread data,应用程序下次从这里复制数据 */  
  6.     u16 advmss; /* Advertised MSS,接收端通告的MSS */  
  7.     u32 window_clamp; /* Maximal window to advertise,通告窗口的上限*/  
  8.   
  9.     /* Receiver side RTT estimation */  
  10.     struct {  
  11.         u32 rtt;  
  12.         u32 seq;  
  13.         u32 time;  
  14.     } rcv_rtt_est; /* 用于接收端的RTT测量*/  
  15.   
  16.     /* Receiver queue space */  
  17.     struct {  
  18.         int space;  
  19.         u32 seq;  
  20.         u32 time;  
  21.     } rcvq_space; /* 用于调整接收缓冲区和接收窗口*/  
  22.   
  23.     /* Options received (usually on last packet, some only on SYN packets). */  
  24.     struct tcp_options_received rx_opt; /* TCP选项*/  
  25.     ...  
  26. };  
  27.   
  28. struct sock {  
  29.     ...  
  30.     int sk_rcvbuf; /* TCP接收缓冲区的大小*/  
  31.     int sk_sndbuf; /* TCP发送缓冲区大小*/  
  32.     unsigned int ...  
  33.         sk_userlocks : 4, /*TCP接收缓冲区的锁标志*/  
  34.     ...  
  35. };   

 

RTT测量

 

在发送端有两种RTT的测量方法(具体可见前面blog),但是因为TCP流控制是在接收端进行的,所以接收端也需要

有测量RTT的方法。

 

(1)没有时间戳时的测量方法

[java] view plaincopy
 
  1. static inline void tcp_rcv_rtt_measure(struct tcp_sock *tp)  
  2. {  
  3.     /* 第一次接收到数据时,需要对相关变量初始化*/  
  4.     if (tp->rcv_rtt_est.time == 0)  
  5.         goto new_measure;  
  6.   
  7.     /* 收到指定的序列号后,才能获取一个RTT测量样本*/  
  8.     if (before(tp->rcv_nxt, tp->rcv_rtt_est.seq))  
  9.         return;  
  10.   
  11.     /* RTT的样本:jiffies - tp->rcv_rtt_est.time */  
  12.     tcp_rcv_rtt_update(tp, jiffies - tp->rcv_rtt_est.time, 1);  
  13.   
  14. new_measure:  
  15.     tp->rcv_rtt_est.seq = tp->rcv_nxt + tp->rcv_wnd; /* 收到此序列号的ack时,一个RTT样本的计时结束*/  
  16.     tp->rcv_rtt_est.time = tcp_time_stamp; /* 一个RTT样本开始计时*/  
  17. }  

此函数在接收到带有负载的数据段时被调用。

此函数的原理:我们知道发送端不可能在一个RTT期间发送大于一个通告窗口的数据量。那么接收端可以把接收一个

确认窗口的数据量(rcv_wnd)所用的时间作为RTT。接收端收到一个数据段,然后发送确认(确认号为rcv_nxt,通告

窗口为rcv_wnd),开始计时,RTT就是收到序号为rcv_nxt + rcv_wnd的数据段所用的时间。

很显然,这种假设并不准确,测量所得的RTT会偏大一些。所以这种方法只有当没有采用时间戳选项时才使用,而内核

默认是采用时间戳选项的(tcp_timestamps为1)。

下面是一段对此方法的评价:

If the sender is being throttled by the network, this estimate will be valid. However, if the sending application did not

have any data to send, the measured time could be much larger than the actual round-trip time. Thus this measurement

acts only as an upper-bound on the round-trip time.

 

(2)采用时间戳选项时的测量方法

[java] view plaincopy
 
  1. static inline void tcp_rcv_rtt_measure_ts(struct sock *sk, const struct sk_buff *skb)  
  2. {  
  3.     struct tcp_sock *tp = tcp_sk(sk);  
  4.     /* 启用了Timestamps选项,并且流量稳定*/  
  5.     if (tp->rx_opt.rcv_tsecr && (TCP_SKB_CB(skb)->end_seq - TCP_SKB_CB(skb)->seq >=  
  6.         inet_csk(sk)->icsk_ack.rcv_mss))  
  7.         /* RTT = 当前时间 - 回显时间*/  
  8.         tcp_rcv_rtt_update(tp, tcp_time_stamp - tp->rx_opt.rcv_tsecr, 0);  
  9. }  

虽然此种方法是默认方法,但是在流量小的时候,通过时间戳采样得到的RTT的值会偏大,此时就会采用

没有时间戳时的RTT测量方法。

 

(3)采样处理

不管是没有使用时间戳选项的RTT采样,还是使用时间戳选项的RTT采样,都是获得一个RTT样本。

之后还需要对获得的RTT样本进行处理,以得到最终的RTT。

[java] view plaincopy
 
  1. /* win_dep表示是否对RTT采样进行微调,1为不进行微调,0为进行微调。*/  
  2. static void tcp_rcv_rtt_update(struct tcp_sock *tp, u32 sample, int win_dep)  
  3. {  
  4.     u32 new_sample = tp->rcv_rtt_est.rtt;  
  5.     long m = sample;  
  6.   
  7.     if (m == 0)  
  8.         m = 1; /* 时延最小为1ms*/  
  9.   
  10.     if (new_sample != 0) { /* 不是第一次获得样本*/  
  11.         /* If we sample in larger samples in the non-timestamp case, we could grossly 
  12.          * overestimate the RTT especially with chatty applications or bulk transfer apps 
  13.          * which are stalled on filesystem I/O. 
  14.          * 
  15.          * Also, since we are only going for a minimum in the non-timestamp case, we do 
  16.          * not smooth things out else with timestamps disabled convergence takes too long. 
  17.          */  
  18.         /* 对RTT采样进行微调,新的RTT样本只占最终RTT的1/8 */  
  19.         if (! win_dep) {   
  20.             m -= (new_sample >> 3);  
  21.             new_sample += m;  
  22.   
  23.         } else if (m < new_sample)  
  24.             /* 不对RTT采样进行微调,直接取最小值,原因可见上面那段注释*/  
  25.             new_sample = m << 3;   
  26.   
  27.     } else {   
  28.         /* No previous measure. 第一次获得样本*/  
  29.         new_sample = m << 3;  
  30.     }  
  31.   
  32.     if (tp->rcv_rtt_est.rtt != new_sample)  
  33.         tp->rcv_rtt_est.rtt = new_sample; /* 更新RTT*/  
  34. }  

对于没有使用时间戳选项的RTT测量方法,不进行微调。因为用此种方法获得的RTT采样值已经偏高而且收敛

很慢。直接选择最小RTT样本作为最终的RTT测量值。

对于使用时间戳选项的RTT测量方法,进行微调,新样本占最终RTT的1/8,即rtt = 7/8 old + 1/8 new。

 

调整接收缓存

 

当数据从TCP接收缓存复制到用户空间之后,会调用tcp_rcv_space_adjust()来调整TCP接收缓存和接收窗口上限的大小。

[java] view plaincopy
 
  1. /*  
  2.  * This function should be called every time data is copied to user space. 
  3.  * It calculates the appropriate TCP receive buffer space. 
  4.  */  
  5. void tcp_rcv_space_adjust(struct sock *sk)  
  6. {  
  7.     struct tcp_sock *tp = tcp_sk(sk);  
  8.     int time;  
  9.     int space;  
  10.   
  11.     /* 第一次调整*/  
  12.     if (tp->rcvq_space.time == 0)  
  13.         goto new_measure;  
  14.   
  15.     time = tcp_time_stamp - tp->rcvq_space.time; /*计算上次调整到现在的时间*/  
  16.   
  17.     /* 调整至少每隔一个RTT才进行一次,RTT的作用在这里!*/  
  18.     if (time < (tp->rcv_rtt_est.rtt >> 3) || tp->rcv_rtt_est.rtt == 0)  
  19.         return;  
  20.   
  21.     /* 一个RTT内接收方应用程序接收并复制到用户空间的数据量的2倍*/  
  22.     space = 2 * (tp->copied_seq - tp->rcvq_space.seq);  
  23.     space = max(tp->rcvq_space.space, space);  
  24.   
  25.     /* 如果这次的space比上次的大*/  
  26.     if (tp->rcvq_space.space != space) {  
  27.         int rcvmem;  
  28.         tp->rcvq_space.space = space; /*更新rcvq_space.space*/  
  29.   
  30.         /* 启用自动调节接收缓冲区大小,并且接收缓冲区没有上锁*/  
  31.         if (sysctl_tcp_moderate_rcvbuf && ! (sk->sk_userlocks & SOCK_RCVBUF_LOCK)) {  
  32.             int new_clamp = space;  
  33.             /* Receive space grows, normalize in order to take into account packet headers and 
  34.              * sk_buff structure overhead. 
  35.              */  
  36.              space /= tp->advmss; /* 接收缓冲区可以缓存数据包的个数*/  
  37.   
  38.              if (!space)  
  39.                 space = 1;  
  40.   
  41.             /* 一个数据包耗费的总内存包括: 
  42.                * 应用层数据:tp->advmss, 
  43.                * 协议头:MAX_TCP_HEADER, 
  44.                * sk_buff结构, 
  45.                * skb_shared_info结构。 
  46.                */  
  47.              rcvmem = SKB_TRUESIZE(tp->advmss + MAX_TCP_HEADER);  
  48.   
  49.              /* 对rcvmem进行微调*/  
  50.              while(tcp_win_from_space(rcvmem) < tp->advmss)  
  51.                  rcvmem += 128;  
  52.   
  53.              space *= rcvmem;  
  54.              space = min(space, sysctl_tcp_rmem[2]); /*不能超过允许的最大接收缓冲区大小*/  
  55.   
  56.              if (space > sk->sk_rcvbuf) {  
  57.                  sk->sk_rcvbuf = space; /* 调整接收缓冲区的大小*/  
  58.                  /* Make the window clamp follow along. */  
  59.                  tp->window_clamp = new_clamp; /*调整接收窗口的上限*/  
  60.              }  
  61.         }  
  62.     }  
  63.   
  64. new_measure:  
  65.      /*此序号之前的数据已复制到用户空间,下次复制将从这里开始*/  
  66.     tp->rcvq_space.seq = tp->copied_seq;  
  67.     tp->rcvq_space.time = tcp_time_stamp; /*记录这次调整的时间*/  
  68. }  
  69.   
  70.   
  71. /* return minimum truesize of the skb containing X bytes of data */  
  72. #define SKB_TRUESIZE(X) ((X) +              \  
  73.                             SKB_DATA_ALIGN(sizeof(struct sk_buff)) +        \  
  74.                             SKB_DATA_ALIGN(sizeof(struct skb_shared_info)))  
  75.   
  76.   
  77. static inline int tcp_win_from_space(int space)  
  78. {  
  79.     return sysctl_tcp_adv_win_scale <= 0 ?  
  80.               (space >> (-sysctl_tcp_adv_win_scale)) :  
  81.                space - (space >> sysctl_tcp_adv_win_scale);  
  82. }  

tp->rcvq_space.space表示当前接收缓存的大小(只包括应用层数据,单位为字节)。

sk->sk_rcvbuf表示当前接收缓存的大小(包括应用层数据、TCP协议头、sk_buff和skb_shared_info结构,

tcp_adv_win_scale微调,单位为字节)。

 

系统参数

(1) tcp_moderate_rcvbuf

是否自动调节TCP接收缓冲区的大小,默认值为1。

(2) tcp_adv_win_scale

在tcp_moderate_rcvbuf启用的情况下,用来对计算接收缓冲区和接收窗口的参数进行微调,默认值为2。

This means that the application buffer is 1/4th of the total buffer space specified in the tcp_rmem variable.

(3) tcp_rmem

包括三个参数:min default max。

tcp_rmem[1] — default :接收缓冲区长度的初始值,用来初始化sock的sk_rcvbuf,默认为87380字节。

tcp_rmem[2] — max:接收缓冲区长度的最大值,用来调整sock的sk_rcvbuf,默认为4194304,一般是2000多个数据包。 

 

小结:接收端的接收窗口上限和接收缓冲区大小,是接收方应用程序在上个RTT内接收并复制到用户空间的数据量的2倍,

并且接收窗口上限和接收缓冲区大小是递增的。

(1)为什么是2倍呢?

In order to keep pace with the growth of the sender's congestion window during slow-start, the receiver should

use the same doubling factor. Thus the receiver should advertise a window that is twice the size of the last

measured window size.

这样就能保证接收窗口上限的增长速度不小于拥塞窗口的增长速度,避免接收窗口成为传输瓶颈。

(2)收到乱序包时有什么影响?

Packets that are received out of order may have lowered the goodput during this measurement, but will increase

the goodput of the following measurement which, if larger, will supercede this measurement. 

乱序包会使本次的吞吐量测量值偏小,使下次的吞吐量测量值偏大。

 

Author

 

zhangskd @ csdn

 

Reference

 

[1] Mike Fisk, Wu-chun Feng, "Dynamic Right-Sizing in TCP".

 

上传压死下载 & 常见TCP选项

http://blog.csdn.net/zhangskd/article/details/7978582
 
 

上传压死下载

 

下载文件的速度非常低。

抓取数据包分析,发现:

服务器 —> 客户端 的包,经历时间 < 1ms后,对方做出反应。

客户端 —> 服务器 的包,经历几十至几百ms后,对方才有反应。一个文件中的第一个SYN请求还超时,3s后重传。

服务器 —> 客户端 的包,至少都重传了一遍,不论是在建立连接时,还是在传送数据时。

可能的原因:客户端 —> 服务器的链路拥塞,丢包率高,客户端的ACK丢失了,服务器就会超时重传。

 

标准TCP的实现借助反馈机制(ACK数据包)来控制流量。当链路上行方向带宽用满后,下行方向数据的ACK数据包

将与上行方向的大数据流竞争上行带宽,丢包的概率增大。ACK数据包的丢失将严重影响TCP的流控机制,从而降低

下行方向的数据吞吐率,造成下行带宽的严重浪费,即所谓的上传压死下载。

根本上的解决方法:杜绝上行拥塞,并给予ACK数据包高优先级。

 

更具体的猜测:

P2P软件在下载的同时,也在为其他用户提供上传。BT、迅雷等软件在下载的同时又作为种子为其他人提供下载服务,

由于ADSL上行带宽最大只有512Kbps,所以使用P2P软件后更容易造成局域网出口上行带宽的拥塞,但是任何上网操作

均需要上行/下行两个方向的流量,如果上行带宽被占满,就会影响到下行带宽的使用。

 

常见TCP SYN包选项构成

 

02 04 05 b4

04 02 08 0a

00 00 ed d1

00 00 00 00

0103 03 06

 

Maximum Segment Size

0x 02 04 05 b4

Kind = 2

Length = 4

Maximum Segment Size = 0x05b4 = 1460

 

SACK permitted

0x 04 02

Kind = 4

Length = 2

 

Timestamp

0x 08 0a 00 00 ed d1 00 00 00 00

Kind = 8

Length = 10

Timestamp Value = 0x0000edd1 = 60881

Timestamp Echo Reply = 0x00000000 = 0

 

NOP

0x 01

Kind = 1

 

Window Scale

0x 03 03 06

Kind  = 3

Length = 3

Shift count = 6

TCP头中Window size value为14600,表示tcp_rmem为14600字节。接下来的数据包中,这个值就要扩大64倍。

posted @ 2015-03-12 15:50  贺大卫  阅读(7152)  评论(0编辑  收藏  举报