蛮荆

TCP 可靠传输实现原理 - 3.拥塞控制

2018-12-30

概述

TCP 实现可靠传输层的核心有三点:

  1. 确认与重传 (已经可以满足 “可靠性”,但是可能存在性能问题)
  2. 滑动窗口 (也就是流量控制,为了提高吞吐量,充分利用链路带宽,避免发送方发的太慢)
  3. 拥塞控制 (防止网络链路过载造成丢包,避免发送方发的太快)

滑动窗口和拥塞控制相互制约,使发送方可以从网络链路的全局角度来自动调整发送速率,从这个角度来看,TCP 对于整个网络的意义已经超过 “传输层”。

本文主要讲解三个核心中的第三点: 拥塞控制

滑动窗口主要关注发送方到接收方的流量控制

拥塞控制更多地关注整个网络 (链路) 层面的流量控制

拥塞控制的视角更为全面,会对整个网络链路中的所有主机、路由器,以及降低网络传输性能的有关因素进行综合考量。

前置知识点

在讲解 TCP 的拥塞控制之前,先来复习几个基本知识点。

1. 丢包

丢包一般分两种情况:

  • 网络质量导致的零星丢包
  • 网络拥塞导致的大量丢包

丢包会引起重传,重传会加剧网络负载,网络负载严重后,丢包会更严重,进入 “恶性循环”。

如果网络中所有发送方和接收方都进入 “恶性循环”,就会形成 “网络丢包” 风暴,直至拖垮整个网络。

所以 TCP 需要站在整个网络层面的角度,对传输的整个过程链路进行规划和把握,也就是本文的主题 拥塞控制


拥塞控制

TCP 的拥塞控制的核心思想是根据网络状况动态调整拥塞窗口(cwnd)的大小,通过反馈机制(如丢包和延迟)检测网络拥塞,并相应地调整数据发送速率,主要由 4 个方面组成:

  1. 慢启动
  2. 拥塞避免
  3. 快速重传 (拥塞发生后)
  4. 快速恢复

拥塞控制状态机

目前有非常多的 TCP 的拥塞控制协议,例如:

  • 基于丢包 的拥塞控制:采取慢启动方式,逐渐增大拥塞窗口,出现丢包时减小拥塞窗口,如 Reno、Cubic、NewReno
  • 基于时延 的拥塞控制:通过延迟和带宽估算来进行拥塞控制,延迟增大时减小拥塞窗口,延时减小时增大拥塞窗口,如 Vegas、FastTCP
  • 基于链路容量 的拥塞控制:实时测量网络带宽和时延,根据网络传输中的 报文总量 和带宽时延的乘积 进行拥塞控制,如 BBR (Google 开发的一种算法,通过估算瓶颈带宽和往返时延来控制拥塞)
  • 基于学习的 拥塞控制:没有特定的拥塞信号,通过分析不同网络条件下的传输行为,借助评价函数,基于训练数据,使用机器学习的方法生成最优的拥塞控制规则,如 Remy

本文不对上述协议展开讲解,感兴趣的读者可以查阅相关资料。

1. 慢启动

发送方和接收方三次握手后连接建立后,是不是应该直接开始快速发送数据包呢?TCP 认为,虽然单个连接上快速发送大量数据包是 OK 的 (毕竟有滑动窗口地址负责流量控制),但是如果网络中的所有连接都这么做的话,避免会导致流量高峰,进而形成网络拥塞。所以,刚开始的时候应该慢点发送数据,然后根据网络状态不断进行调整 (网络负载较小时就多发送,网络负载较小时就少发送)。

Typically, ssthresh starts at 65535 bytes.

  • 初始状态下,cwnd 设置为一个较小的值(例如 2 个 MSS)
  • 每收到一个 Ack,cwnd 加 1 (cwnd += 1)
  • 所以当前的 cwnd, 每经过一个 RTT, cwnd 翻倍 (cwnd *= 2)
  • 直到 cwnd >= ssthresh (预定义的慢启动阈值) 或者检测到丢包,cwnd 增长结束 (进入拥塞避免阶段)

因为是指数级增长,所以 “慢启动” 其实并不慢。

图片来源: https://coolshell.cn/articles/11609.html

Linux 3.0 之后,采用了 Google 的建议,将 cwnd 初始化为 10 个 MSS。

https://github.com/torvalds/linux/blob/02f8c6aee8df3cdc935e9bdd4f2d020306035dbe/include/net/tcp.h#L200

# Linux 3.0 默认 cwnd 大小

#define TCP_INIT_CWND		10

2. 拥塞避免

不同拥塞控制算法的核心差异主要在 拥塞避免 阶段。

拥塞避免 并非指完全能够避免网络拥塞,而是指在拥塞避免阶段,将拥塞窗口控制为按线性规律增长,使网络尽可能避免出现拥塞。

Typically, ssthresh starts at 65535 bytes.

慢启动持续一段时间后,拥塞窗口变为一个较大的值,此时距离 网络拥塞点 也比较接近了,所以需要降缓速度慢下来。

图片来源: Wireshark 网络分析就是这么简单

