0%

TCP - 拥塞控制

在使用TCP协议进行数据传输的时候,我们考虑一下如下情况:

  • A通过TCP协议向B发送数据,但是B对数据的处理性能比较慢,导致B的TCP接收缓冲区被填满了,无法再对数据进行ACK应答,此时A则认为之后的数据都发送失败,反而继续发送无法ACK应答的数据,形成一个恶性循环;
  • A通过TCP协议向B发送数据,数据会途径网络设备C,假设网络设备C的数据转发性能已经达到了极限,那么数据会在网络设备C上堆积,导致网络拥堵。

第一个问题暴露了发送端和接收端自身的数据收发能力;第二个问题暴露了TCP协议对整个网络的影响,并且第一个问题会加剧第二个问题的产生。

TCP是网络中的基础传输协议,如果协议本身没能很好的控制上面的两个问题,则反而会给整个网络带来灾难,因此TCP协议中,需要定义相关的策略,来防止数据传输导致的网络拥塞。

滑动窗口

滑动窗口的设计,主要是用来解决上述第一个问题,在TCP数据头中,有一个window字段,占用了2个字节,这个字段就是接收端用来告诉发送端,我的TCP缓冲区现在还能接收多少字节的数据,而发送端会依据这个字段,来设置发送端滑动窗口的大小。

window字段取值

TCP缓冲区中,相关的数据结构如下:

1

发送端:

  • LastByteAcked:指向数据已经发送出去,并且收到了ACK的位置;
  • LastByteSent:指向数据已经发送出去了,但是还没收到ACK的位置;
  • LastByteWritten:指向上层应用正在写入数据的位置。

接收端:

  • LastByteRead:指向上层应用正在读取TCP缓冲区中的数据的位置;
  • NextByteExpected:指向收到的连续的包的最后一个字节的位置;
  • LastByteRcved:指向收到的所有的包的最后一个字节的位置,与NextByteExpected之间,可能有些数据包还没收到。

接收端会计算当前TCP缓冲区还能接受的字节数量(MaxRcvBuffer – LastByteRcvd – 1),然后设置到ACK包中的window字段里,发送端会以这个作为参考,计算下次发送的数据大小。

发送端滑动窗口

我们来举个例子,假设下图为发送端的滑动窗口:

2

TCP连接建立的时候,双方都会告诉对方自己的接收窗口大小(假设为20),对方会根据这个值来构造数据发送的滑动窗口的大小,其中:

  • 左侧绿色部分数据为已经发送过的数据,并且成功的接收到了ACK回复;
  • 中间蓝色的数据端,为发送端的数据发送窗口;
  • 中间蓝色部分数据为滑动窗口中,待发送的数据;
  • 右侧黄色部分数据为不允许发送的窗口。

假设现在发送端将24-29数据块发送出去:

3

数据发送之后,滑动窗口并不能移动,需要等待数据的ACK回复,但是处于滑动窗口内的待发送数据,依然可以继续发送。

假设数据并不没有按发送顺序收到ACK,并且在等待过程中,陆续又发送了数据30-32:

4

上图中,蓝色数据25,26,27,29为对端已经收到了的数据,由于窗口首位的数据24的ACK并没有收到,因此窗口依然不能移动,继续等待,同时窗口中的其他数据也都被发送出去了:

5

此时滑动窗口中已经没有可发送的数据了,但是由于数据24的ACK一直没有收到,因此现在发送端现在不会发送任何数据给接收端,直到收到数据24的ACK:

6

数据24的ACK收到了,但是数据28的ACK依然没有收到,但是滑动窗口已经可以向右滑动4个位置了,此时原先状态为不允许发送的数据44-47被纳入到窗口内,变为可发送状态。

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

在七层网络协议的数据链路层中有个概念,称之为最大传输单元(MTU,Maximum Transmission Unit),用来通知对方所能接受数据服务单元的最大尺寸,是数据包的最大长度。大于MTU的包有两种结局,一种是直接被丢弃,另一种是会被重新分块打包发送。

对于以太网来说,MTU一般是1500字节,TCP+IP数据头占用40个字节,因此真正的数据传输最大为1460个字节,也称之为MSS(Max Segment Size),如果每次发送的数据包可以塞满MTU,那就相当于你可以利用整个带宽;相反,如果每次发送的数据包都很少很少的字节,那就意味着浪费了带宽。

