每个早高峰都会上演「拥堵悖论」:人人都想快点到,可人人同时踩油门,结果谁也走不动。真正有经验的老司机反而会先慢后快、边开边探路——刚上高速时轻踩油门试探路况,确认路况通畅后逐步提速,遇到前方缓行立刻松油减速,等路通了再慢慢补回来。TCP 的拥塞控制,正是网络世界里的这位「老司机」。它的工作,是帮发送方摸清网络的「路况脾气」,既不一股脑灌爆管道,也不缩手缩脚浪费带宽。本文用「高速公路」这个贯穿全篇的比喻,带你走完一条数据从出发到到达的全过程,看看它在链路上要经历怎样一场「智能交通管制」。

岗位关联

拥塞控制是保证互联网不「堵死」的底层机制,掌握它意味着你具备了排查网络性能问题、优化数据传输效率的硬核能力,是成为资深后端、网络或基础设施工程师的必修课。

目录
  1. 可视化演示
  2. 拥塞的本质
  3. 滑动窗口机制
  4. 慢启动算法
  5. 拥塞避免阶段
  6. 快重传快恢复
  7. 拥塞信号识别
  8. BBR新算法
  9. 工程实践调优
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

展示拥塞窗口AIMD增长与缩减、快重传3次重复ACK机制、以及丢包与延迟信号检测策略对比。

本节配有交互动效(进入视口后自动加载,约 3KB)。

AIMD拥塞窗口演化 慢启动指数增长后线性增长,丢包时减半 cwnd:1→2→4→8→linear AIMD加性增乘性减是经典拥塞控制框架

拥塞的本质

早高峰的高速公路入口匝道,是观察「拥堵悖论」最好的地方:车流一股脑涌入主路,而主路的通行能力是固定的,于是车辆开始减速、排队,队尾越拉越长,最终连匝道口都被堵死。你堵在车流里缓慢挪动时,其实并不是路坏了,而是到达的汽车数量超过了道路的疏散能力。

网络世界完全一样。多个发送方同时往一条瓶颈链路灌数据,速率一旦超过链路的承载上限,路由器里的缓存队列就会像匝道排队一样越积越长。数据包在网络设备里排队,意味着每一跳的转发延迟上升;当队列彻底塞满,后续到达的数据包就会被无情丢弃——这就好比匝道口排队排到马路牙子,交警直接摆手禁止进入,后面的车只能掉头或原地等待。

衡量这种「排队积压」最经典的数学模型是利特尔定律。它用一条极简的公式把队列长度、到达速率和等待时间联系起来:

理解了这条公式,你就明白为什么「网络拥塞」不只是「慢」的问题:它是一连串恶性的连锁反应。发得越多,队列越长,延迟越高,甚至丢包。老司机不会在堵车时猛踩油门——他会收油、控速、与前车保持距离。TCP 的拥塞控制,就是给自己的发送行为装上这样一套「路况感知 + 动态控速」的油门系统。

L=λ×W\displaystyle L = \lambda \times W
队列里等待的数据包数 = 每秒新到的数据包 × 每个包平均等待的时间。排队越长、到达越猛,每辆「车」等得就越久。
  1. L(队列长度)代表路由器缓存中正在排队等待转发的数据包数量,相当于匝道上拥堵的汽车数。
    LL
  2. λ(到达速率)单位时间内到达路由器的新数据包数量,相当于每秒涌进匝道的汽车数量。
    λ\lambda
  3. W(平均等待时间)每个数据包从进入队列到被转发出去所消耗的平均时间,相当于每辆车在队里等了多少秒。
    WW
  4. 乘号(三者关系)把到达速率和等待时间相乘,就得到此刻积压在队列里的数据包总数——队伍越壮大,等待越漫长。
    ×\times

滑动窗口机制

高速公路上常有匝道信号灯,每隔几秒放行几辆车进入主路。为什么不定时放行而是按「批」放行?因为主路与匝道之间有一段缓冲带,一次放太多车会把缓冲带塞满,主路上反而乱成一团。TCP 发明了滑动窗口来做同样的事:不让发送方一次把所有数据都扔进网络,而是一次只放行「一个窗口」那么多。

