TCP 流量&拥塞控制详解

TCP 流量控制与窗口管理

TCP 必需要解决的可靠传输以及包乱序( reordering )的问题,所以,TCP 必需要知道网络实际的数据处理带宽或是数据处理速度,这样才不会引起网络拥塞,导致丢包。

TCP 协议设计了一些技术来做网络流量控制管理,sliding window ( 滑动窗口 )是其中的一个技术。

一些基本的概念:

  • 窗口( Window ):窗口是在 TCP 协议的头部的一个字段,又被为通告窗口( Advertised-Window ),用于接收端告诉发送端自己还有多少缓冲区可以接收数据。发送端可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。

发送端如何根据 ACK 和 接收端的窗口大小计算出应该发送多少字节给接收端?

ACK 当中包含了两个重要的信息,一是期望接收到的下一字节的序号 n ,该 n 代表接收方已经接收到了前 n-1 字节的数据;二是当前的窗口大小 m 。有了这两个信息,发送端就可以计算出需要发送的字节数。例如当前发送方已发送到第 x 字节,则可以发送的字节数就是 y=m-(x-n) 。这就是滑动窗口控制流量的基本原理。

滑动窗口机制

滑动窗口发送端和接收端的基本结构

发送端

按照上图,发送缓冲区中基本分为了4个区域:

  • Sent and Acknowledged :表示已经发送成功并已经被发送端确认(ACK)的数据。这部分数据区域属于滑动窗口的范围之内。
  • Send But Not Yet Acknowledged :表示已经发送但是还没有被发送端确认的数据。这部分数据区域属于滑动窗口的范围之内。需要注意的是,这部分区域又被称为: Window Already Sent 。
  • Not sent,Recipient Ready to Receive :表示需要尽快发送的数据,这部分数据已经被加载到发送缓冲区中,等待发送,需要注意的是,这部分的空间大小完全是由接收端决定的。这部分数据区域属于滑动窗口的范围之内。需要注意的是,这部分区域又被称为: Usable Window 。
  • Not Sent,Recipient Not Ready to Receive :表示数据未发送,同时接收端也不允许发送的,因为数据已经超出了发送端所接收的范围。这部分数据区域属于滑动窗口的范围之内。

下图是当滑动窗口动态变化了之后的情况,此时,滑动窗口的左边界产生了右移,由最开始的32移动到了37,并发送了46 ~ 51之间的数据。

接收端

对于接收端也有一个接收窗口,类似于发送端,接收端的数据只有3个分类:

  • Received and ACK Not Send to Process :表示数据已经被内核缓冲区接收了,但是还没有被用户态的进程接。

  • Received Not ACK :已经被内核接收但是还没有向发送端回复ACK。

  • Not Received :内核缓冲区中空闲的区域,还没有被接收的数据区域。

一个能够直观理解滑动窗口机制的动画视频

建议大家都能够花几十秒的时间看一下: v.youku.com/v_show/id_X...

窗口扩大选项( TCP window scale option )

由于 TCP 头定义的 window 是 16 位,这意味着 window 的最大值是: 2 ^ 16 = 65536 byte = 64kb 。这个窗口大小对于现代高速的网络情况下,显然是一个很小的值。因此,TCP 在 RFC 1323 中添加了一个扩展选项: TCP window scale option ,用于处理长肥网络long fat networks

让我们来看看如果没有窗口扩大选项时,网络传输效率会有什么影响。假设在一个局域网网络内,带宽为 1.5 Mbit/s , RTT 为 513 ms ,那么 bandwidth-delay product 就为: 1,500,000 * 0.513 = 769500 bits , 96187 bytes 。如果按照原有的 TCP 定义的窗口大小的最大值:64 kb 来计算, 65535 / 96187 = 68% ,只能使用到 68 % 的网络带宽,大约为 1.02 Mbit/s 。

如何设置 linux 下开启窗口扩大选项?

net.ipv4.tcp_window_scaling 设置为1表示为开启。从 Linux 内核 2.6.8 版本开始,窗口扩大因子默认就是打开的。

sysctl -n net.ipv4.tcp_window_scaling
1

如何查看窗口扩大选项的具体值?