cwnd >= ssthresh 时,进入拥塞避免阶段,此时 cwnd 的增长方式变化为:

  • 每收到一个 Ack,cwnd = cwnd + 1/cwnd (使用 cwnd 自己的倒数作为增长值,cwnd 越大,增长值越小,反之 cwnd 越小,增长值越大)
  • 每经过一个 RTT, cwnd = cwnd + 1

从算法的变化过程可以看出,进入拥塞避免后,cwnd 由指数型增长变化为线性增长。

利用 Wireshark 估算网络拥塞点

网络拥塞的特征是连续丢包,丢包会引起重传,Wireshark 能够标识出重传的数据包,因此可以先从 Wireshark 中找到一连串重传包中的第 1 个,再根据该重传包的 Seq 值找到其原始包,最后计算该原始包发送时刻的在途字节数。由于网络拥塞就是在该原始数据包发出去的时刻发生的,所以这个在途字节数就大致代表了 网络拥塞点 的大小。

可以根据这个结果,多次采样进行参考,然后选定一个合适的值作为该连接的拥塞点。例如根据采样结果取一个偏小的 (类似接口响应的 95 分位) 数值。

3. 拥塞发生

传统 (基于丢包) 的 TCP 流量控制认为,一旦发生丢包,就是网络链路发生了拥塞,所以发送方会急剧地减小发送窗口,甚至进入短暂的等待状态(超时重传)。 所以,1% 的丢包率并不只是降低 1% 的传输性能,而是可能降低 50% 甚至更多 (取决于具体的 TCP 实现)。

图片来源: Wireshark 网络分析就是这么简单

拥塞发生之后,又有几种常用的算法来解决:

1. TCP Tahoe 算法

该算法和超时重传机制一样,工作过程如下:

  • 修改 sshthresh = cwnd / 2
  • 修改 cwnd = 1
  • 进入慢启动过程

图片来源: Wireshark 网络分析就是这么简单

因为拥塞窗口大小直接变为 1,然后重新开始慢启动,所以这是一种极为保守的算法。

2. TCP Reno 算法

该算法工作过程如下:

  • 修改 cwnd = cwnd / 2
  • 修改 sshthresh = cwnd
  • 进入快速恢复过程 (下文中会提到)

图片来源: Wireshark 网络分析就是这么简单

3. TCP Cubic 算法

Cubic 是一种改进的拥塞控制算法 (Linux 2.6.18 之后默认使用),是基于丢包机制的拥塞控制算法中表现最好的。

Cubic 和 Reno 算法一样,发生拥塞后也是采用加法线性增加,但是每次不是加 1, 而是根据一个特定的公式来决定窗口增加的大小,从而到达更快地收敛 cwnd。

如图所示,相比于 Reno 算法发生拥塞后的 “大起大落”,Cubic 算法要相对 “顺滑” 一些。

由此可见,没有网络拥塞时,发送窗口越大,性能越好,此时应该尽量增大接收窗口 (例如 LAN 环境中)。如果经常发生拥塞,那限制发送窗口反而能提高性能,因为丢包 + 重传是绝对的性能杀手。

4. 快速恢复

在重传数据包之后,发送方不会立即进入慢启动阶段,而是通过调整 cwnd 来快速恢复到丢包前的传输速率。

拥塞发生之后,如果发生了快速重传 (收到 3 个及以上 Dup Ack), 说明此时网络拥塞并没有很严重,所以只要保证接下来降低发送速率就可以了,完全不需要像超时重传一样那么保守,此时就可以进入到快速恢复阶段了。

进入 快速恢复 阶段之前,cwnd 和 sshthresh 已经被修改过了,例如 TCP Reno 算法修改如下:

cwnd = cwnd / 2
sshthresh = cwnd

快速恢复 (Fast Recovery) 算法如下:

  • cwnd = sshthresh + 3 * MSS(收到 3 个 Ack)
  • 重传 Dup Ack 指定的数据包
  • 再次收到 Dup Ack,修改 cwnd = cwnd + 1
  • 再次 Ack,修改 cwnd = sshthresh, 进入拥塞避免阶段

不足 (局限性)

相比保守的 RTO 超时重传,快速恢复调整 cwnd 和 sshthresh 更加符合真实的网络链路场景,但是该算法的不足在于: 严重依赖引起快速重传的 Dup Ack, 引发快速重传至少需要 3 个 Dup Ack 包,但是 3 个 Dup Ack 并不意味着只丢了 3 个数据包 (很可能是多个数据包),但是快速重传只会重传一个,其他丢失的数据包依然只能等待 RTO 超时重传,于是进入一个恶性循环:

  • 超时一个数据包,sshthresh 就锐减一半
  • 超时多个数据包,sshthresh 就会减少到 1, 并且不会触发快速恢复算法了

慢启动和快速恢复到底指什么?

慢启动和快速恢复针对的目标是同一个: cwnd 初始值大小,慢启动设置 cwnd 初始值为 1, 快速恢复设置 cwnd 初始值为 ssthresh。


小结

下面这张图,将慢启动、拥塞控制、拥塞发生、快速重传和超时重传各个不同的状态值,很好地展示了出来。


扩展阅读

转载申请

本作品采用 知识共享署名 4.0 国际许可协议 进行许可,转载时请注明原文链接,图片在使用时请保留全部内容,商业转载请联系作者获得授权。