发送方每发一个数据包,都期待对方回一个 ACK(确认)。如果严格「发一个、等一个」,网络传输效率会低到令人发指——高速公路上每辆车都等到前车完全到达才启动,那还要高速公路干什么?滑动窗口允许发送方在收到确认之前,一次性发出窗口大小那么多的数据包。窗口滑动的过程,就像匝道信号灯按批次放行:绿灯亮起,一批车驶入;前面的车确认到达,后面的车再补上。

这里的关键是:发送窗口并不是发送方想定多大就定多大,它受两个「老板」约束。第一个是接收方的接收窗口(rwnd),代表接收方的缓冲区还能装多少数据,相当于目的地停车场还剩多少车位;第二个是拥塞窗口(cwnd),代表发送方对当前网络容量的估算,相当于发送方判断主路还能容纳多少车不堵。真正的发送窗口取两者的较小值:

匝道信号灯的好处是「有序疏导」,滑动窗口的好处也一样:它把一次性的「洪水式发送」拆解成一批批可控的「车队放行」。接下来我们要问:这个 cwnd 到底怎么涨、怎么跌?这就引出了慢启动和拥塞避免这两大核心算法。

发送窗口=min⁡(cwnd, rwnd)\displaystyle \text{发送窗口} = \min(\text{cwnd},\ \text{rwnd})
发送方实际能发多少数据,既不能超过「网络能装下」(cwnd),也不能超过「对接收方能收下」(rwnd)。谁小听谁的。
  1. cwnd(拥塞窗口)发送方根据丢包、延迟等信号推测出的网络承载能力,相当于司机凭路况判断主路还能放入多少车。
    cwnd\text{cwnd}
  2. rwnd(接收窗口)接收方在 ACK 中通告的剩余接收缓存大小,相当于目的地停车场剩余车位的实时播报。
    rwnd\text{rwnd}
  3. min(取小函数)两个限制条件同时生效,取更严格的那个作为最终阈值。
    min⁡(⋅, ⋅)\min(\cdot,\ \cdot)
  4. 发送窗口(最终结果)一次能发出去的数据总量,必须同时满足网络和接收端的双重约束。
    发送窗口\text{发送窗口}

慢启动算法

新手司机刚上高速时,绝不会一脚油门踩到底——他先以 60 公里时速试探路况,发现前方通畅,加到 80、再加速到 100……每过一段距离确认安全,就大胆地提一档。TCP 的慢启动(Slow Start)正是这个思路的算法版:它不是真的「慢」,而是「从谨慎起步,然后飞快地试探性加速」。

慢启动的工作方式是这样的:连接刚建立时,cwnd 被设为一个很小的初始值(经典实现是 1 个 MSS,即一个最大段大小;现代 Linux 放宽到 10 个 MSS)。每收到一个 ACK,cwnd 就增加 1 个 MSS。因为一个 RTT(往返时间)内发出的包都能收到 ACK,所以每个 RTT 后 cwnd 会翻一倍:1 → 2 → 4 → 8 → 16……这就是指数增长。

这种增长不可能永远持续。TCP 里预设了一个慢启动阈值(ssthresh),它像高速公路上的限速牌。当 cwnd 小于 ssthresh 时,处于慢启动阶段,窗口指数增长;当 cwnd 达到或超过 ssthresh 时,进入拥塞避免阶段,改用线性增长。数学上,慢启动的增长规律可以写成:

慢启动的「试探性加速」非常契合真实网络:开局时谁都不知道这条链路的水有多深,老老实实从小批量开始,确认一路没丢包,才敢大幅提速。如果中途发生超时丢包,ssthresh 会被设为当前 cwnd 的一半,cwnd 重置回 1,重新走慢启动。这就像新手司机刚把速度提起来,突然发现前方事故亮起刹车灯——立刻松油,重新谨慎起步。可是每次都从 1 重新来,效率太低了。于是 TCP 还有后半招:拥塞避免。

cwndnew=cwndold×2(每经过一个 RTT)\displaystyle cwnd_{new} = cwnd_{old} \times 2 \quad (\text{每经过一个 RTT})
每一轮往返时间过后,拥塞窗口就翻倍。指数增长极其迅猛,所以它必须在阈值处果断刹车,切换到保守模式。
  1. cwnd(当前拥塞窗口)发送方此刻允许在途的数据包数量,相当于当前允许进入主路的车队规模。
    cwndoldcwnd_{old}
  2. ×2(每轮翻倍)每个 RTT 内所有 ACK 都到达,说明路况良好,窗口直接加倍。
    ×2\times 2
  3. cwnd_new(增长后的窗口)新一轮 RTT 中允许发送的数据包数量,翻倍后的车队规模。
    cwndnewcwnd_{new}
  4. 条件(每经过一个 RTT)翻倍不是瞬间完成的,而是以「往返时间」为节拍器,一个节拍翻一倍。
    每经过一个 RTT\text{每经过一个 RTT}