当设置开启窗口扩大选项了之后,TCP 会通过建立连接的 SYN 包来告诉对方窗口扩大选项的具体值,参考如下的 tcpdump 抓取的三次握手的数据包:

ini 复制代码
IP 192.168.1.122.64543 > 192.168.1.123.8000: Flags [S],  seq 768366895,  win 65535,  options [mss 1460, nop, wscale 5, nop, nop, TS val 1148856935 ecr 0, sackOK, eol],  length 0
IP 192.168.1.123.8000 > 192.168.1.122.64543: Flags [S.],  seq 2363927244,  ack 768366896,  win 14480,  options [mss 1440, sackOK, TS val 519920553 ecr 1148856935, nop, wscale 7],  length 0
IP 192.168.1.122.64543 > 192.168.1.123.8000: Flags [.],  ack 1,  win 4105,  options [nop, nop, TS val 1148856999 ecr 519920553],  length 0

wscale 即为窗口扩大选项的大小。可以看到发送端的值为 5 , 接收端的值为 7 。

窗口扩大选项的值是如何动态计算出来的?

窗口扩大选项的值是基于内核接收缓冲区的最大值( maximum size of receive buffer )来动态计算出的,maximum size of receive buffer 的值可以通过 net.ipv4.tcp_rmem 查看到,最后一列的 4194304 即表示接收缓冲区的最大值。

yaml 复制代码
sysctl -n net.ipv4.tcp_rmem
4096    87380   4194304

下面的代码描述了如何根据 maximum size of receive buffer 计算窗口扩大选项的大小。

ini 复制代码
if (wscale_ok) {
       space = max_t(u32, sysctl_tcp_rmem[2], sysctl_rmem_max);
       while (space > 65535 && (*rcv_wscale) < 14) {
               space >>= 1;
               (*rcv_wscale)++;
       }
}

根据上述算法,可以看到在 tcp_rmem 的最大值为 4194304 的情况下,窗口扩大选项的值为:7。

ref:access.redhat.com/solutions/2...

窗口扩大选项的具体值是如何影响接收窗口的最大值的?

还是上述例子,发送端的 wscale 为 5,那么 rwnd 的值为: ((2 ^ 16) x (2 ^ 5)) / 1024 = 2048 kb 。 另外需要注意的是,虽然 TCP option 规定 length 占用 1 个字节,理论上来说可以设置的最大值为 2 ^ 8 = 255 ,但是实际能够设置的范围为:0 ~ 14 ,这意味着 rwnd 的最大值为: (2 ^ 16) x ( 2 ^ 14) = 1,073,741,824 byte,也就是 1GB 。此外,rwnd 也不能超过 sequence no , 也就是 2 ^ 32 (seq 在 TCP header 中占用4个字节)。

什么样的网络状况下,能够完全利用最大窗口大小?

如果想要完全利用 1GB 的窗口大小,典型的网络环境为:10Gbit/s ,RTT = 800ms,此时的 BDP = (10 ^ 9) * 0.8 \ 8 = 100000000 ,这种情况下,能够打满整个 10 Gbit/s 的带宽。

Zero Window

当滑动窗口降为 0 了之后,发送端就不再发送数据了,因此需要有一种机制来让接收端重新将此时的窗口大小告诉发送端,以便可以重新发送数据。TCP 采用了一种 Zero Window Probe 技术,简称 ZWP 。当发送端的窗口变为 0 之后,会发 ZWP 包给接收方,接收方收到 ZWP 包了之后,会发送 ack 的包给发送端,其中就包含了此时的接收窗口大小。一般来说发送端会重试 3 次发送 ZWP 包,如果 3 次之后依然没有收到接收端返回的 ACK 包, 则发送端会认为数据已经发送完毕,并发送 RST 包给将链接断开。

糊涂窗口综合症 (Silly Window Syndrome)

Silly Window Syndrome (简称 SWS)缺陷是指,交换数据段的大小是一些较小的数据段,由于每个报文段中有用数据相对于头部信息的比例较小,因此耗费的资源比较多,传输效率也比较低。

