TCP 拥塞控制是怎么工作的?

深入原理约 7 分钟读完

一句话回答

拥塞控制防止发送方发得太快、把网络塞满。发送方维护一个拥塞窗口 cwnd,实际能发的数据量取 cwnd 和接收窗口 rwnd 中的较小值。经典做法是:慢启动阶段 cwnd 指数增长,到达阈值 ssthresh 后进入拥塞避免,改为线性增长;发生超时就把 ssthresh 设为当前窗口的一半、cwnd 降为 1 个 MSS 重新慢启动;收到 3 个重复 ACK 则快重传、快恢复,窗口只减半。现在 Linux 默认使用 CUBIC,Google 提出的 BBR 则根据测得的带宽和延迟控制发送速率。

详细解析

目的和核心变量

路由器的缓冲区满了就会丢包。如果发送方在丢包后还猛发,网络只会越来越堵。拥塞控制让发送方根据网络状况自己调节速度,它照顾的是整个网络,和照顾接收方的流量控制不同(见 TCP 可靠传输)。

变量 含义
cwnd 拥塞窗口,由发送方根据网络状况估算,只存在于发送方内部
ssthresh 慢启动阈值,cwnd 低于它时用慢启动,达到后改用拥塞避免
发送窗口 取 cwnd 和 rwnd 中的较小值

经典的四个机制

下面以经典的 Reno 风格行为为例,不同算法的细节不同。cwnd 以 MSS(最大报文段长度)为单位:

  1. 慢启动:cwnd 从一个较小的初始值开始,每收到一个 ACK 就加 1 个 MSS,效果是每经过一个 RTT 大约翻倍。"慢"指的是起点低,增长其实是指数级的
  2. 拥塞避免:cwnd 达到 ssthresh 后,每个 RTT 只增加约 1 个 MSS,线性增长,慢慢试探网络的上限
  3. 超时重传:说明网络可能严重拥塞。ssthresh 设为当前 cwnd 的一半,cwnd 降为 1 个 MSS(RFC 5681 的规定,和初始窗口是多少无关),重新慢启动
  4. 快重传和快恢复:收到 3 个重复 ACK,说明只是个别数据段丢了,后面的数据还能到达,拥塞并不严重。发送方立即重传丢失的段(快重传),把 ssthresh 设为 cwnd 的一半,cwnd 也降到这个值附近(Reno 先设为 ssthresh 加 3 个 MSS,恢复完成后回到 ssthresh),然后直接进入拥塞避免,而不是从头慢启动(快恢复)

一个简化的例子(初始 cwnd = 1、ssthresh = 16,方便观察;现代系统的初始窗口通常是 10):

文本
RTT  cwnd
  1     1  #                     慢启动:每个 RTT 翻倍
  2     2  ##
  3     4  ####
  4     8  ########
  5    16  ################      到达 ssthresh,进入拥塞避免
  6    17  #################     每个 RTT 加 1
  7    18  ##################
  8    19  ###################
  9    20  ####################  发生超时:ssthresh = 10,cwnd = 1
 10     1  #                     重新慢启动
 11     2  ##
 12     4  ####
 13     8  ########
 14    10  ##########            到达新的 ssthresh,进入拥塞避免
 15    11  ###########
 16    12  ############          收到 3 个重复 ACK:ssthresh = 6,cwnd = 6
 17     6  ######                快恢复:从 6 开始线性增长
 18     7  #######
 19     8  ########

线性增长试探带宽、遇到丢包就把窗口降下来,cwnd 整体呈"锯齿形",这就是常说的"加法增大、乘法减小"(AIMD)。

现代算法

算法 拥塞信号 特点
Reno / NewReno 丢包 上面描述的经典行为
CUBIC 丢包 Linux 的默认算法。窗口按三次函数增长:离上次丢包时的窗口大小还远时增长快,接近时放缓,超过后再逐渐加速探测;丢包时窗口降到原来的 0.7 倍,而不是减半。适合高带宽、高延迟的网络
BBR 测量到的带宽和延迟 Google 提出。持续估算瓶颈带宽和最小 RTT,按估算结果控制发送速率,不以丢包作为主要的拥塞信号;在有随机丢包的网络中吞吐更高,排队延迟也更低

Linux 可以通过 net.ipv4.tcp_congestion_control 查看和切换算法。

对前端的启示

  • 新连接起步慢:每个新连接都要经历慢启动,刚开始能发的数据很少。所以连接复用很重要,比如 HTTP 的 keep-alive,以及 HTTP/2 让一个域名通常只用一个连接(见 HTTP 版本区别)
  • 关键 HTML 尽量控制在约 14KB 以内:初始拥塞窗口通常是 10 个 MSS,约 14KB。首屏关键 HTML 控制在这个范围内,可以在第一个往返里送达

面试官可能追问

为什么关键 HTML 建议控制在约 14KB 以内?

新连接的初始拥塞窗口通常是 10 个 MSS,一个 MSS 约 1460 字节,算下来约 14KB。响应不超过这个大小,服务器在第一个往返里就能全部发出,浏览器收到后马上可以开始解析和渲染;超出的部分要等 ACK 回来、窗口增大后才能发,至少多等一个 RTT。这里算的是压缩后实际传输的字节数,响应头也包括在内。它是一个经验值,TLS 握手等因素也会影响实际效果。

BBR 和 CUBIC 有什么区别?
  • CUBIC 基于丢包:发生丢包才认为拥塞、降低窗口。它往往要把路由器的缓冲区填满才会丢包,所以排队延迟偏高;在无线网络这类有随机丢包的环境里,还会把不是拥塞造成的丢包当成拥塞而降速
  • BBR 基于测量:周期性测量瓶颈带宽和最小 RTT,让发送速率贴近链路容量,同时尽量不让数据在缓冲区里排队,所以吞吐高、延迟低
  • BBR 也有争议:和基于丢包的算法共享同一个瓶颈链路时,带宽分配可能不公平,后续版本一直在改进
拥塞控制和流量控制有什么区别?

流量控制防止发送方把接收方淹没,窗口 rwnd 由接收方在 ACK 中通告;拥塞控制防止发送方把网络塞满,窗口 cwnd 由发送方根据丢包、延迟等信号自己估算。发送方实际能发的数据量取两者中的较小值。

易错点

  • "慢启动"并不慢,它是指数增长,"慢"指的是起点低
  • 超时和 3 个重复 ACK 的处理不同:超时时 cwnd 降到 1 个 MSS、重新慢启动;3 个重复 ACK 时只把窗口减半,然后线性增长
  • TCP 首部里的窗口字段是接收窗口;拥塞窗口只存在于发送方内部,不在报文中传输

AI 模拟面试官

用自己的话回答,AI 对照参考答案打分、指出遗漏,再追问,最多 3 轮

登录后就可以和 AI 面试官对练,面试记录也会保存下来。登录

这道题你掌握了吗?

选一个最接近的状态,没掌握的题会出现在"我的进度 · 待复习"里。

学习记录暂存在本机浏览器。登录后自动同步到账号,换设备也能看到。