拥塞避免阶段

老司机把车开到时速 100 公里后,不会继续猛踩油门翻倍提速,因为再快就可能失控。他会怎么做?轻点油门,让车速以「每公里加 1 公里」的节奏缓慢爬升,直到找到一个既稳又快的平衡速度。这就是拥塞避免(Congestion Avoidance)的核心哲学:在接近网络容量上限时,把指数增长切换为小心翼翼的线性试探。

拥塞避免阶段的规则很简单:每经过一个 RTT,cwnd 加 1(单位是 MSS),而不是翻倍。用前面慢启动的数值举例:当 ssthresh = 16 时,cwnd 从 16 开始,每个 RTT 变成 17、18、19……像爬楼梯一样一格一格往上走。这种「每次只多放一辆车进主路」的节奏,极大降低了突然压垮网络的风险。

拥塞避免与乘性减窗合在一起,构成了 TCP 最经典的 AIMD 框架:加法增加(Additive Increase)、乘法减小(Multiplicative Decrease)。「加法增加」是我们刚才说的每 RTT 加 1;「乘法减小」指的是,一旦检测到丢包,ssthresh 立即降为当前 cwnd 的一半,cwnd 也一并削减。增长时如履薄冰,遇险时果断断腕,这是 AIMD 维持网络稳定性的根本逻辑:

而「乘法减小」的数学表达同样关键:一旦出现丢包信号,发送方执行 ssthresh = cwnd / 2。这相当于司机看到前方刹车灯亮起,立刻把车速砍半。注意 AIMD 的妙处:它不要求所有司机都知道路况的全貌,只要每个发送方都遵守「加一减半」的守则,整个系统就能自发地收敛到公平且高效的平衡点。不过,丢包检测也有快慢之分——是傻等超时,还是迅速反应?下一节揭晓。

cwndnew=cwndold+1(每经过一个 RTT)\displaystyle cwnd_{new} = cwnd_{old} + 1 \quad (\text{每经过一个 RTT})
拥塞避免阶段,每轮往返时间只把窗口加大 1 个报文段。它放弃了指数级的激进,换来了接近容量上限时的从容与稳定。
  1. cwnd(当前拥塞窗口)此刻允许发送的数据包总量,即当前车队的规模。
    cwndoldcwnd_{old}
  2. +1(每轮只加一)每个 RTT 只增加 1 个 MSS,相当于车队每轮只多放一辆车进去。
    +1+1
  3. cwnd_new(增量后的窗口)线性增长后的新窗口值,与慢启动的翻倍形成鲜明对比。
    cwndnewcwnd_{new}
  4. 条件(每经过一个 RTT)以 RTT 为节拍,每个节拍加一,永不跳档。
    每经过一个 RTT\text{每经过一个 RTT}

快重传快恢复

假如你的车在高速上爆了胎,你是停下车打电话等拖车,还是靠边停车后立刻换上备胎继续走?显然,换备胎继续走远比原地等待高效。TCP 早期处理丢包的方式就是「原地等待」——等超时计时器到期再重传,往往要白白消耗一个 RTT 甚至更多。后来工程师发明了更聪明的办法:快重传(Fast Retransmit)与快恢复(Fast Recovery)。

快重传的原理基于一种「旁车提醒」现象。接收方收到乱序数据包时,会立即重复确认「我还在等第 N 号包」。当发送方连续收到 3 个重复的 ACK 时,基本可以断定第 N 号包在途中丢了——这就像你旁边的司机连续按了三次喇叭提醒你「备胎掉了」。此时发送方不再傻等超时,而是立即重传那个丢失的报文段。

快恢复则是对 ssthresh 的巧妙处置。以前发生丢包后,cwnd 要重置回 1,重新走一遍慢启动,代价高昂。快恢复的思路是:既然还能收到 3 个重复 ACK,说明网络还没完全瘫痪,没必要「停车重启」,把车速降一半继续跑就行。于是发送方把 ssthresh 设为当前 cwnd 的一半,cwnd 设置为新阈值,然后跳到拥塞避免阶段线性增长。整个过程的数学描述十分简洁:

