第四节:TCP拥塞控制与流量控制——慢启动、拥塞避免、快速重传、快速恢复、滑动窗口机制

TCP的拥塞控制和流量控制,说白了就是两套“刹车系统”。

流量控制管的是“接收方吃不吃得消”,拥塞控制管的是“网络中间节点扛不扛得住”。很多新手容易把这两者搞混,我刚开始做网络调优时也踩过这个坑。今天咱们就把它们彻底掰扯清楚。

4.1 滑动窗口机制:流量控制的基石

先讲流量控制。它的核心就是滑动窗口。

接收方会在TCP报文头里塞一个字段叫win(窗口大小),告诉发送方:“哥们,你最多再发这么多字节,多了我缓冲区就炸了。”发送方看到这个值,就得乖乖遵守。

核心要点:滑动窗口的大小是动态变化的。接收方应用程序读得快,窗口就变大;读得慢,窗口就缩小。如果窗口变成0,发送方就得停下来等——这叫“零窗口探测”。

我记得有一次排查一个文件传输慢的问题,抓包一看,接收窗口频繁降到0。后来发现是接收端的磁盘I/O成了瓶颈,应用层读数据太慢。说白了,不是网络不行,是后端处理跟不上。

4.2 慢启动:别一上来就猛踩油门

拥塞控制的第一阶段就是慢启动。名字叫“慢”,其实它增长得飞快。

发送方维护一个拥塞窗口(cwnd),初始值通常是10个MSS(最大报文段长度)。每收到一个ACK,cwnd就加1。你想想看,这其实是指数增长:

  • 第1个RTT:cwnd = 10
  • 第2个RTT:cwnd = 20
  • 第3个RTT:cwnd = 40
  • 第4个RTT:cwnd = 80

为什么会这样?因为每个报文段都会触发一个ACK,而每个ACK又会让cwnd增加。所以实际上每个RTT内,cwnd会翻倍。

注意:慢启动不是真的“慢”,而是“谨慎地开始”。它假设网络是空的,先试探一下,发现没问题就快速增加。如果一开始就发大量数据,很可能直接把路由器打爆。

我曾在数据中心里见过一个案例:某新上线的服务,TCP连接建立后直接发了1MB数据,结果中间交换机队列溢出,大量丢包。这就是典型的“慢启动没起作用”——因为应用层用了sendfile,内核还没来得及做拥塞控制。

4.3 拥塞避免:进入线性增长阶段

当cwnd增长到慢启动阈值(ssthresh)时,TCP就切换到拥塞避免阶段。ssthresh的初始值通常很大(比如64KB),但发生丢包后会动态调整。

在拥塞避免阶段,cwnd的增长方式变了:每个RTT只增加1个MSS。说白了,就是线性增长,不再翻倍了。

为什么?因为此时cwnd已经比较大了,网络可能接近饱和。再指数增长,很容易引发拥塞崩溃。所以TCP变得“小心翼翼”,每次只增加一点点,试探网络的极限。

我的经验:在长肥网络(高带宽高延迟)中,拥塞避免阶段的线性增长太慢了。比如一个RTT是100ms,cwnd要增长1000个MSS才能充分利用带宽,那得花100秒!所以后来有了CUBIC、BBR等算法,专门优化这个场景。

4.4 快速重传:别等超时,赶紧补发

丢包了怎么办?传统做法是等超时重传(RTO),但RTO通常要几百毫秒甚至几秒,太慢了。

快速重传的思路是:如果发送方连续收到3个重复的ACK,就认为某个报文段丢了,不等超时,立即重传。

举个例子:发送方发了报文1、2、3、4、5。接收方收到了1、3、4、5,但没收到2。那么接收方每次收到后续报文,都会回复“ACK 2”(期望收到2)。当发送方连续收到3个“ACK 2”时,就知道2丢了,立刻重传。

关键点:快速重传的前提是接收方启用了延迟确认吗?不,恰恰相反。快速重传依赖的是接收方对乱序报文立即发送重复ACK。如果接收方合并了ACK,反而会延迟重传的触发。

我曾经调优过一个游戏服务器,发现丢包恢复特别慢。抓包一看,接收方开启了延迟确认(Nagle算法),导致重复ACK被合并了。关闭Nagle后,快速重传立刻生效,延迟降低了40%。

4.5 快速恢复:丢包后别从头再来

快速重传之后,TCP进入快速恢复阶段。它的核心思想是:既然还能收到重复ACK,说明网络还没完全堵死,没必要像超时重传那样把cwnd降到1。

快速恢复的流程大致如下:

  1. 收到3个重复ACK时,将ssthresh设为cwnd的一半,cwnd设为ssthresh + 3(因为已经收到的重复ACK相当于“释放”了3个报文段的容量)。
  2. 每收到一个重复ACK,cwnd加1(表示网络还在传输数据)。
  3. 当收到新的ACK(非重复)时,说明丢失的报文已经补上了,将cwnd设为ssthresh,进入拥塞避免阶段。

注意:快速恢复只适用于“轻度丢包”。如果发生了超时重传(RTO),说明网络可能严重拥塞,TCP会直接回到慢启动阶段,cwnd重置为初始值。这是两种完全不同的恢复策略。

嗯,这里要注意:快速恢复并不是所有TCP实现都完全一样。比如Linux内核的TCP实现,在快速恢复期间还会做PRR(比例速率降低),更精细地控制发送速率。但核心思想不变——丢包后不要“一刀切”,要尽量保持吞吐。

4.6 整体流程:一张图看懂

下面我用一张SVG流程图,把慢启动、拥塞避免、快速重传、快速恢复的完整状态机画出来。你一看就明白了:

TCP拥塞控制状态机 慢启动 cwnd指数增长 直到ssthresh 拥塞避免 cwnd线性增长 每个RTT加1 快速恢复 cwnd = ssthresh + 3 收到新ACK后退出 超时重传 cwnd重置为初始值 回到慢启动 cwnd ≥ ssthresh 3个重复ACK 收到新ACK RTO超时 RTO超时 RTO超时 图例 正常状态转换 超时回退路径

4.7 实战中的避坑指南

讲完了理论,我分享几个实际调优中遇到的坑:

  • 窗口缩放因子没开启:早期TCP的窗口字段只有16位,最大65535字节。在高带宽环境下,这个值太小了。我曾经调一个跨洋链路,带宽100Mbps,RTT 200ms,理论吞吐应该是100Mbps,实际只有2Mbps。后来发现是接收方没开启window scaling选项。开启后,窗口可以到1GB,吞吐直接拉满。
  • Nagle算法与延迟确认的冲突:Nagle算法会合并小报文,延迟确认会合并ACK。两者一起用,可能造成“死锁”——发送方等ACK,接收方等数据。我建议在交互式应用(如SSH、游戏)中关闭Nagle(设置TCP_NODELAY)。
  • BBR算法不是万能的:BBR通过测量带宽和延迟来调整发送速率,不依赖丢包。但在有大量缓存的路由器上,BBR可能会过度占用带宽,导致其他流饿死。我个人习惯在共享链路上用CUBIC,在专线上用BBR。

一句话总结:流量控制是“接收方说了算”,拥塞控制是“网络说了算”。滑动窗口是流量控制的工具,慢启动、拥塞避免、快速重传、快速恢复是拥塞控制的四个阶段。理解它们之间的协作关系,你就能诊断90%以上的TCP性能问题。

想要更多资料,联系可可