TCP 拥塞控制是怎么工作的?
一句话回答
拥塞控制防止发送方发得太快、把网络塞满。发送方维护一个拥塞窗口 cwnd,实际能发的数据量取 cwnd 和接收窗口 rwnd 中的较小值。经典做法是:慢启动阶段 cwnd 指数增长,到达阈值 ssthresh 后进入拥塞避免,改为线性增长;发生超时就把 ssthresh 设为当前窗口的一半、cwnd 降为 1 个 MSS 重新慢启动;收到 3 个重复 ACK 则快重传、快恢复,窗口只减半。现在 Linux 默认使用 CUBIC,Google 提出的 BBR 则根据测得的带宽和延迟控制发送速率。
详细解析
目的和核心变量
路由器的缓冲区满了就会丢包。如果发送方在丢包后还猛发,网络只会越来越堵。拥塞控制让发送方根据网络状况自己调节速度,它照顾的是整个网络,和照顾接收方的流量控制不同(见 TCP 可靠传输)。
| 变量 | 含义 |
|---|---|
| cwnd | 拥塞窗口,由发送方根据网络状况估算,只存在于发送方内部 |
| ssthresh | 慢启动阈值,cwnd 低于它时用慢启动,达到后改用拥塞避免 |
| 发送窗口 | 取 cwnd 和 rwnd 中的较小值 |
经典的四个机制
下面以经典的 Reno 风格行为为例,不同算法的细节不同。cwnd 以 MSS(最大报文段长度)为单位:
- 慢启动:cwnd 从一个较小的初始值开始,每收到一个 ACK 就加 1 个 MSS,效果是每经过一个 RTT 大约翻倍。"慢"指的是起点低,增长其实是指数级的
- 拥塞避免:cwnd 达到 ssthresh 后,每个 RTT 只增加约 1 个 MSS,线性增长,慢慢试探网络的上限
- 超时重传:说明网络可能严重拥塞。ssthresh 设为当前 cwnd 的一半,cwnd 降为 1 个 MSS(RFC 5681 的规定,和初始窗口是多少无关),重新慢启动
- 快重传和快恢复:收到 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 轮
这道题你掌握了吗?
选一个最接近的状态,没掌握的题会出现在"我的进度 · 待复习"里。
学习记录暂存在本机浏览器。登录后自动同步到账号,换设备也能看到。