快重传与快恢复是「备胎逻辑」的完美体现:遇到故障,不等待外部救援(超时机制),靠自己的感知(重复 ACK)快速响应,以最小代价恢复行驶。这一机制还有个重要的前提——它依赖接收方的重复 ACK 作为「喇叭提醒」。那么问题来了:发送方凭什么判断网络出问题了?是靠丢包,还是靠延迟?这就进入拥塞信号识别的主题。

ssthresh=cwnd2\displaystyle \text{ssthresh} = \frac{cwnd}{2}
cwnd=ssthresh\displaystyle cwnd = \text{ssthresh}
检测到 3 个重复 ACK 后,阈值减半,窗口直接落到新阈值上继续跑,而不是跌回 1 重新慢启动。这是「快」字的真正含义。
  1. 分母 2(减半)乘法减小策略的标准动作,无论当前 cwnd 多大,一律砍半,相当于车速减半。
    cwnd2\frac{cwnd}{2}
  2. ssthresh(新阈值)丢包后的新慢启动阈值,未来窗口增长到此处就切换为线性模式。
    ssthresh\text{ssthresh}
  3. 等号(直接赋值)快恢复不把 cwnd 打回 1,而是直接设为新阈值,避免重新慢启动的时间开销。
    ==
  4. cwnd(恢复后的窗口)新窗口从阈值处继续按拥塞避免节奏增长,相当于半速巡航后缓缓提速。

拥塞信号识别

判断一个路口堵不堵,老司机有两种办法:一是看到前车刹车灯全亮(丢包信号),二是感觉自己的车速没变但发动机转速越来越高(延迟信号)。TCP 的拥塞检测同样有这两类信号,它们各有各的「脾气」。

丢包信号是经典 TCP 的主流判据。它的逻辑很简单:报文丢了,就说明网络撑不住了。在路由器采用「尾丢弃」(队列满后直接丢包)的时代,丢包确实是拥塞的可靠标志。但它有个致命缺陷:信号是二元的、滞后的。只有当队列完全溢出时才知道网络堵了,好比一定要等到堵死才知道路况不好,此时已经错过了最佳减速时机。更麻烦的是,无线链路、光纤切换等场景会产生「随机丢包」,本来网络不堵,却被误判为拥塞,导致无谓地降低发送速率。

延迟信号则细腻得多。网络延迟可以拆成三个部分:传播延迟(光在光纤里跑的时间)、传输延迟(数据包被推上链路的时间)和排队延迟(在路由器队列里等待的时间)。前两部分基本恒定,只有排队延迟会随拥塞程度连续变化——队列越深,延迟越高。于是 RTT 的升高就成了「发动机转速升高」式的早期预警信号:

除了丢包和延迟,抖动(延迟的方差)也是辅助信号:抖动加剧,说明队列长度在剧烈波动,网络状态不稳定。丢包信号和延迟信号并非互斥,现代拥塞控制往往两者兼用——丢包作为兜底判据,延迟作为早期预测。但这两类信号都属于「被动感知」,就像司机根据刹车灯猜路况。能不能主动去「测量」道路的极限容量,然后照着容量跑?这正是 BBR 算法颠覆性的地方。

D=Dp+Dt+Dq\displaystyle D = D_p + D_t + D_q
总延迟 = 传播延迟 + 传输延迟 + 排队延迟。传播和传输基本固定,真正波动的是排队延迟 Dq,它直接反映队列积压程度。
  1. Dp(传播延迟)信号在链路上以光速传播所需时间,由物理距离决定,几乎不变,相当于路本身的长度。
    DpD_p
  2. Dt(传输延迟)把整个数据包按链路速率「推」上线路的时间,由包大小和带宽决定。
    DtD_t
  3. Dq(排队延迟)数据包在路由器队列中等待转发的时长,是拥塞程度最敏感的晴雨表。
    DqD_q
  4. 总和(延迟结构)三者叠加得到端到端总体延迟。Dq 上升意味着队列在积压,网络正在走向拥塞。
    ++

BBR新算法

传统 TCP 的思路是「开车看路况,堵了就减速」——永远在试探和退缩之间震荡。而 BBR(Bottleneck Bandwidth and Round-trip propagation time)换了一个完全不同的思路:与其被动应对拥堵,不如直接测量这条高速公路的物理极限——瓶颈路段最多能容纳多少车流?车在通畅状态下往返一趟要多久?测准了这两个数,就按这个「管道容量」稳定巡航,既不塞车也不浪费。