更为通俗的解释是:接收方太忙了,来不及取走 Receive Window 里的数据,那么,就会导致发送方的 window 越来越小。到最后,如果接收方能够空出几个字节,并告诉发送方现在有几个字节的 window ,这时发送方就会根据 Receive Window 来发送这几个字节。显然,为了几个字节的数据包就让发送端进行发送,效率是非常低的。

要避免 SWS 问题,需要在发送端或接收端实现相关的规则:

  • 对于接收端来说,不应该通告较小的窗口值。RFC 1122 描述的接收算法中,在窗口增加至一个 MSS 之前,或增长至接收端缓冲空间的一半之前( 取 MSS 和 缓冲空间中较小的那个),不能通告比当前窗口(可能为0)更大的窗口值。

  • 对于发送端来说,不应该发送小的报文段,需由 Nagle 算法控制何时发送。需要满足以下任意条件之一,才能够传输报文段:

    • 达到 MSS 长度的报文段可以发送。
    • 数据端长度 >= 接收端通告过的最大窗口值的一半。
    • 禁用了 Nagle 算法。
    • 没有未经 ACK 的正在进行传输的数据。

大容量缓存与自动调优

通过前面介绍的知识可以看到,使用较小内核接收缓冲的 TCP 应用的吞吐性能会比较差。同时,必须发送端和接收端都指定一个较大的缓冲,才能够使性能得到提升。

在一些较新的 Windows( Vista/7 ) 版本和 Linux 中,支持接收窗口自动调优( Receive Window Auto-Tuning )。有了自动调优,该连接的在传数据值需要不断被估算,在剩余的内核缓存空间足够的情况下,通告窗口值不能小于这个值。这种方法能够让 TCP 传输达到最大可用的吞吐率,而不必提前在发送端或接收端设置过大的缓存。

在 Windows 系统中,默认自动设置接收端缓冲的大小。当然也可以通过 netsh 命令更改默认值:

vbnet 复制代码
netsh interface tcp set heuristics disabled
netsh interface tcp set global autotuninglevel=X

autotuninglevel 的值可以设置为,不同的设置会影响接收窗口的自动选择。

  • disabled
  • highlyrestricted
  • restricted
  • normal
  • experimental

在 Linux 2.4 以及之后的版本,支持发送端的自动调优。而从 2.6.7 的版本之后,能够支持发送端和接收端。需要注意的是,自动调优受限于内核缓冲的大小。如下的内核变量控制了发送端和接收端的内核最大缓冲大小:

yaml 复制代码
sysctl -n net.ipv4.tcp_rmem
4096    87380   4194304

sysctl -n net.ipv4.tcp_wmem
4096    16384   4194304

从最左边到最右边的数值依次表示:最小值、默认值、最大值。

TCP 拥塞控制

TCP 拥塞控制是为了避免网络因为大规模的通信负载而出现瘫痪,其控制的原理是根据当前网络是否进入到了拥塞状态而出现路由器丢包的情况,减缓 TCP 传输的速度。

什么是拥塞?

路由器因为无法处理高速率到达的流量而被迫丢弃数据信息的现象被称为拥塞

如何减缓 TCP 发送?

在 TCP 中,有一个反映网络的传输能力的变量被称为拥塞窗口( congestion window ),记作 cwnd 。发送端的实际可用窗口 W 为 接收端通告窗口 rwnd 与 拥塞窗口 cwnd 较小的那个值,即:

ini 复制代码
W = min(cwnd, rwnd)

在传输的过程中,cwnd, rwnd 都是一个根据网络情况动态调整的值。我们期望其接近带宽延迟积( Bandwidth Delay Product , BDP ),也被称作最佳窗口大小。

BDP

指的是一个数据链路的能力(每秒比特)与 RTT 的乘积。其结果是以比特(或字节)为单位的一个数据总量,等同在任何特定时间该网络线路上的最大数据量------已发送但尚未确认的数据。

一个具有大带宽时延乘积的网络也被称之为长胖网络(long fat network,简写为LFN,经常发音为"elephen")。根据RFC 1072中的定义,如果一个网络的带宽时延乘积显著大于105比特(12500字节),该网络被认为是长胖网络。

