TCP拥塞控制原理:慢启动、拥塞避免、快速重传、快速恢复

聊到TCP拥塞控制,我脑子里第一个蹦出来的画面,是十年前在数据中心调优时遇到的一个场景。那会儿业务流量一上来,整个集群的TCP连接就像堵车一样,丢包率飙升,吞吐量直接腰斩。后来我花了整整一周,把拥塞控制的四个核心阶段——慢启动、拥塞避免、快速重传、快速恢复——彻底捋了一遍,才把问题压下去。

说白了,TCP拥塞控制就是一套“交通管制”策略。它不让发送方一股脑把数据全扔出去,而是根据网络的实际承载能力,动态调整发送速率。这四招,就是它的核心手段。

1. 慢启动:从试探开始

慢启动这个名字,其实有点误导人。它并不是真的“慢”,而是“谨慎地加速”。

一开始,发送方并不知道网络能承受多少数据。所以它先设一个很小的拥塞窗口(cwnd),比如1个MSS(最大报文段大小)。然后每收到一个ACK,窗口就翻倍。你想想看,这其实是指数增长。

关键点:慢启动的窗口增长是“每RTT翻倍”,而不是“每包翻倍”。

举个例子:

  • 初始cwnd = 1 MSS
  • 收到1个ACK后,cwnd = 2 MSS
  • 再收到2个ACK后,cwnd = 4 MSS
  • 再收到4个ACK后,cwnd = 8 MSS

我在项目中遇到过一种情况:有些工程师把慢启动阈值(ssthresh)设得特别大,结果连接刚建立时,窗口涨得太快,直接把中间路由器的缓存打爆了。嗯,这里要注意——慢启动不是无限加速的,它有一个“刹车点”,就是ssthresh。

当cwnd达到ssthresh时,慢启动结束,进入拥塞避免阶段。

2. 拥塞避免:线性增长,小心试探

进入拥塞避免后,窗口的增长方式从“指数”切换为“线性”。具体来说,每个RTT只增加1个MSS。说白了,就是每收到一个ACK,cwnd增加 1/cwnd 个MSS。

为什么这么设计?因为此时网络已经接近饱和了。再像慢启动那样猛冲,很容易引发拥塞。线性增长是一种“保守试探”——慢慢加,直到出现丢包。

我的经验:在长肥网络(高带宽高延迟)中,拥塞避免阶段的线性增长其实很慢。我曾经调过一个跨国专线,RTT接近200ms,窗口从1000涨到2000,花了整整20秒。这时候可以考虑启用TCP窗口缩放选项,或者改用BBR等新型拥塞控制算法。

3. 快速重传:不等超时,主动重传

传统TCP靠超时重传。但超时时间通常设得比较长(比如200ms以上),效率很低。快速重传的思路是:如果发送方连续收到3个相同的ACK(即重复ACK),就认为某个报文段丢了,不等超时,立即重传。

为什么会是3个?我个人的理解是:1个重复ACK可能是乱序,2个也可能是乱序,但3个重复ACK,丢包的概率就很高了。这是实践中总结出来的经验值。

注意:快速重传只针对单个报文段的丢失。如果发生大量丢包(比如网络严重拥塞),快速重传可能不够用,最终还是得靠超时重传来兜底。

4. 快速恢复:丢包后不归零

在早期的TCP实现中,一旦检测到丢包,cwnd直接降到1,重新慢启动。这太粗暴了——网络只是轻微拥塞,你却把窗口清零,白白浪费带宽。

快速恢复的改进在于:丢包后,cwnd减半(而不是归零),然后进入拥塞避免阶段。这样既响应了拥塞,又保留了部分吞吐能力。

具体流程是这样的:

  1. 收到3个重复ACK,触发快速重传
  2. ssthresh = cwnd / 2
  3. cwnd = ssthresh + 3(因为已经收到的重复ACK表明有3个报文段离开了网络)
  4. 每收到一个重复ACK,cwnd临时增加1(相当于“偷”一点带宽)
  5. 当收到新的ACK时,cwnd = ssthresh,进入拥塞避免

我曾经在调试一个视频流服务时,发现快速恢复阶段的“临时窗口膨胀”会导致短暂的流量尖峰。如果网络设备缓存很小,这个尖峰反而会引发新的丢包。所以,快速恢复不是万能的,它更适合缓存充足的网络。

核心逻辑流程图

下面这张图,是我自己画的一个简化版拥塞控制状态机。你可以把它当作“速查手册”。

慢启动 cwnd指数增长 拥塞避免 cwnd线性增长 快速重传 3个重复ACK触发 快速恢复 cwnd减半+临时膨胀 cwnd >= ssthresh 丢包/3个重复ACK 重传后进入 收到新ACK 超时(RTO)

这张图里,我特意用虚线标出了“超时回退到慢启动”的路径。这是很多新手容易忽略的——快速恢复不是万能的,一旦超时,一切归零。

四个阶段的对比

阶段 触发条件 窗口变化 典型场景
慢启动 连接建立 / 超时重传后 指数增长(每RTT翻倍) 新连接、严重拥塞后恢复
拥塞避免 cwnd >= ssthresh 线性增长(每RTT +1 MSS) 稳态传输
快速重传 收到3个重复ACK 立即重传丢失报文 单个报文段丢失
快速恢复 快速重传后 cwnd减半,临时膨胀 轻微拥塞,避免归零

避坑指南:我曾经在配置Linux内核参数时,把tcp_slow_start_after_idle设成了0,结果空闲连接恢复后直接跳过慢启动,窗口保持原值。这在某些场景下能提升性能,但如果网络状态已经变化,反而会引发拥塞。所以,除非你很清楚自己在做什么,否则别乱改这个参数。

最后说一句:这四个阶段不是孤立的,它们是一个闭环。慢启动试探网络容量,拥塞避免维持稳态,快速重传和快速恢复应对小规模丢包。理解了这个闭环,你就能在遇到TCP性能问题时,快速定位是哪个环节出了岔子。

可可精品资料库