BBR 用两个核心参数描述网络特性。第一个是 BtlBw(瓶颈带宽):整条链路中最窄的那段每秒最多能传输多少数据,相当于高速公路上最窄车道的最大车流通过率;第二个是 RTprop(最小往返传播时间):网络上没有任何排队时,数据包往返一次的最短时间,相当于空无一车时从入口到出口再返回的最快时间。两个参数相乘,就是网络管道的总容量 BDP(带宽延迟积):

BBR 的工作节奏和传统 AIMD 完全不同:它周期性加速探测更高的 BtlBw,再周期性减速探测更低的 RTprop,然后稳定在「刚好填满管道」的发送速率上。相比 TCP 的「锯齿状」窗口曲线——升升升、断了、砍半、再升——BBR 更像一辆开启了自适应巡航的车:平稳行驶,极少急刹。在弱网高丢包场景(如移动网络、长肥链路)中,BBR 通常能跑出数倍于传统算法的吞吐量。不过它并非万能,在公平性、与旧算法的共存方面仍有争议,工程落地时需要谨慎评估。

BDP=BtlBw×RTprop\displaystyle \text{BDP} = \text{BtlBw} \times \text{RTprop}
BDP 是「路有多长、能同时容纳多少数据」的物理上限。发送速率接近 BtlBw、在途数据量接近 BDP,就能跑满带宽又不制造排队。
  1. BtlBw(瓶颈带宽)链路中最小的一段带宽,决定了整条路的最大数据吞吐率,相当于最窄车道的通行能力。
    BtlBw\text{BtlBw}
  2. RTprop(最小往返时间)无排队状态下数据包往返的最小耗时,相当于空荡荡的高速上跑一趟全程的时间。
    RTprop\text{RTprop}
  3. 乘号(容量计算)带宽乘以时间,得到「管道体积」——这条路上最多能同时承载多少比特的数据。
    ×\times
  4. BDP(带宽延迟积)网络管道容量的量化指标。发送窗口超过 BDP 不会提速,只会把队列撑高。
    BDP\text{BDP}

工程实践调优

一座城市的交通管理,最终要靠交通指挥中心的大屏:传感器汇报实时车流、匝道信号灯自动调节放行速率、可变限速板引导司机减速。网络拥塞控制的工程落地也一样,考验的是「调参、监控、按场景决策」的综合能力。把算法搞清楚只是第一步,真正让 TCP 在业务中跑出高性能的,是实践中的细节。

参数调优是基础。Linux 下我们可以通过 sysctl 调整 TCP 栈的关键参数:net.ipv4.tcp_wmem / tcp_rmem 控制收发缓冲区大小,直接影响窗口上限;net.ipv4.tcp_window_scaling 是否开启决定窗口能否突破 64KB 上限;net.ipv4.tcp_sack 开启选择性确认,避免快重传时不必要的回退;连接初始窗口也可以从默认值调大,减少短连接在慢启动阶段的时间损耗。这就像根据城市路况调整匝道信号灯的绿灯时长和主路限速牌。

监控告警是眼睛。拥塞控制的效果最终要落到四个指标上:丢包率(重传比例升高意味着拥塞信号密集)、RTT(平均延迟与延迟抖动反映队列积压情况)、吞吐量(实际传输速率是否逼近瓶颈带宽)以及队列深度(网络设备缓冲区的占用情况)。用带宽利用率衡量链路是否跑满,是最直观的判断:

部署场景决定算法选型:数据中心内部网络延迟极低、队列极短,适合启用 DCTCP 或带 ECN 的拥塞控制;跨地域长肥链路(带宽大、延迟高)上,BBR 往往能显著提升吞吐量;而实时音视频场景则更看重延迟稳定性,需要配合 FEC 前向纠错等机制。工程化的本质,是理解每个算法在什么路况下才能发挥最佳性能——就像交通指挥中心不会在雨雪天照搬晴天的信号灯方案。搞清楚了算法原理与信号差异,你就能在实际系统中做出有理有据的拥塞控制优化决策。