如下是一些常见的 BDP 示例:

Gigabit LAN Interface with 1 ms round trip time:

ini 复制代码
1000000000 bits * 0.001 seconds = bandwidth delay product 1000000 bits (or 125000 bytes)

FastEthernet LAN Interface with 1 ms round trip time:

ini 复制代码
100000000 bits * 0.001 seconds = bandwidth delay product 100000 bits (or 12500 bytes)

ADSL2 20 Mbit with 50 ms round trip time:

ini 复制代码
20000000 bits * 0.05 seconds = bandwidth delay product 1000000 bits (or 125000 bytes)

可以通过 iperf 工具来测试当前网络的大致带宽:

在服务器端使用 iperf 开启 5001 端口

iperf -s

然后再在客户端使用 iperf 进行网络的测试,可以看到如下的测试中,带宽为 16.4 Mbits/s 。

css 复制代码
iperf -c target-ip

[ ID] Interval       Transfer     Bandwidth
[  4] local xx.xx.xx.xx port 5001 connected with xx.xx.xx.xx port 60484
[  4]  0.0-10.1 sec  19.6 MBytes  16.4 Mbits/sec

一些经典拥塞控制算法

慢启动 ( Slow Start )

当一个新的 TCP 连接建立或检测到由重传超时( RTO )导致的丢包时,需要执行慢启动。另外,TCP 发送端长时间处于空闲状态也可能调用慢启动算法。

TCP 以发送一定数据的数据段开始慢启动(发生在 SYN 交换之后),被称为初始窗口( Initial Window, IW )。 IW 的值初始设为一个 SMSS (发送方的最大段大小,第一个字母 S 表示 Sender),但是在 RFC 5681 中被设为一个比 SMSS 较大的值,其计算公司如下:

  • IW = 2 * (SMSS) 且小于等于 2 个数据段(当 SMSS > 2190 字节)
  • IW = 3 * (SMSS) 且小于等于 3 个数据段(当 2190 >= SMSS > 1095 字节)
  • IW = 4 * (SMSS) 且小于等于 4 个数据段

上述 IW 的计算方式可能使得初始窗口为几个数据包大小(比如 2倍、3倍、4倍 SMSS)。在这里我们只讨论 IW = 1 SMSS 的情况,即初始的可用窗口也为 1 SMSS 。假设没有出现丢包情况并且每一个数据包都有相应的 ACK ,第一个数据段的 ACK 到达,说明可发送一个新的数据段。每接收到一个正确的 ACK (新接收的 ACK 号大于之前的 ACK 号)影响,慢启动算法会以 min(N, SMSS) 来增加 cwnd 的值。 这里的 N 是指在未经确认的传输数据中,正确 ACK 的字节数。

因此,在接收到一个数据段的 ACK 后,通常 cwnd 值会增加到2,之后重复此步骤,依次增加至4,8,按照指数级别进行增长。

因此,总结一下上述过程,可以归纳为:

1)连接建好的开始先初始化cwnd = 1,表明可以传一个MSS大小的数据。 2)每当收到一个ACK,cwnd++; 呈线性上升。 3)每当过了一个RTT,cwnd = cwnd*2; 呈指数让升。 4)ssthresh(slow start threshold)是一个上限,当 cwnd >= ssthresh 时,就会进入"拥塞避免算法"。

拥塞避免算法 - Congestion Avoidance

当 cwnd >= ssthresh 时,就会进入到拥塞避免算法。一般的场景下,ssthresh 的值为65535,单位是字节,当 cwnd 达到这个值时候,算法为:

  • 收到一个 ACK 时, cwnd = cwnd + 1 / cwnd
  • 当每过一个 RTT 时, cwnd = cwnd + 1

上述算法呈线性增长,这样就可以避免增长过快导致网络拥塞,慢慢地增加调整到一个网络的最佳值。

拥塞状态时的算法

有两种主要的算法:

  • TCP Tahoe:其实现与 RTO 超时重传算法一致。

  • TCP Reno:

    • cwnd = cwnd / 2
    • sshthresh = cwnd
    • 进入快速恢复算法( Fast Recovery )

