第四章:TCP与拥塞控制 — 拥塞窗口、慢启动、拥塞避免、快速重传与快速恢复
说到网络拥塞,TCP的拥塞控制机制是绕不开的核心话题。我刚开始做网络运维那会儿,遇到一次跨国传输特别慢的问题,查了半天链路带宽、延迟、丢包率,最后发现是拥塞窗口没调好。嗯,从那以后我就把这块啃得透透的。
说白了,TCP拥塞控制就是一套“试探-退让-恢复”的算法。它不像流量控制那样管接收方能不能收得下,而是管网络能不能扛得住。你想想看,如果大家都拼命发数据,路由器队列一满,丢包就来了,然后重传更多,网络直接崩溃。TCP就是靠下面这几个机制来避免这种惨剧的。
4.1 拥塞窗口(cwnd)
拥塞窗口,英文叫 Congestion Window,简称 cwnd。它是发送方自己维护的一个变量,决定了在没收到确认之前,最多能发多少数据。
实际发送的数据量 = min(拥塞窗口, 接收窗口)。接收窗口是接收方告诉你的,拥塞窗口是你自己猜的。说白了,TCP 既要照顾接收方的消化能力,又要照顾网络的承载能力。
核心要点:拥塞窗口不是固定值,它会根据网络状态动态调整。网络好就涨,网络差就降。
我在项目中遇到过一种情况:两台服务器之间延迟只有 2ms,但吞吐量死活上不去。一查,发现 cwnd 初始值太小,加上应用层发送间隔大,窗口根本来不及增长。后来调整了初始窗口大小,吞吐量直接翻倍。
4.2 慢启动(Slow Start)
慢启动这个名字其实有点误导人。它并不慢,恰恰相反,它增长得非常快。
慢启动的规则很简单:每收到一个 ACK,cwnd 增加 1 个 MSS(最大报文段长度)。
你想想看,这相当于每经过一个 RTT(往返时间),cwnd 就翻倍。1 → 2 → 4 → 8 → 16 ... 这是指数增长。
为什么要这样设计?因为刚开始你不知道网络的容量有多大。从 1 开始试探,如果网络能承受,就加倍;如果出现丢包,就说明到极限了。
我的经验:慢启动阶段最容易出现“突发性丢包”。因为窗口翻倍太快,路由器队列可能瞬间被打满。我曾经在 10Gbps 链路上看到过,慢启动到第 5 个 RTT 时,丢包率从 0% 飙升到 5%。解决方案是启用 TCP 的 pacing( pacing 发送),让数据均匀发出,而不是一次性爆发。
慢启动什么时候结束?有两个条件:
- 遇到丢包(收到重复 ACK 或超时)
- cwnd 达到慢启动阈值(ssthresh)
4.3 拥塞避免(Congestion Avoidance)
当 cwnd 达到 ssthresh 后,TCP 就进入拥塞避免阶段。这个阶段增长就慢多了:每经过一个 RTT,cwnd 增加 1 个 MSS。
用大白话说:慢启动是“大胆试探”,拥塞避免是“小心求证”。
具体实现上,常见的做法是:每收到一个 ACK,cwnd 增加 MSS / cwnd。这样算下来,一个 RTT 内收到的 ACK 数正好等于 cwnd/MSS,所以 cwnd 总共增加 1 个 MSS。
// 拥塞避免的伪代码示例
void on_ack_received() {
if (cwnd < ssthresh) {
// 慢启动阶段
cwnd += MSS;
} else {
// 拥塞避免阶段
cwnd += MSS * MSS / cwnd;
}
}
我记得有一次排查一个长肥网络(高带宽高延迟)的问题,发现吞吐量始终上不去。原因就是拥塞避免阶段增长太慢,从 64KB 涨到 1MB 需要几百个 RTT,而 RTT 是 200ms,算下来要等好几分钟。后来启用了 TCP 窗口缩放选项,才把问题解决。
4.4 快速重传(Fast Retransmit)
丢包了怎么办?传统做法是等超时重传,但超时时间通常几百毫秒甚至几秒,太慢了。
快速重传的思路是:收到 3 个重复的 ACK,就认为报文丢了,不等超时,立即重传。
为什么是 3 个?因为乱序也可能导致重复 ACK,但乱序通常只产生 1-2 个重复 ACK。3 个是一个经验值,能比较好地平衡误判和延迟。
注意:快速重传只适用于单个报文丢失的情况。如果连续丢多个报文,或者发生了超时,还是要走超时重传。我见过一个案例,某交换机在拥塞时连续丢弃了 5 个报文,快速重传只恢复了第一个,后面的全靠超时,导致应用层感知到 3 秒的卡顿。
4.5 快速恢复(Fast Recovery)
快速重传之后,TCP 不会回到慢启动,而是进入快速恢复阶段。这是 TCP Reno 和 TCP NewReno 的核心改进。
快速恢复的流程大致如下:
- 收到 3 个重复 ACK 时,ssthresh 设为 cwnd / 2
- cwnd 设为 ssthresh + 3 * MSS(因为已经收到的 3 个重复 ACK 表明有 3 个报文离开了网络)
- 每收到一个重复 ACK,cwnd 增加 1 个 MSS(允许发送新的数据)
- 收到新的 ACK 时,cwnd 设为 ssthresh,进入拥塞避免
说白了,快速恢复就是在丢包后不把窗口降到底,而是降一半,然后慢慢恢复。这样能保持较高的吞吐量。
关键区别:TCP Tahoe 在丢包后直接回到慢启动(cwnd=1),而 TCP Reno 使用快速恢复。实际测试中,Reno 在高延迟链路上的吞吐量比 Tahoe 高出 30%-50%。
4.6 知识体系总览
下面这张图把 TCP 拥塞控制的整体流程画出来了。我建议你多看几遍,把每个阶段的条件和动作记清楚。
4.7 实际排查中的要点
讲完了理论,说说我在实际排查中怎么用这些知识。
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 吞吐量远低于带宽 | cwnd 太小,或 ssthresh 设置过低 | 用 tcpdump 抓包,观察窗口变化;检查是否启用了窗口缩放 |
| 频繁出现重复 ACK | 网络存在轻微丢包或乱序 | 查看重复 ACK 数量;区分是丢包还是乱序(乱序通常只产生 1-2 个重复 ACK) |
| 超时重传频繁 | 严重拥塞或链路故障 | 检查 RTO 值是否合理;查看是否有连续丢包 |
| 慢启动阶段就丢包 | 初始窗口太大,或链路缓冲区不足 | 尝试减小初始窗口;启用 pacing 发送 |
避坑指南:我曾经遇到一个案例,某应用在跨机房传输时性能极差。抓包发现 cwnd 始终在 10 左右徘徊,ssthresh 被设成了 10。原因是中间某台设备启用了 TCP 流量整形,强制把 ssthresh 改小了。排查了三天才找到根因。所以,遇到拥塞控制相关的问题,别忘了检查中间设备有没有做手脚。
最后说一句,TCP 拥塞控制算法还有很多变种,比如 CUBIC、BBR 等。但不管怎么变,慢启动、拥塞避免、快速重传、快速恢复这四大块是基础。把基础打牢了,遇到再花哨的算法也能快速上手。
嗯,这一章就到这里。记住,拥塞控制的核心就是一句话:试探网络容量,丢包就退让,稳定就增长。搞懂了这句话,你就掌握了 TCP 拥塞控制的精髓。