U=实际吞吐量瓶颈带宽\displaystyle U = \frac{\text{实际吞吐量}}{\text{瓶颈带宽}}
利用率 U 越接近 1,说明链路利用越充分;但如果 U 长期很高而 RTT 也同步飙升,说明链路已经「超载排队」,需要警惕缓冲膨胀。
  1. 实际吞吐量(分子)单位时间内真正成功传输到对端的数据量,相当于高速公路上实际通过的车流量。
    实际吞吐量\text{实际吞吐量}
  2. 瓶颈带宽(分母)链路上最窄处的理论极限速率,相当于道路设计通行能力。
    瓶颈带宽\text{瓶颈带宽}
  3. 除号(利用率计算)实际车流除以设计容量,得到当前道路的繁忙程度。
    ÷\div
  4. U(利用率)0-1 之间的比值。U 高而 RTT 平稳是理想状态;U 高而延迟飙升则是拥塞前兆。
    UU

AI 应用场景

数据中心网络性能调优

阶段:模型训练与推理服务的网络基础设施调优。

解决的问题:AI 大模型训练采用分布式并行架构,通信模式呈现典型的「多对一」Incast 流量突发——几十甚至上百台 GPU 服务器同时向一台参数服务器发送数据,瞬间把交换机缓存打满,导致尾延迟飙升、训练效率骤降。传统 TCP 基于丢包的拥塞控制反应慢,等到丢包才降速已经晚了。

具体案例:某云厂商在千卡 GPU 集群中启用 ECN 显式拥塞通知 + DCTCP 拥塞控制算法,让交换机在队列积压早期就打上拥塞标记,发送方据此平滑降速。结果是把参数服务器链路的 P99 延迟从 200ms 降到 20ms,分布式训练的整体吞吐提升了约 30%。

直播推流与 CDN 分发优化

阶段:AI 生成内容的实时分发与媒体传输。

解决的问题:AI 数字人直播、实时渲染视频流对带宽与延迟极其敏感。传统拥塞控制遇到丢包会激进的减半窗口,导致画质频繁波动;而 CDN 面向的用户可能处于弱网环境(Wi-Fi 波动、4G 信号不稳),网络状态远比数据中心复杂。丢包信号在这里失效,延迟信号和带宽估计更有价值。

具体案例:某直播平台在推流链路启用 BBR 拥塞控制后,实测在 5% 随机丢包率的弱网环境下,平均码率提升了 2.1 倍,卡顿率下降 60%。原因正是 BBR 基于带宽估计而非丢包反应,能在高丢包链路中更充分地利用带宽。

移动端 API 网关优化

阶段:智能助手 App 的云端 API 响应优化。

解决的问题:AI 对话类应用对首包延迟极其敏感。移动网络下 TCP 连接建立后的慢启动阶段往往浪费大量 RTT,如果每次请求都从小窗口重新爬升,交互体验会非常差。合理调大初始窗口、开启窗口缩放并配合连接复用,能显著减少启动阶段的浪费。

具体案例:某大模型产品 App 的 API 网关对短连接场景进行调优:将 TCP 初始窗口调整为 10 MSS,开启 SACK 与窗口缩放,同时复用长连接。上线后首包时间平均下降 35%,弱网环境下的请求成功率从 97.2% 提升到 99.1%。

Python 演示