可以看到 RTO 超时后,sshthresh 会变成 cwnd 的一半,这意味着,如果 cwnd <= sshthresh 时出现的丢包,那么 TCP 的 sshthresh 就会减了一半,然后等 cwnd 又很快地以指数级增涨爬到这个地方时,就会成慢慢的线性增涨。

Fast Recovery

TCP Reno 算法
  • cwnd = sshthresh + 3 * MSS (3的意思是确认有3个数据包被收到了)
  • 重传Duplicated ACKs指定的数据包
  • 如果再收到 duplicated Acks,那么cwnd = cwnd +1
  • 如果收到了新的Ack,那么,cwnd = sshthresh ,然后就进入了拥塞避免的算法了。

TCP Reno存在的问题:依赖于3个重复的 Acks。注意,3个重复的 Acks 并不代表只丢了一个数据包,很有可能是丢了好多包。但这个算法只会重传一个,而剩下的那些包只能等到 RTO 超时,于是,进入了恶梦模式------超时一个窗口就减半一下,多个超时会超成TCP的传输速度呈级数下降,而且也不会触发 Fast Recovery算法了。

TCP New Reno 算法

该算法是为了解决 TCP Reno 算法的问题:

  • 当 Sender 这边收到了3个 Duplicated Acks ,进入 Fast Retransimit 模式,开发重传重复 Acks 指示的那个包。如果只有这一个包丢了,那么,重传这个包后回来的 Ack 会把整个已经被sender传输出去的数据 ack 回来。如果没有的话,说明有多个包丢了。我们叫这个 ACK 为 Partial ACK 。

  • 一旦 Sender 这边发现了 Partial ACK 出现,那么,sender 就可以推理出来有多个包被丢了,于是乎继续重传 sliding window 里未被 ack 的第一个包。直到再也收不到了 Partial Ack ,才真正结束 Fast Recovery 这个过程 我们可以看到,这个" Fast Recovery 的变更"是一个非常激进的算法法,因为它同时延长了 Fast Retransmit 和 Fast Recovery 的过程。

FACK 算法

对基于 Reno 和 New Reno 算法,当快速重传结束后 cwnd 值减少,在 TCP 发送新数据之前至少可以接收一般已发送数据返回的 ACK 。这和检测到丢包后立即将拥塞窗口值减半相一致。这样 TCP 发送端在前一半的 RTT 时间内处于等待状态,在后一半 RTT 才能发送新数据,这是我们不愿看到的。

在丢包后,为了避免出现等待空闲而又不违背将拥塞窗口减半的做法,新的算法提出了转发确认的策略( forward Acknowledged, FACK )。具体算法为:

  • 首先把 SACK 中最大的 Sequence Number 保存在 snd.fack 这个变量中, snd.fack 的更新由 ack 承载,如果网络一切安好则和 snd.una 一样( snd.una 就是还没有收到 ack 的地方,也就是前面 sliding window 里的 category 2 的第一个地方)。
  • 然后定义一个 awnd = snd.nxt -- snd.fack( snd.nxt 指向发送端 sliding window 中正在要被发送的地方------前面 sliding windows 图示的 category 3 第一个位置)。awnd = actual quantity of data outstanding in the network 。
  • 如果需要重传数据,awnd = snd.nxt -- snd.fack + retran_data,也就是说,awnd 是传出去的数据 + 重传的数据。
  • 最后触发 Fast Recovery 的条件是:
scss 复制代码
(((snd.fack -- snd.una) > (3*MSS)) || (dupacks == 3))

这样一来,就不需要等到3个 duplicated acks 才重传,而是只要 sack 中的最大的一个数据和 ack 的数据比较长了(3个 MSS),那就触发重传。在整个重传过程中 cwnd 不变。直到当第一次丢包的 snd.nxt<=snd.una (也就是重传的数据都被确认了)。

我们可以看到如果没有 FACK 在,那么在丢包比较多的情况下,原来保守的算法会低估了需要使用的 window 的大小,而需要几个 RTT 的时间才会完成恢复,而 FACK 会比较激进地来干这事。 但是,FACK如果在一个网络包会被 reordering 的网络里会有很大的问题。

其它拥塞算法

Vegas

