TCP拥塞控制核心四算法:慢启动、拥塞避免、快重传与快恢复实战
发布时间:2026/9/11 1:35:01 作者:尧图编辑部 阅读量:1,286

1. 先把拥塞控制放回真实场景里期末复习周总有人拿着“TCP 的拥塞控制”这一章犯愁慢启动、拥塞避免、快重传、快恢复到底谁先谁后不瞒你说408考过保研面试问过工作后排查线上服务变慢的时候也照样用得上。这一章是 TCP 里最像“黑盒博弈”的部分——你永远不知道网络中间发生了什么只能靠丢包和延迟去猜然后动态调整发送速度。这篇文章不打算按课本顺序平铺直叙我尽量用“现场排查”和“备考刷题”两条线串起来讲。不管是计算机网络期末复习、考研 408还是保研复试准备又或者你只是个想把 TCP 基础补扎实的开发者都可以照着下面的思路来读。读完之后再看课本你会觉得那四个算法不再是孤立名词而是一套完整的“堵车应对策略”。2. 拥塞控制和流量控制别再傻傻分不清2.1 一个堵车例子讲清拥塞想象你开车出门导航显示前方五公里处有事故但你还是按照原计划一路加速往那儿开。结果就是你和大家一起堵死在路上谁也没办法按时到达。网络里的拥塞是同一个道理TCP 发送方不知道中间路由器已经排队排到极限了还在一个劲儿地发数据包路由器缓冲区被塞满只能把后来的包丢掉。拥塞控制的本质就是让 TCP 发送方学会“根据网络的承载能力调整自己的发送速率”。它不是靠某个中心节点发号施令而是每个发送端通过观察丢包、延迟、重复确认这些间接信号自己判断当前网络是不是“堵了”。这和你开车时不是靠交通广播而是靠导航上的红色路段去猜前方路况非常像。之所以要专门做拥塞控制是因为如果每个 TCP 连接都只管自己拼命发网络很快就会进入“拥塞崩溃”数据包被路由器丢弃发送方等不到 ACK 就重传重传又加剧拥塞最后吞吐量反而趋近于零。计算机网络的教材里常说的“拥塞窗口 cwnd”就是发送方主动限制自己能飞多少数据的一个“油门”。2.2 拥塞窗口和接收窗口两个窗口别搞混很多人刚学到这里会卡住TCP 不是已经有滑动窗口做流量控制了吗接收方会通过窗口字段告诉发送方“你能发多少”为什么还要另搞一个拥塞窗口关键区别在于是谁在限速。流量控制的窗口叫接收窗口rwnd是接收方根据自己缓冲区剩余容量给出的限制防止发送方把接收方内存撑爆。拥塞窗口cwnd则是发送方自己给自己加的限制防止把网络中间的路由器撑爆。真正的发送上限是这两个窗口里较小的那个实际可发送的数据量 min(cwnd, rwnd)这是一个特别核心的公式。我见过不少面试者能把慢启动算法背得滚瓜烂熟但一问他“发送窗口到底由谁决定”就答不上来就是没把两个窗口的关系吃透。接收方只能约束它那一端中间网络里有多少带宽、多少队列、哪些路由器正在高负载只有发送方通过丢包和延迟去推断。2.3 为什么拥塞控制设计起来这么难TCP 发送方看不到网络内部状态这是一个典型的“信息不完整”问题。它拿到的反馈信号非常有限收到 ACK 说明数据正在平稳送达收到重复 ACK 或者超时没等到 ACK则说明网络可能出问题了。麻烦的是这些信号并不绝对无线网络里偶尔丢包和信号不好不代表网络真的拥塞。所以拥塞控制算法本质上是一套“推理机制”用极少的观测信号去猜一个看不见的状态。这也解释了为什么 TCP 版本迭代了这么多年从 Tahoe、Reno、NewReno 到现在 Linux 默认的 CUBIC以及 Google 后来搞的 BBR算法始终在“如何更快地探测可用带宽”和“如何避免把网络塞爆”之间找平衡。基础课里学的四个算法虽然是老技术但它们包含的设计思想——加性增、乘性减、慢启动探测、快速恢复——至今仍是所有拥塞控制算法的底子。3. 核心四件套慢启动、拥塞避免、快重传、快恢复3.1 慢启动不是一直慢是探路先记住一个小前提TCP 建立连接后发送方并不知道当前网络到底能承受多大的发送速率。如果一上来就按接收窗口的最大值猛发网络很可能直接被冲垮。所以 TCP 采用“慢启动”的策略发送速率从很小开始每收到一个 ACK拥塞窗口就增加一个 MSS。这个策略的关键词是“指数增长”。每经过一个 RTT往返时间cwnd 就翻一倍初始窗口如果是 1 个 MSS经过 1 个 RTT 变成 2再经过 1 个 RTT 变成 4再来一个 RTT 变成 8。3 个 RTT 之后是初始的 8 倍10 个 RTT 后是 1024 倍。这就是为什么教材上说慢启动“增长很快”它只是在初始阶段慢慢试水真正跑起来后增长效率非常惊人。慢启动不会无限地指数增长下去它遇到一个阈值就叫 ssthresh慢启动阈值。当 cwnd 小于 ssthresh 时处于慢启动阶段当 cwnd 达到或超过 ssthresh 时就转入拥塞避免阶段。具体到 Linux 实现初始 ssthresh 可以很大甚至大于接收窗口所以实际连接建立后通常先经历一段很短的慢启动等 cwnd 撞到 ssthresh 或者出现丢包才开始“收敛”。注意现代 TCP 的初始拥塞窗口已经不限于 1 个 MSSRFC 6928 建议初始窗口设为 10 个 MSS。所以“慢启动”名字听着慢实际上一开始就有一定的突发能力适用于绝大多数宽带和移动网络。3.2 拥塞避免从指数换成线性进入拥塞避免阶段后cwnd 不再每个 RTT 翻倍而是每个 RTT 只增加 1 个 MSS。如果以“收到每个 ACK 增加”来算就是每收到一个 ACK 增加MSS * (MSS / cwnd)的字节数。这个线性增长很克制让发送速率慢慢逼近网络瓶颈不至于像慢启动那样瞬间冲过头。为什么把指数改成线性因为慢启动本来就是用来快速“探路”的探到 ssthresh 附近后说明我们已经接近网络能承受的边界了再指数增长就太危险。拥塞避免阶段的原则是小步试探平稳靠近。这个阶段最理想的情况是 cwnd 一直缓慢增长直到某一天网络出现丢包然后进入下一轮的“减窗”过程。教材里常写“加法增大、乘法减小”前者就是拥塞避免阶段每个 RTT 增加 1 个 MSS后者指的是发生拥塞后 cwnd 按比例减半甚至清零。这套机制把网络推断和速率调整绑定在一起没丢包就证明当前速率还受得住那就继续加一旦丢包就默认网络已经到了临界点立刻降速给网络“喘口气”的时间。3.3 快重传别傻等超时正常传输中接收方每收到一个数据段就会回复一个 ACK 表示“下一个期待收到的字节序号”。如果中间某个数据段丢了接收方每收到一个乱序的数据段都会重复发送同一个 ACK告诉发送方“我还在等我缺的那一段”。当发送方收到 3 个重复的 ACK 时几乎可以断定那个数据段已经丢了这时无需等待超时立即重传丢失的数据段这就是快重传。这里有个容易忽略的细节为什么不等到超时再重传非要搞个“3 个重复 ACK”因为超时等待时间 RTO 通常比 RTT 大不少现代网络的 RTO 一般至少 1 秒而 3 个重复 ACK 可能在几个 RTT 之内就会出现比如 10 毫秒到几百毫秒不等。早一点重传就能早一点把数据补上避免整个连接因为一个丢包一直卡着不动。所以快重传本质上是“用重复 ACK 代替超时作为丢包信号”。值得注意的是收到重复 ACK 不一定代表网络拥塞也可能只是数据包乱序到达。但因为一个 TCP 连接里同一时刻往往有多个数据段在途连续 3 个重复 ACK 已经足够说明问题大概率不是偶然乱序而是真丢了。教材上把这个阈值定为 3既有理论依据也经过了大量实测验证不要随便改成 2 或者 4。3.4 快恢复丢了包不一定要回到解放前较早的 TCP Tahoe 版本遇到拥塞时很“暴力”只要发生丢包就把 cwnd 降到 1 个 MSS重新进入慢启动。这种方式简单可靠但代价很大好不容易探测到的带宽全被清零网络吞吐会断崖式下跌。于是 TCP Reno 加入了快恢复当收到 3 个重复 ACK 触发快重传时发送方认为网络“有点堵但没堵死”没必要从头再来。快恢复的具体行为可以概括为“乘性减但保留一部分”ssthresh 被设置为当前 cwnd 的一半cwnd 也先设置为 ssthresh 加上 3 个 MSS因为这 3 个重复 ACK 说明已经有 3 个数据段被接收方正常收到了等于网络里腾出了 3 个 MSS 的“额度”。之后每收到一个重复 ACKcwnd 临时增加 1 个 MSS允许发送方补发一部分新数据直到收到新的 ACK才把 cwnd 降到调整后的 ssthresh然后进入拥塞避免阶段。这段逻辑背起来有点绕我用大白话翻译一下网络虽然丢了包但还能收到重复 ACK说明通信链路没有完全中断所以不用跌到 1而是砍半之后再慢慢往上加。对比 Tahoe 的“清零重启”Reno 的“减半恢复”显然更高效这也是为什么考试里经常拿这两个版本做对比。NewReno 后来又改进了处理一个窗口内多个数据包丢失的情况但基础思想仍然是“减半 线性恢复”。4. 从卷子到网络用抓包和实验把算法看明白4.1 Wireshark 里看到的不只是“重传”很多人以为抓包就能直接看到 cwnd这是个误解。Wireshark 抓的是网卡上的数据包而 cwnd 是 TCP 发送端内部的一个状态变量不会直接写在报文头里。你抓包能看到的是 cwnd 变化后的“结果”——比如突然出现大量重传、重复 ACK、RTO 超时。我建议复习时这样玩开两个虚拟机或者直接在本机用 tcpdump 抓包然后用tc模拟网络丢包和延迟。先正常下载一个大文件再在链路上增加丢包对比前后抓包文件的变化。你会发现当有 10% 的丢包时抓包里会出现成片的 duplicate ACK发送方很快重传缺失数据段同时传输速度明显下降。这个现象就是教材里快重传、快恢复在现实中的映射。Wireshark 里我用得最多的过滤器是tcp.analysis.retransmission和tcp.analysis.duplicate_ack。前者直接标记出重传的数据段后者标记重复确认。打开一个带丢包的 pcap 文件先用tcp.analysis.flags全局筛一遍再按时间排序很容易看到一个丢包引发的连锁反应重复 ACK 出现若干条随后一条重传包跟上接着恢复传输。4.2 用 tc 模拟丢包观察吞吐和重传想亲手感受拥塞控制不需要花钱搭复杂环境Linux 上用tc这个命令就够了。比如给本地网卡加 10% 丢包和 100ms 延迟sudo tc qdisc add dev eth0 root netem loss 10% delay 100ms加完之后用任何 TCP 下载工具拉一次数据然后对比正常情况下的速度。你会发现加了丢包后TCP 传输的吞吐量远远不是“正常速度的 90%”而可能直接掉到原来的三分之一甚至更低。原因就是拥塞控制算法一旦检测到丢包会把 cwnd 减半甚至重新进入慢启动发送速率被一次次“打回去”。测试完记得把规则删掉sudo tc qdisc del dev eth0 root如果你用的是ss -tin还能看到更细的信息。ss -i会打印 TCP 连接的窗口、RTT、RTO 等参数其中cwnd和ssthresh都会直接显示出来。在丢包场景下你会看到 cwnd 的数值像过山车一样上下跳动丢包时骤降无丢包时又按拥塞避免的节奏慢慢爬升。这段实验做完你再回头看课本里的公式会通透很多。4.3 理解“丢包 拥塞”这个基本假设所有经典拥塞控制算法都建立在一个假设上网络丢包是因为中间路由器太忙缓冲区一旦满了就把新到的数据包丢弃。这个假设在有线网络上大体成立但在无线网络、卫星链路、共享 Wi-Fi 环境下并不完全准确因为电磁干扰、信号衰弱也会造成丢包。这个知识点在很多期末题里不会直接考但实际工作中特别重要。我记得有一次排查一个远程服务“时快时慢”的问题抓包一看大量重传重传率超过 5%。第一反应是带宽不够后来查无线网卡信号发现是天线老化导致的不稳定丢包。如果当时无脑认为是拥塞调低发送速率或者加宽带问题根本治不好。从算法层面说这就是“丢包不完全是拥塞”的现实意义。CUBIC、BBR 这些新算法本质上都是在尝试修正“丢包就等于拥塞”这个生硬假设让 TCP 在丢包率较高的链路上也能保持比较好的吞吐。基础课可以只讲 Reno 系列但心里要知道这些假设的边界在哪。5. 期末、408、保研、面试的高频考点速查5.1 五道必背简答题第一题简述慢启动的原理。答出“指数增长”“从 1 个 MSS 开始”“每个 RTT 翻倍”“遇到 ssthresh 转入拥塞避免”就能拿分最好再补充一句“现代实现初始窗口可能是 10 个 MSS”。第二题拥塞发生后 cwnd 和 ssthresh 怎么变分两种情况如果是超时重传ssthresh 设为当前 cwnd 的一半cwnd 重置为 1 个 MSS重新慢启动如果是收到 3 个重复 ACK则 ssthresh 设为 cwnd 的一半然后进入快恢复。考试最坑的就是把这两种情况混在一起说。第三题为什么快重传用 3 个重复 ACK 而不是 1 个因为 1 个重复 ACK 很可能是数据包乱序到达导致的不足以判定丢包3 个重复 ACK 说明后面至少有三个数据段已经到达丢包概率高可以安全重传。第四题快恢复和慢启动的区别。快恢复恢复后的起点是新的 ssthresh而不是 1它不重新执行指数增长而是从 ssthresh 附近直接进入线性增长。核心是“减半但不归零”。第五题TCP 如何判断网络发生拥塞主要通过两个信号RTO 超时以及收到 3 个重复 ACK。前者认为是严重拥塞后者认为是一般性拥塞。有些教材还会补充说 RTT 增大也是一种拥塞信号但在经典算法里不作为主要触发条件。5.2 一张表搞定核心参数变化事件cwndssthresh后续状态慢启动阶段收到 ACK每个 RTT 翻倍不变cwnd 到 ssthresh 后进入拥塞避免拥塞避免阶段收到 ACK每个 RTT 1 MSS不变保持线性增长发生超时重置为 1 MSS设为丢包前 cwnd 的一半重新慢启动收到 3 个重复 ACK设为 ssthresh 3 MSS设为丢包前 cwnd 的一半进入快恢复快恢复后收到新 ACK设为 ssthresh不变进入拥塞避免背这张表之前先理解一个关系cwnd 是发送速率的核心ssthresh 是“快慢状态”的分界线。超时等于“道路完全堵死”所以要回到起点重新探路重复 ACK 等于“堵了一部分但还在走”所以只是减半再缓慢恢复。5.3 最容易丢分的三个误区第一个误区把流量控制和拥塞控制混为一谈。流量控制是接收方怕自己处理不过来拥塞控制是网络怕自己被塞爆两者由不同的“信号”驱动一个是接收窗口一个是丢包和 RTT。答题时一定要分清楚。第二个误区认为收到重复 ACK 时 cwnd 直接减半。准确说法是先更新 ssthresh进入快恢复之后 cwnd 先等于 ssthresh 3 MSS不是简单地减半。这个“3”很容易被忽略但它恰好体现了快恢复的设计用意已经有 3 个数据段被接收网络空出了空间。第三个误区把三次握手、四次挥手当成拥塞控制的一部分。三次握手解决的是连接建立时的“双方是否在线”问题四次挥手解决的是连接释放问题拥塞控制解决的是“连接建立后发多快”的问题。考试里的简答题如果串了正确答案也容易被判定为逻辑不清。还有一个面试中常问的延伸题如果网络一直不丢包拥塞窗口会无限增长吗答案是理论上会但实际到达接收窗口或者链路瓶颈时要么被 rwnd 卡住要么网络开始排队丢包所以不可能无限增长。这个问题考的就是 min(cwnd, rwnd) 有没有真正理解。6. 实际网络排查里的拥塞控制影子6.1 先看重传率别一上来就 ping遇到“应用卡顿”“下载慢”“视频加载不出来”这类问题很多人的第一反应是 ping 一下看通不通。但 ping 用的是 ICMP不能代表 TCP 的真实传输质量。更稳妥的做法是用ss -tin查看 TCP 连接的重传、RTT、cwnd或者用 tcpdump 抓包统计重传率。重传率高通常意味着网络中确实存在丢包TCP 的拥塞控制已经把 cwnd 压低了很多所以吞吐上不去。不要小看 cwnd 这个数字它低的时候即使链路带宽再大发送方也只能像挤牙膏一样一点点发。检查的时候我会同时看两个指标RTO 平均值和重传包占比。如果重传占比超过 2%网卡或链路很可能已经有明显问题。排查现场经常用到的一个命令组合ss -tin | grep -E cwnd|rtt|retrans当然ss的输出里没有叫 “retrans” 的字段实际上要看重传计数更常用的还是抓包统计。用 tcpdump 抓 30 秒流量再用 Wireshark 打开统计tcp.analysis.retransmission标记数量比靠肉眼看命令行输出直观得多。6.2 用 cwnd 判断“为什么快不起来”我记得帮朋友排查过一个内部工具上传慢的问题客户端到服务器在同一机房按理说带宽很足但文件传输速度始终上不去。抓包后发现一个奇怪现象发送方的 cwnd 长期只有几百字节远小于接收窗口。后来一查是客户端所在虚拟机网卡模式的丢包率高得离谱TCP 每次还没把 cwnd 涨起来就触发丢包被一次次打回原型。拥塞控制的“探测机制”在这种场景下反而成了拖后腿的因素——它把随机丢包当成了网络饱和信号不断降速。最后换了虚拟网卡驱动上传速度马上恢复正常。这个案例给我们的启发是拥塞控制是保护网络的重要手段但当成因不在“拥塞”而在“链路质量”时不能只靠调 TCP 参数解决还得从物理层、链路层找原因。普通业务方遇到这种问题优先检查网卡丢包、交换机端口统计、无线信号强度都比调内核参数更有效。6.3 线上服务常见的“拥塞窗口恢复慢”还有一种很常见的坑TCP 长连接长时间空闲后再次发数据时 cwnd 已经降回初始值如果数据量比较大就会感觉第一波请求明显偏慢。Linux 内核里有一个tcp_slow_start_after_idle参数默认开启就是用来控制这种情况的。如果公司内部网络质量稳定RTT 很低可以关掉空闲后慢启动让连接保留下来的 cwndsysctl -w net.ipv4.tcp_slow_start_after_idle0这个参数不是银弹它本身也是“拥塞状态是否应该被遗忘”的策略选择。对于内网非常稳定的服务关掉确实能减少首包延迟但在公网、移动网络下直接沿用旧 cwnd 可能造成突发发送反而触发丢包。所以调这个参数一定要结合具体场景先观察再动手。我个人在实际排查中的体感是拥塞控制就像一个“交通管制系统”平时感觉不到它的存在一旦网络出现抖动它的反应速度和恢复能力直接决定了用户体验。学这一章的时候别只背那几个算法的名字和公式多想想每个决策背后的取舍。等你哪天真在线上看到重传率飙升、传输速率骤降会突然明白课本里那些简洁的图示原来描述的正是这些真实世界里的网络故事。