现在假设接收端处理数据的能力很弱,TCP缓冲区很快就被占满了,然后应用程序处理了几十个字节的数据之后,TCP缓冲区就腾出来几十个字节的空间,然后告诉发送端,发送端就发送几十个字节的数据过来;当接收端的TCP缓冲区再次空闲出来很小的空闲空间时,再次重复上述步骤,甚至有的时候发送的数据还没有数据头大。这就好比一列16节车厢的高铁,只载了3,5个乘客,严重浪费了资源。上述这种情况被称之为Silly Window Syndrome,解决办法倒是简单:

  • 接收端

    设置一个最小值,如果TCP缓冲区的空闲空间不足这个最小值,则在ACK的数据包中,回复可接受数据长度为0给接收端,这样发送端就不会发送数据过来,直到空闲空间大于最小值,或者是空闲空间大于缓冲区的一半,则正常回复。

  • 发送端

    如果是因为发送端待发送的数据量很小,那么就选择延时处理,要么等到待发送数据大小>=MSS,或者是收到之前发送数据的ACK之后才会发数据,否则就一直积累待发送数据数据。

    这个算法默认是打开的,所以对于一些需要小包场景的程序——比如像telnet或ssh这样的交互性比较强的程序,你需要关闭这个算法。你可以在Socket设置TCP_NODELAY选项来关闭这个算法(关闭Nagle算法没有全局参数,需要根据每个应用自己的特点来关闭)

Zero Window Probe(ZWP)

如果接收端回复的ACK包中,window为0,那么发送端就不会继续发送数据,那么当接收端恢复之后,如何告诉发送端呢?TCP中定义了Zero Window Probe技术:发送端在收到的ACK中,发现窗口变成0后,转而定时发送ZWP包给接收方,接收方会在ZWP包中回复当前的window大小,一般会发送3次,如果3次之后还是0的话,有的TCP实现中,会将TCP连接断开。

拥塞处理

当网络上的延时已经增加的时候,势必会造成更多的数据包超时,导致重传,重传又会进一步导致网络拥堵,形成恶性循环,因此,TCP协议不仅考虑了协议本身的一些流控处理,还有一套拥塞处理机制,根据整个网络的当前情况,来调整数据包的传输策略。

TCP拥塞处理的核心思想是,维护一个称之为拥塞窗口的变量cwnd(Congestion Window),发送端将cwnd作为发送窗口。在数据传输过程中,根据数据包的发送情况,侧面反映网络的拥塞程度,从而不断修改cwnd,如果网络情况良好,就增大cwnd;否则就降低cwnd。

注:下文中,为了讨论方便,将拥塞窗口大小的单位改为数据报的个数,而实际上应当是字节。

慢启动

慢启动的思想是,TCP连接之后,还不知道当前网络是什么情况,于是先采用谨慎一点的方式,以一个较低的频率发送数据包,同时根据RTT的情况来改变发送频率。算法如下:

  • 连接建好之后,初始化拥cwnd为1,表明可以传一个MSS大小的数据;
  • 每当收到一个ACK之后,cwnd++;
  • 当cwnd >= ssthresh(slow start threshold)时,进入“拥塞避免算法”。

当网络情况良好的时候,cwnd的变化为1->2->4->8->…,呈指数上升,所以其实慢启动一点也不慢。

拥塞避免

拥塞避免是指,cwnd不能一直以指数形式无限制的膨胀下去,当cwnd达到足够大的值的时候,应该放缓其增长速度。

具体算法为:当cwnd>=ssthresh时,cwnd进入线性增长阶段,每收到一个ACK时,cwnd加一。

拥塞状态

如果数据发送出现异常情况,例如超时未收到ACK,或者收到重复ACK,则进入拥塞状态,根据异常情况不同,对应方式不同。

  • 超时未收到ACK

    TCP认为发生超时是一件很严重的事情,因此处理方式也比较强烈:

    • 设置sshthresh = cwnd / 2;
    • 设置cwnd = 1;
    • 回到慢启动过程。

    震荡过程如下(图片来源):

    7

    • cwnd以指数上升,直到触及sshthresh,然后变成线性递增;
    • 在红点处发生超时,sshthresh变为当前cwnd的一半,同时cwnd重置为1,回到慢启动状态。
  • 收到重复ACK

    在1988年TCP-Tahoe提出的算法中,这种情况的应对方式与超时未收到ACK一致;1990年TCP-Reno优化了算法,TCP-Reno认为能够收到重复ACK,说明网络状况还可以,只是某个包出现了异常,需要处理但是处理方式不用太强烈,因此引入了快速恢复算法来处理:

    • 设置cwnd = cwnd / 2;
    • 设置sshthresh = cwnd;
    • 进入下面的快速恢复算法。

快速恢复

快速恢复的算法如下(注:cwnd与sshthresh的值已经更新):

  • 设置cwnd = sshthresh + 3,(PS,实际上是3 * MSS);
  • 快速重传重复ACK指定的数据包,即使连续的包之后,被丢失的那个包;
  • 重传之后:
    • 如果再次收到重复ACK,则设置cwnd = cwnd +1;
    • 如果收到了新的ACK,则设置cwnd = sshthresh,之后进入拥塞避免的算法。

震荡过程如下(图片来源):

8

参考



-=全文完=-