Vegas 算法试图在维持较好吞吐量的同时避免拥塞。它通过观察 RTT 来预测网络拥塞。当 RTT 增大时, Vegas 认为网络正在发生拥塞,于是线性降低发送速率。利用 RTT 判断拥塞使得 Vegas 算法有较高的效率,但也导致采用 Vegas 的连接有较差的带宽竞争力。

BIC-TCP

BIC-TCP 算法的主要目的在于,即使在拥塞窗口非常大的情况下也能满足线性RTT公平性。使用二分搜索增大加法增大两种算法探测饱和点,通过最大值探测机制实现。Linux 2.6.8 至 2.6.17 内核版本中默认开启该算法。

CUBIC

CUBIC 算法改进了 BIC-TCP 算法中在某些情况下(低速网络)增长过快的不足,并对窗口增长机制进行了简化。它通过一个三次函数来控制窗口的增长。除此之外 CUBIC 支持TCP友好策略,确保在低速网络中CUBIC的友好性。从Linux 2.6.18 内核版本开始 CUBIC 成为了 Linux 默认的 TCP 拥塞控制算法。

Linux 系统下查看当前所使用的拥塞算法

sysctl -n net.ipv4.tcp_congestion_control
cubic

与时俱进的 BBR 算法

之所以将该算法单独拿出来讲,是因为该算法是由 google 在 2016 年下半年公开的一种开源拥塞控制算法,目前已经包含在了 Linux 4.9 内核版本中。该算法一出来就引起了业界的广泛关注,并在实际的测试中取得了较好的成绩,例如知乎上的一次讨论:

中国科大在 LUG HTTP 代理服务器上部署了 Linux 4.9 的 TCP BBR 拥塞控制算法。从科大的移动出口到新加坡 DigitalOcean 的实测下载速度从 647 KB/s 提高到了 22.1 MB/s。

TCP BBR 致力于解决两个问题:

  • 在有一定丢包率的网络链路上充分利用带宽。非常适合高延迟、高带宽的网络链路。
  • 降低网络链路上的 buffer 占有率,从而降低延迟。非常适合慢速接入网络的用户。

与传统拥塞算法不太相同的是,BBR 不再关注丢包作为拥塞信号,而是通过交替测量带宽和延迟,用一段时间内的带宽极大值和延迟极小值作为估计值的乘积作为窗口估计值,因此 BBR 可以更充分的利用带宽。目前对 BBR 的评价有褒有贬,有人说时黑科技,有人说其抢占带宽不道德,有人说这是 TCP 发展的一大进步也是拥塞控制的未来发展方向,还有人说大范围部署 BBR 将是一场灾难。

更多的讨论可以参考: www.zhihu.com/question/53...

另外,如果你想要在低于 4.9 的 Linux 上进行尝鲜,可以参考这2篇文章: www.vultr.com/docs/how-to... github.com/iMeiji/shad...

What's difference between flow control and congestion control


如下的这篇文章介绍得非常详细,值得一看。

techdifferences.com/difference-...

ref


en.wikipedia.org/wiki/TCP_wi... www.ibm.com/developerwo... sandilands.info/sgordon/imp...

相关推荐
哈哈哈哈cwl1 小时前
还搞不明白浏览器缓存?
后端·node.js·浏览器
橘子海全栈攻城狮2 小时前
【源码+文档+调试讲解】基于Android的固定资产借用管理平台
android·java·spring boot·后端·python·美食
Stark、3 小时前
《数据结构》--队列【各种实现,算法推荐】
开发语言·数据结构·c++·后端·算法
程序员大金3 小时前
基于SpringBoot+Vue+MySQL的中医院问诊系统
java·javascript·vue.js·spring boot·后端·mysql·tomcat
AskHarries3 小时前
Spring Boot集成RBloomFilter快速入门Demo
java·spring boot·后端
谢尔登3 小时前
使用代理服务器进行跨域可能引发的问题
前端·网络协议·http
或许未必不过3 小时前
TCP TIME_WAIT 详解
后端·网络协议
LaLaLa_OvO4 小时前
springboot 用request.setAttribute 传值到控制层
spring boot·后端