python30 行
#!/usr/bin/env python3"""TCP 拥塞控制 (AIMD) 模拟: 慢启动、拥塞避免、快恢复""" import random  def simulate(loss_rate=0.02, initial_ssthresh=16, rounds=100):    cwnd = 1                # 拥塞窗口, 单位 MSS    ssthresh = initial_ssthresh    history = []     for r in range(rounds):        history.append(cwnd)        sent = int(cwnd)        lost = sum(random.random() < loss_rate for _ in range(sent))        if lost > 0:            ssthresh = max(sent // 2, 2)            cwnd = ssthresh          # 快恢复: 直接降到新阈值        else:            if cwnd < ssthresh:                cwnd = cwnd * 2      # 慢启动: 指数增长            else:                cwnd = cwnd + 1      # 拥塞避免: 线性增长    return history  if __name__ == "__main__":    hist = simulate()    for i, v in enumerate(hist):        print(f"RTT {i:3d}: cwnd = {v}")
目的:模拟一个 TCP 连接在 100 个 RTT 内拥塞窗口的变化过程,帮助理解慢启动、拥塞避免与快恢复之间的状态切换。
  1. cwnd = 1初始拥塞窗口设为 1 个 MSS,对应慢启动从最谨慎的试探开始。
  2. ssthresh = initial_ssthresh慢启动阈值决定指数增长何时切换为线性增长。
  3. lost = sum(random.random() < loss_rate for _ in range(sent))模拟这轮 RTT 里发出的数据包中,有几个因网络拥塞而丢失。
  4. ssthresh = max(sent // 2, 2)遇丢包时执行乘法减小,阈值降为当前窗口的一半。
  5. cwnd = ssthresh快恢复的策略:不回到 1,而是直接落在新阈值上继续跑。
  6. cwnd = cwnd * 2慢启动的指数增长:每轮 RTT 窗口翻倍。
  7. cwnd = cwnd + 1拥塞避免的线性增长:每轮 RTT 只加 1 个 MSS。

常见误区

误区我一直以为慢启动就是发送数据很慢,所以 TCP 传输前期效率特别低,应该尽量调大初始窗口来提升速度。

正解慢启动的「慢」是指谨慎地从小窗口起步,但它一旦确认路况良好,就会指数级提速——1、2、4、8、16……几个 RTT 内就能冲到很高的窗口值。它像新手司机先缓慢起步但紧接着就大胆加速,真正意图是「试探性激进」,而非「恒定低速」。盲目的调大初始窗口反而可能在网络状况未知时瞬间造成拥塞。

为何常见:因为名字里带「慢」,初学者容易望文生义;且短连接场景下慢启动确实让传输前期吞吐偏低,很容易让人觉得「慢」是缺陷而不是保护机制。

误区带宽不够就调大发送缓冲区、把拥塞窗口上限调得很高,数据传输就会更快。

正解当窗口超过带宽延迟积(BDP)后,多出来的数据并不会提升吞吐量,而只是填充路由器队列,造成延迟上升、丢包增加。这就像高速公路上车流已经达到道路容量,再多放车进来只会让所有人更慢。正确的做法是让窗口匹配 BDP,而不是无限扩大。

为何常见:直觉上「允许发更多数据 = 传输更快」,但这个直觉忽略了网络中「队列积压」这个中间变量,导致把窗口和吞吐简单画上等号。

误区丢包率一升高,我就断定网络拥堵了,然后大幅度降低发送速率。

正解丢包可能由多种原因造成:无线链路的信号衰减、光纤切换的瞬时中断、路由器缓存策略导致的随机丢弃,这些都与拥塞无关。如果把「随机丢包」误判为「拥塞信号」,就会像司机被路面的小石子颠了一下就猛踩刹车,白白损失吞吐量。这也是 BBR 等基于延迟/带宽估计的算法能胜出的原因——它们不会对所有丢包都作出过度反应。

为何常见:经典 TCP 的教科书把丢包当作拥塞的代名词,初学者形成了强关联;现实中却忽略了网络底层硬件也会产生非拥塞丢包。

误区进入了拥塞避免阶段,网络就不会拥塞了,我可以放心大胆地传输。

正解拥塞避免的本意是「避免拥塞崩溃」——即防止发送速率无限增长把网络彻底压垮,它并没有能力阻止局部拥塞的发生。事实上塞车还是会来的,算法只是用线性增长替代指数增长,让网络在接近容量时更平稳,并在拥塞发生后快速恢复。它不是消除拥堵,而是把拥堵控制在一个可接受的范围。

为何常见:名称中的「避免」二字具有误导性,容易让人把它理解成一种「免疫能力」,而忽略了它只是放缓增长节奏的保守策略。

面试题

Q1. 慢启动阶段,拥塞窗口 cwnd 每经过一个 RTT 通常如何变化?

单选题 · easy

  • A.增加 1 个 MSS
  • B.翻倍增长← 正确答案
  • C.保持不变
  • D.减半

答案:B。慢启动的典型特征是「试探性加速」:每收到一个 ACK,cwnd 加 1 个 MSS,因此在完整的一个 RTT 内窗口整体翻倍。选项 A 是拥塞避免阶段的特征,C 和 D 与慢启动无关。

Q2. 发送方收到几个重复 ACK 时会触发快重传?

单选题 · easy

  • A.1 个
  • B.2 个
  • C.3 个← 正确答案
  • D.4 个

答案:C。TCP 约定收到 3 个重复 ACK 就认为某个报文段丢失,立即重传而不再等待超时。这就像旁边司机连按三次喇叭提醒你,你才确信出问题了。1 个重复 ACK 只是乱序的常见信号,不足以触发重传。

Q3. 拥塞避免阶段,cwnd 每经过一个 RTT 如何变化?

单选题 · easy

  • A.加 1 个 MSS← 正确答案
  • B.翻倍
  • C.加 10 个 MSS
  • D.减半

答案:A。拥塞避免采用「加性增窗」策略,每轮 RTT 只增加 1 个 MSS,对应 AIMD 中的 AI(Additive Increase)。翻倍是慢启动的行为,减半是乘性减窗的行为。

Q4. 以下哪些是经典 TCP 拥塞控制(AIMD 框架)的组成部分?

多选题 · mid

  • A.慢启动← 正确答案
  • B.拥塞避免← 正确答案
  • C.快重传与快恢复← 正确答案
  • D.BBR 带宽探测

答案:ABC。经典 AIMD 框架由慢启动、拥塞避免、快重传和快恢复四个阶段组成,构成完整的窗口演化闭环。BBR 是一种独立于 AIMD 的新算法,不属于经典框架的组成部分。

Q5. 发生丢包时,TCP 的慢启动阈值 ssthresh 会如何变化?

单选题 · mid

  • A.降为当前 cwnd 的一半← 正确答案
  • B.降为当前 cwnd 的四分之一
  • C.保持不变
  • D.翻倍

答案:A。AIMD 的乘性减窗(MD)策略要求 ssthresh 降为 cwnd 的一半。这相当于老司机看到前方刹车灯亮起,立刻把车速砍半。降至四分之一或翻倍都不符合 TCP 标准行为。

Q6. 以下哪些可以作为判断网络拥塞的信号?

多选题 · mid

  • A.数据包丢失← 正确答案
  • B.RTT 延迟上升← 正确答案
  • C.延迟抖动加剧← 正确答案
  • D.接收方通告窗口 rwnd 增大

答案:ABC。丢包信号(丢包)、延迟信号(RTT 上升)和抖动信号(延迟方差加剧)都是拥塞的常见指示。rwnd 是接收方空闲缓存的大小,它与网络拥塞无关,增大 rwnd 表示接收端愿意接收更多数据。

Q7. 为什么基于丢包的拥塞控制算法在光纤链路中可能失效?

单选题 · hard

  • A.光纤链路的带宽太小
  • B.光纤存在大量随机丢包且与拥塞无关← 正确答案
  • C.光纤链路的 RTT 太长
  • D.光纤不支持 TCP 协议

答案:B。光纤切换、信号衰减等会产生少量「随机丢包」,这种丢包与网络拥塞无因果关系。基于丢包的算法会将其误判为拥塞信号,无谓地减半发送速率,导致带宽严重浪费。这正是 BBR 等基于带宽估计的算法在长肥链路上更优的原因。

Q8. BBR 算法主要通过哪两个参数来确定最优发送速率?

单选题 · hard

  • A.cwnd 与 ssthresh
  • B.BtlBw 与 RTprop← 正确答案
  • C.rwnd 与 MSS
  • D.丢包率与重传次数

答案:B。BBR 的核心是测量瓶颈带宽 BtlBw 和最小往返传播时间 RTprop,两者的乘积 BDP 代表网络管道容量。BBR 按 BDP 发送数据,既跑满带宽又不制造排队。cwnd/ssthresh 是传统 AIMD 的参数,BBR 已不再以丢包作为主要反馈。

Q9. 缓冲区膨胀(Bufferbloat)会导致哪些问题?

多选题 · hard

  • A.端到端延迟明显上升← 正确答案
  • B.延迟抖动加剧← 正确答案
  • C.基于丢包的拥塞控制反应滞后← 正确答案
  • D.物理链路带宽自动增大

答案:ABC。路由器缓存过大导致数据包大量排队,延迟和抖动同时飙升;同时因为缓存不易溢出,丢包信号迟迟不来,基于丢包的算法无法及时降速。物理带宽是硬件属性,不会因为缓存增大而改变。

Q10. 发送窗口的最终取值由 cwnd 和 rwnd 中的哪一个决定?

单选题 · easy

  • A.两者中的较大值
  • B.两者中的较小值← 正确答案
  • C.两者之和
  • D.两者之差

答案:B。发送窗口取 min(cwnd, rwnd),两者谁小听谁的。cwnd 受网络容量约束,rwnd 受接收端缓存约束,发送方必须同时满足两者。这就像匝道放行既要看主路余量,也要看目的地停车场是否还有空位。