你肯定经历过这种场景:明明宽带是千兆的,但下载东西就是跑不动,视频卡成PPT,网页半天打不开。这时候你第一反应往往是“网断了”或者“运营商坑我”,但实际上,网络并没有坏,它只是“堵车”了。
TCP拥塞控制算法,就是网络世界的“交通调度员”。它决定数据能不能发、发多少、怎么发。今天我们就来聊聊这个藏在互联网底层、每天处理亿万次连接、却鲜为人知的“隐形守护者”——TCP拥塞控制。
一、 什么是拥塞控制?为什么我们需要它?
想象一下,你家到公司的路只有一条主干道,这段路一次只能并行通过10辆车。如果你突然派出100辆车同时上路,结果会怎样?
- 堵死:所有车都动不了。
- 喇叭声(丢包):车辆频繁碰撞,导致通信中断。
- 效率骤降:原本10分钟的路程,现在要1小时。
网络世界里的“主干道”就是路由器之间的链路,而“车”就是数据包。当太多数据涌入网络时,路由器缓冲区(Buffer)会被撑爆,导致数据包丢失(丢包)。TCP拥塞控制的目的,就是动态调整发送方的发送速率,避免网络过载,同时在网络空闲时尽可能充分利用带宽。
TCP的拥塞控制主要包含四个算法:
- 慢启动(Slow Start)
- 拥塞避免(Congestion Avoidance)
- 快速重传(Fast Retransmit)
- 快速恢复(Fast Recovery)
二、 慢启动:从零开始,指数级增长
1. 什么是拥塞窗口(cwnd)?
在TCP中,有一个关键变量叫拥塞窗口(congestion window,简称cwnd)。它表示发送方在收到确认(ACK)之前,可以发送的最大数据量。你可以把它理解为“当前允许我在路上跑的车数”。
2. 慢启动的过程
当一个新的TCP连接建立时,发送方不知道网络的承载能力,所以它会从一个小窗口开始,逐步试探网络的带宽。这个过程就叫慢启动。
- 初始值:通常cwnd初始值为1个MSS(Maximum Segment Size,最大分段大小,约1460字节)。
- 增长方式:每收到一个ACK,cwnd就加1个MSS。这意味着,每经过一个RTT(往返时延),cwnd会翻倍。
举个例子:
- 第1个RTT:发送1个分段,收到ACK后,cwnd=2
- 第2个RTT:发送2个分段,收到ACK后,cwnd=4
- 第3个RTT:发送4个分段,收到ACK后,cwnd=8
- 第4个RTT:发送8个分段,收到ACK后,cwnd=16
- …
这种指数级增长的目的是快速找到网络的“可用带宽”。
3. 为什么叫“慢”启动?
其实,慢启动并不慢!它的指数增长非常快,在短时间内就能把cwnd提升到很大。之所以叫“慢”,是因为它需要避免一开始就猛冲导致网络拥堵。就像开车时,先慢慢起步,确认路况后再加速。
4. 慢启动阈值(ssthresh)
慢启动不会一直持续下去。当cwnd达到一个阈值时,就会切换到拥塞避免模式。这个阈值就是慢启动阈值(ssthresh)。
- 如果网络没有丢包,cwnd会持续增长,直到达到ssthresh。
- 一旦cwnd >= ssthresh,就进入拥塞避免阶段。
三、 拥塞避免:线性增长,稳扎稳打
当cwnd超过ssthresh后,TCP进入拥塞避免模式。
1. 增长方式的变化
- 慢启动阶段:每RTT,cwnd 翻倍(指数增长)。
- 拥塞避免阶段:每RTT,cwnd 加1个MSS(线性增长)。
具体实现是:每收到一个ACK,cwnd增加1/cwnd个MSS。这样,每经过一个RTT,cwnd就增加1个MSS。
举个例子:
- 假设cwnd=16,ssthresh=24。
- 第1个RTT:发送16个分段,收到16个ACK,每个ACK让cwnd增加1/16,总共增加1,cwnd=17。
- 第2个RTT:发送17个分段,收到17个ACK,总共增加1,cwnd=18。
- …
- 当cwnd达到24时,就满了。
2. 拥塞避免的目的
拥塞避免阶段的增长更平缓,目的是避免网络突然过载。指数增长太快,容易撞墙;线性增长则更温和,能让网络逐步适应流量。
四、 丢包检测:拥塞的信号
那么,TCP怎么知道网络拥塞了?答案是:丢包。
当路由器缓冲区满时,它会丢弃数据包。发送方发现丢包后,就会认为网络发生了拥塞,并采取措施减小发送速率。
1. 超时重传(Timeout)
如果发送方在一段时间内没有收到任何ACK,就会认为发生了严重拥塞。这时,TCP会:
- 将ssthresh设置为当前cwnd的一半。
- 将cwnd重置为1个MSS。
- 重新进入慢启动阶段。
2. 快速重传(Fast Retransmit)
如果发送方收到3个重复的ACK(Duplicate ACKs),说明有数据包丢失,但网络并没有完全拥塞(因为还有ACK在返回)。这时,TCP会:
- 不等待超时,立即重传丢失的数据包。
- 将ssthresh设置为当前cwnd的一半。
- 将cwnd设置为ssthresh + 3个MSS(快速恢复阶段)。
3. 快速恢复(Fast Recovery)
在快速重传后,TCP进入快速恢复阶段:
- cwnd = ssthresh + 3*MSS(3个MSS是为了补偿已经“在路上的”3个重复ACK对应的数据包)。
- 每收到一个重复的ACK,cwnd增加1个MSS(模拟数据包被接收,允许发送方继续发送新数据)。
- 当收到丢失数据包的ACK时,cwnd设置为ssthresh,进入拥塞避免阶段。
五、 四种算法的协作流程
让我们用一个完整的例子来看看这四种算法是如何协作的。
假设一个TCP连接开始发送数据:
慢启动阶段:
- cwnd从1开始,每RTT翻倍:1, 2, 4, 8, 16, 32, 64…
- 假设ssthresh设为100。
- 当cwnd达到64时,下一个RTT后变成128,超过ssthresh=100,切换到拥塞避免。
拥塞避免阶段:
- cwnd从100开始,每RTT加1:100, 101, 102, 103…
- 假设网络突然拥堵,发生丢包。
丢包处理:
- 情况A:超时
- ssthresh = cwnd / 2 = 50
- cwnd = 1
- 重新进入慢启动。
- 情况B:3个重复ACK
- ssthresh = cwnd / 2 = 50
- cwnd = 50 + 3 = 53(快速恢复)
- 进入快速恢复,每收到一个重复ACK,cwnd加1。
- 当丢失的数据包被确认,cwnd = ssthresh = 50,进入拥塞避免。
- 情况A:超时
六、 现代改进:CUBIC算法
传统的TCP拥塞控制(Reno)在长肥网络(Long Fat Network,LFN)上表现不佳。因为它的线性增长太慢,无法充分利用高带宽、高延迟的链路。
1. CUBIC算法简介
CUBIC是Linux从内核2.6.19开始采用的默认拥塞控制算法。它由Harald Welte和Songun Kim等人开发,旨在更好地利用高带宽延迟积(BDP)的网络。
2. CUBIC的核心思想
CUBIC使用一个三次函数来控制cwnd的增长:
W(t) = C * (t - K)^3 + Wmax
其中:
W(t)是时间t时的窗口大小。C是一个常数,用于控制曲线的陡峭程度。t是自上次拥塞事件以来的时间。K是到达最大窗口Wmax所需的时间。Wmax是上一次拥塞事件前的窗口大小。
3. CUBIC的特点
- 凸函数阶段:当cwnd < Wmax时,CUBIC使用凸函数增长,比Reno的线性增长更快,能快速恢复带宽。
- 凹函数阶段:当cwnd接近Wmax时,CUBIC使用凹函数增长,避免过度激进导致拥塞。
- 长期友好性:CUBIC与Reno在共享链路上是公平的,不会因为增长更快而占满所有带宽。
4. CUBIC vs Reno
| 特性 | Reno | CUBIC |
|---|---|---|
| 增长方式 | 线性(每RTT+1) | 三次函数(凸+凹) |
| 恢复速度 | 慢 | 快 |
| 稳定性 | 较好 | 更好 |
| 适用场景 | 低延迟、低带宽网络 | 高带宽、高延迟网络(如光纤、卫星) |
七、 为什么网络会卡顿?
现在我们可以回答最初的问题了:为什么网络会卡顿?
1. 拥塞窗口太小
当网络发生拥塞时,TCP会减小cwnd。如果网络恢复较慢,或者拥塞事件频繁,cwnd可能一直很小,导致发送速率低,表现就是“卡顿”。
2. 带宽延迟积(BDP)
BDP = 带宽 × 延迟。它表示在网络中“在空中飞行”的数据量。如果cwnd小于BDP,就无法充分利用带宽。
例如:
- 带宽 = 100 Mbps
- 延迟 = 100 ms
- BDP = 100 Mbps × 0.1 s = 10 Mbits = 1.25 MB
如果cwnd只有1 MB,那么发送方发送完1 MB后,需要等待ACK回来才能继续发送,导致带宽利用率只有1 MB / 1.25 MB = 80%。
3. 丢包导致的速率下降
每次丢包,cwnd都会减半(或重置为1)。如果网络不稳定,丢包频繁,cwnd会一直在低位徘徊,无法增长。
4. 缓冲区膨胀(Bufferbloat)
现代路由器使用较大的缓冲区来应对突发流量,但这会导致数据包在缓冲区中排队等待,增加延迟。高延迟又会导致cwnd增长变慢,形成恶性循环。
八、 如何优化带宽利用率?
1. 调整TCP参数
你可以通过调整系统的TCP参数来优化性能。
Linux下的调整
# 查看当前的TCP拥塞控制算法
cat /proc/sys/net/ipv4/tcp_congestion_control
# 设置为CUBIC
echo cubic > /proc/sys/net/ipv4/tcp_congestion_control
# 调整接收窗口自动调优
# 0: 关闭
# 1: 仅接收窗口调优
# 2: 仅发送窗口调优
# 3: 自动调优(推荐)
echo 3 > /proc/sys/net/ipv4/tcp_autotuning_enabled
# 调整接收窗口最大大小(默认可能是256KB,对于高带宽网络太小)
echo 16777216 > /proc/sys/net/ipv4/tcp_rmem
echo 16777216 > /proc/sys/net/ipv4/tcp_wmem
验证调整
# 查看TCP统计信息
netstat -s | grep -i tcp
# 实时监控cwnd
tcpdump -i any -s 0 -n 'tcp[tcpflags] & tcp-ack != 0' | awk '{print $5}' | grep -oP '\d+'
2. 使用QUIC协议
QUIC(Quick UDP Internet Connections)是Google开发的基于UDP的传输协议,已被集成到HTTP/3中。它解决了TCP的一些问题:
- 更快的连接建立:0-RTT握手。
- 更好的多路复用:避免队头阻塞。
- 内置拥塞控制:可以使用BBR等更先进的算法。
- 连接迁移:网络变化时不重连。
3. BBR拥塞控制算法
BBR(Bottleneck Bandwidth and Round-trip propagation time)是Google开发的另一种拥塞控制算法,它不依赖丢包来检测拥塞,而是主动测量网络的瓶颈带宽和RTT。
# 设置BBR为拥塞控制算法
echo bbr > /proc/sys/net/ipv4/tcp_congestion_control
# 启用BBR
modprobe tcp_bbr
BBR的优势:
- 在高带宽、高延迟网络(如卫星、5G)上表现更好。
- 减少缓冲区膨胀。
- 更稳定的吞吐量。
4. 应用层优化
- 使用CDN:减少延迟,降低BDP。
- 内容压缩:减少数据量。
- 分块传输:将大文件分成小块,避免长时间阻塞。
九、 实际案例:为什么我的下载速度上不去?
假设你有一个1 Gbps的宽带连接,延迟为50 ms。
1. 计算BDP
BDP = 1 Gbps × 0.05 s = 50 Mbits = 6.25 MB
这意味着,你需要一个至少6.25 MB的cwnd才能充分利用带宽。
2. 慢启动阶段
- 初始cwnd = 1 MSS ≈ 1.46 KB
- 每RTT翻倍:1.46 KB, 2.92 KB, 5.84 KB, 11.68 KB…
- 要达到6.25 MB,需要多少RTT?
- 6.25 MB = 6.4 × 10^6 KB
- 2^23 ≈ 8.4 × 10^6
- 所以大约需要23个RTT。
- 23个RTT × 50 ms = 1.15秒。
在慢启动阶段,你需要等待1.15秒才能充分利用带宽。如果在这个过程中发生丢包,cwnd会减半,又要重新等待。
3. 拥塞避免阶段
- cwnd = 6.25 MB,ssthresh = 6.25 MB。
- 每RTT增加1 MSS ≈ 1.46 KB。
- 要从6.25 MB增加到7 MB(假设网络有突发流量),需要增加0.75 MB = 768 KB。
- 768 KB / 1.46 KB/RTT ≈ 526 RTT。
- 526 RTT × 50 ms ≈ 26秒。
这意味着,拥塞避免阶段增长非常缓慢。如果网络需要动态调整,TCP可能跟不上变化。
4. 使用CUBIC或BBR
- CUBIC使用三次函数增长,速度更快。
- BBR主动测量带宽,不受丢包影响。
十、 总结
TCP拥塞控制算法是互联网稳定运行的基石。从慢启动的指数增长,到拥塞避免的线性增长,再到快速重传和快速恢复的应急响应,这些算法协同工作,确保数据高效、公平地传输。
然而,传统的TCP拥塞控制在高带宽、高延迟的现代网络上存在局限。CUBIC、BBR等改进算法正在逐步取代传统的Reno算法,提供更好的性能。
下次当你感到网络卡顿时,不妨想想:这背后是无数次的拥塞窗口调整、丢包检测和速率重分配。互联网的世界,远比我们看到的更复杂、更精妙。
参考资料:
- TCP Congestion Control (RFC 5681)
- CUBIC TCP Congestion Control (RFC 8312)
- BBR Congestion Control (Google Research)
- Linux TCP Tuning Guide
