尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

TCP拥塞控制详解:慢启动、拥塞避免、快重传与快恢复

TCP拥塞控制详解:慢启动、拥塞避免、快重传与快恢复 TCP拥塞控制是运输层最绕、也最容易被问崩的一块内容。不管是期末复习、考研408还是工作中排查网络为什么忽快忽慢最后都会撞上这四个算法慢启动、拥塞避免、快重传、快恢复。很多同学把这几个概念背得滚瓜烂熟但真遇到“cwnd突然减半”“带宽明明很高吞吐却上不去”的时候又完全对不上号。这篇我打算把TCP拥塞控制从原理到实操整个过一遍用具体的数值例子和抓包观察把脉络捋清楚适合正在复习计算机网络基础、备战考研或者工作中想搞懂网络为什么慢的开发运维同学。1. 拥塞控制到底在解决什么问题1.1 拥塞是怎么发生的先说清楚这里说的“拥塞”到底是什么。我们平时上网数据不是直接从发送端飞到接收端的中间要经过一串路由器。每台路由器都有缓存buffer数据包到了之后先在缓存里排队再由转发引擎送往下一跳。如果流量太大路由器缓存被填满后续到达的数据包就只能被丢弃。你可能觉得丢包无所谓TCP本来就有重传机制丢了再传不就行了。问题在于重传会进一步把更多数据引入网络让拥塞更加严重。这就是拥塞崩溃congestion collapse的雏形——大量数据包在途中被丢弃接收端收不到完整数据发送端不断重传网络吞吐量趋近于零。TCP拥塞控制的核心目标就是让发送端在“让更多数据进入网络”和“避免压垮中间路由器”之间找到一个平衡点。这里要区分一个概念拥塞是网络的整体状态不是接收端的个体状态。你可以把网络想象成一条多车道的公路路由器就是收费站。发送端是高速入口接收端是出口。如果所有车都挤进高速收费站的排队缓冲区会溢出后续车辆连进都进不去。TCP拥塞控制就是让每辆车在进入高速前先试探一下现在路上堵不堵我该以多快的速度汇入车流。1.2 流量控制与拥塞控制是两码事这个点几乎每次都会被问。流量控制flow control和拥塞控制congestion control虽然都是控制发送速率但控制的对象完全不同。流量控制是点对点的解决的是“接收方处理不过来”的问题。接收方会在TCP报文段首部写明自己的接收窗口rwndreceiver window告诉发送方“你现在最多再发这么多字节多了我缓存装不下”。它保护的是接收端。拥塞控制是端到端的解决的是“网络中间节点处理不过来”的问题。发送端维护一个拥塞窗口cwndcongestion window通过观测丢包和确认来推测网络的承载能力控制自己注入网络的数据量。它保护的是整个网络路径上的路由器。最终的发送窗口swnd min(rwnd, cwnd)。也就是说发送端能发多少取决于接收端能收多少和网络能承载多少中更小的那个。我在实际排查中见过很多“传大文件到最后越来越慢”的情况就是接收窗口和拥塞窗口没有区分清楚盲目调大了某个参数结果瓶颈压根不在那个位置。1.3 拥塞控制的总框架AIMD整个TCP拥塞控制可以浓缩成四个字AIMD即加性增、乘性减Additive Increase Multiplicative Decrease。没有拥塞时窗口缓慢线性增长加性增检测到拥塞时窗口按比例缩小乘性减。这套思想贯穿了慢启动、拥塞避免、快重传、快恢复四个算法。慢启动负责快速从小窗口爬升到接近可用带宽的位置拥塞避免负责在接近带宽上限后做线性微调快重传负责及时修复单个丢包快恢复负责在丢包后避免吞吐量暴跌。下面我们逐个拆开讲。2. 慢启动和拥塞避免TCP的第一步2.1 慢启动从1开始翻倍增长慢启动Slow Start这个名字有很强的误导性——它其实一点都不慢是“指数增长”。刚开始发送端不知道网络能承受多少数据为了安全起见cwnd初始值设为1个MSSMaximum Segment Size最大报文段长度典型值1460字节。每收到一个确认ACKcwnd就增加1个MSS。注意这里的增长方式。每收到一个ACK就cwnd 1并不是说一个RTT内只能增加一次。假设当前cwnd n在一个RTT内有n个报文段发出它们全部被确认每个ACK都会让cwnd增加1因此一个RTT后cwnd n n 2n。也就是说只要所有报文都顺利被确认每过1个RTTcwnd就翻一倍。我举个例子。假设MSS 1KBRTT 100ms第1个RTTcwnd 1KB发送1个报文段收到1个ACK后cwnd 2KB第2个RTT发送2个报文段收到2个ACK后cwnd 4KB第3个RTT发送4个报文段收到4个ACK后cwnd 8KB第4个RTT发送8个报文段cwnd 16KB四个RTT之后发送速率从1KB/RTT变成16KB/RTT增长非常迅猛。但慢启动不能无限增长下去否则马上就会把网络打爆所以需要一个切换点这就是慢启动阈值ssthreshslow start threshold。2.2 拥塞避免从指数切换到线性当cwnd达到ssthresh时TCP从慢启动切换到拥塞避免Congestion Avoidance。拥塞避免阶段采用加性增每个RTTcwnd只增加1个MSS而不是翻倍。为什么到了这个阶段就“保守”了因为慢启动的指数增长是用来快速探测带宽的但探测到一定程度网络可能已经接近容量上限继续指数增长会瞬间造成拥塞。线性增长则是在每个RTT内缓慢试探一点点额外带宽更加温和。拥塞避免的“避免”不是说你永远遇不到拥塞而是指用一种缓慢试探的方式去逼近网络容量上限尽量避免过度冲击网络。实际上拥塞仍然会发生TCP正是通过“遇到拥塞→减小窗口”来反复逼近网络的真实容量。这就是为什么你能在抓包工具里看到TCP吞吐量的曲线呈锯齿状升一点、撞一次、降下来再升。ssthresh的初始值怎么定不同的操作系统和协议栈实现不一样常见的是初始值设置为一个较大的数例如64KB或更大的缓冲区上限或者在握手阶段根据双方缓冲区大小动态调整。在考试和面试中最常考的不是初始值而是拥塞发生后ssthresh如何动态变化——记住每次发生拥塞ssthresh max(2, cwnd/2)然后根据拥塞类型决定cwnd是归1还是减半。2.3 拥塞判定的两条路径TCP靠什么判断网络拥塞了靠丢包。而丢包在TCP中有两种主要表现第一种是超时重传Timeout Retransmission。发送端发出数据后启动一个计时器如果在RTORetransmission Timeout重传超时时间内没收到确认就认为这个报文段丢了。超时通常意味着网络拥堵得比较严重连ACK都无法正常返回。第二种是收到3个重复ACK3 Dup ACKs。假设发送端发了1、2、3、4、5五个报文段其中3号丢失。接收方能收到4和5但TCP要求按序交付接收方只能缓存4和5然后对最后一个按序到达的报文段2号回复ACK。于是接收方每收到一个乱序报文段就重复发送对2号的确认。当发送端连续收到3个重复ACK即第4个确认2号的ACK时可以推断3号丢失了。超时和3个重复ACK对拥塞严重程度的判断不同。超时说明网络可能已经堵死反馈链路也不畅通重复ACK虽然说明有丢包但后续报文能到达说明网络还能传输数据拥塞程度相对较轻。这两种判定路径触发了不同的拥塞处理策略这正是第3章要展开的内容。3. 快重传与快恢复遇到丢包不认输3.1 快重传不等超时就重传如果没有快重传Fast Retransmit发送端丢了一个报文段后只能干等到超时计时器到期才重传。RTO在最坏情况下可能长达几秒这个等待对吞吐量的打击是致命的尤其在高带宽长距离网络所谓的“长肥网络”上一个RTO时间浪费的数据量是巨大的。快重传的思路很简单接收方收到乱序报文段就立即发送重复ACK不等自己需要发送数据时再捎带确认发送端一旦连续收到3个重复ACK立即重传丢失的报文段不需要等超时计时器到期。注意这里的“3个重复ACK”是一个经验性阈值。1个重复ACK可能只是乱序到达比如网络里报文走不同路径顺序轻微错乱2个重复ACK也有可能是乱序。连续3个重复ACK才说明“后续报文都到了就是中间缺一段”此时判定丢包比较可靠。我在实际抓包时见过很多初学者在Wireshark里看到重传还一脸茫然“这个包明明被回复ACK了呀为什么标记为TCP Retransmission”就是因为没有理解快重传的触发逻辑。Wireshark会在数据包前加“TCP Dup ACK”标记并针对同一序号出现多个Dup ACK的情况提示“Fast Retransmission”。3.2 快恢复避免回到起点传统的TCP Tahoe版本在收到3个重复ACK时和超时处理一样ssthresh cwnd/2cwnd 1重新进入慢启动。这样做虽然安全但代价很大——好不容易把窗口从1涨到几十个MSS一丢包全归零吞吐量断崖式下跌。于是Reno版本引入了快恢复Fast Recovery。思路是既然收到的重复ACK说明网络还能传数据毕竟ACK能回来后面的数据还在流动那就不必回到慢启动的起点。Reno快恢复的基本流程是这样的收到3个重复ACK时ssthresh cwnd / 2然后cwnd ssthresh有些实现是cwnd ssthresh 3×MSS因为已经有3个数据段离开网络相当于给“在途数据”腾一点空间接下来进入拥塞避免阶段cwnd每个RTT增加1个MSS线性增长也就是说丢包后窗口只是减半不是归零。这既响应了拥塞信号比之前保守又不会让吞吐量瞬间崩塌。3.3 Tahoe与Reno算法演进史我把Tahoe和Reno在丢包场景下的行为对比一下场景TahoeReno超时ssthresh cwnd/2cwnd 1慢启动同左3个重复ACKssthresh cwnd/2cwnd 1慢启动ssthresh cwnd/2cwnd ssthresh快恢复拥塞避免Tahoe是最早的完整拥塞控制实现它证明了“拥塞后必须退避”的原则。Reno在Tahoe基础上增加了快恢复减少了不必要的吞吐量损失成为很长时间里Linux等系统的默认算法。但Reno有个毛病如果在一个窗口内丢了多个报文段它只能在每个RTT内恢复一个丢包效率很低。于是后来出现了NewReno改进了对部分确认Partial ACK的处理能在同一窗口内恢复多个丢包。再后来出现了Vegas、Westwood等基于时延或带宽估计的算法但这些在课本上通常不会细讲考试默认还是以Reno/NewReno为主。4. 四个算法如何协同运作完整实例与实战观测4.1 一个完整例子从1 MSS开始到拥塞前面把四个算法拆开讲了但它们在实际运行中是连续切换的。我用一个具体的数值例子来演示完整流程假设MSS 1KBssthresh初始为16KB接收窗口足够大不成为限制。第一阶段慢启动cwnd从1KB开始每个RTT翻倍1 → 2 → 4 → 8 → 16当cwnd 16KB时cwnd ssthresh进入拥塞避免第二阶段拥塞避免cwnd线形增长16 → 17 → 18 → 19 → 20 → 21 → 22 → 23 → 24假设cwnd 24KB时发生超时第三阶段超时后的乘法减小ssthresh cwnd / 2 12KBcwnd 1KB重新进入慢启动第四阶段再次爬坡慢启动1 → 2 → 4 → 8 → 12注意到这里cwnd12KB等于新的ssthresh进入拥塞避免13 → 14假设cwnd 14KB时收到3个重复ACK某个报文段丢失但后续报文到达第五阶段快重传快恢复立即重传丢失报文段ssthresh 14 / 2 7KBcwnd 7KB进入拥塞避免之后继续线性增长8 → 9 → 10 ...这个例子覆盖了全部四个算法。你可能会发现一个规律——拥塞避免阶段的线性增长本质上是在试探网络容量而每一次拥塞事件都会把ssthresh拉低导致后续窗口更难爬高。如果网络持续拥塞窗口就会在低位徘徊吞吐量自然上不去。4.2 用Wireshark实际观察TCP窗口变化理论讲了这么多怎么在实际网络里验证我推荐用Wireshark抓包观察这是最直观的方式。第一步抓包。在Linux服务器上执行tcpdump -i eth0 host 目标IP and tcp port 80 -w tcp_cwnd.pcap抓到足够多条TCP报文后用Wireshark打开。第二步在Wireshark里观察TCP Stream。右键任意一个TCP包 → Follow → TCP Stream可以看完整会话。在Statistics → TCP Stream Graphs → Time-Sequence (Stevens)中可以看到发送序列随时间的变化。如果看到一段段“阶梯式”爬升、然后突然下坠的曲线那就是典型的拥塞窗口变化轨迹。第三步关注标记。Wireshark会自动标记“TCP Dup ACK”和“TCP Retransmission”。如果你看到大量重复ACK后面跟着快重传说明发判断丢包并触发了快恢复。如果你看到RTO超时图上表现为一段长时间空白然后重新发起点说明网络拥塞较严重走了超时重传路径。这里有个需要注意的地方Wireshark直接显示的“Window Size”是接收窗口rwnd不是拥塞窗口cwnd。拥塞窗口是发送端的内部状态抓包时看不见。但你可以通过观察发送速率的变化来反推cwnd的增减如果一段时间内发送方每个RTT发出的字节数翻倍就是慢启动如果每个RTT增加一个MSS的速率就是拥塞避免如果速率突然降一半说明发生了乘法减小。4.3 Linux内核里的拥塞控制开关在Linux上拥塞控制算法是可以通过内核参数切换的。我平时排查网络性能问题时第一步就是确认当前用的是哪个算法。# 查看当前拥塞控制算法 sysctl net.ipv4.tcp_congestion_control # 查看内核支持哪些算法 sysctl net.ipv4.tcp_available_congestion_control常见的输出是cubic或者reno。CentOS/RHEL系列默认通常显示cubic这是Linux的默认算法采用三次函数控制窗口增长在高带宽长距离链路上比Reno表现更好。如果你看到的是reno而你的场景是跨机房大流量传输可以考虑切换算法。开启BBR的方法如下# 在/etc/sysctl.conf中加入 net.ipv4.tcp_congestion_control bbr # 生效 sysctl -p # 验证 sysctl net.ipv4.tcp_congestion_controlBBRBottleneck Bandwidth and RTT是Google提出的算法它不再把丢包当作主要拥塞信号而是根据最大带宽和最小RTT构建网络模型在高速网络和有一定丢包率的环境比如无线网络下表现非常惊艳。但要注意切换BBR后如果链路中还有其他TCP Reno/CUBIC流量可能出现公平性问题——BBR会抢占更多带宽。还有一个容易被忽视的参数sysctl net.ipv4.tcp_slow_start_after_idle默认值是1表示TCP连接空闲一段时间后重新进入慢启动状态。对于长连接交互式请求比如数据库连接池这个机制会让空闲后的第一次请求变慢。很多高并发后端会在初始化脚本里把它设为0避免频繁慢启动。4.4 CUBIC与BBR现代改进方向聊到现代Linux默认的CUBIC毕业设计和面试题里经常让人对比。CUBIC的窗口增长不再依赖RTT的线性反馈而是定义一个以时间为自变量的三次函数。它的大致行为是发生拥塞减窗后先快速恢复到拥塞前的窗口值然后在接近上限时放慢增长速度这样既保证高带宽网络的利用率又减少对队列的冲击。BBR则是另一个维度。传统算法只在“发快了→丢包→减窗”的循环里震荡BBR却试图直接测量瓶颈路由器的带宽和传输时延让发送速率收敛到“刚好填满管道但不排队”的状态。我用BBR跑跨洋大文件传输时吞吐量提升非常明显因为普通算法会被少量随机丢包误判为拥塞不断减窗而BBR不把丢包当唯一天条。不过任何算法都不是银弹。考试的时候还是以Reno为主但如果你去公司面试基础架构岗位能讲清楚CUBIC和BBR的差异是很加分的。5. 常见问题速查与面试考点5.1 实战中频繁踩坑的场景在实际开发和运维中TCP拥塞控制的表现很多时候不是“题型”而是真实的性能瓶颈。我把几个高频问题整理出来第一高带宽但吞吐量上不去。这不是简单的拥塞控制能解决的。先看接收窗口和缓冲区使用netstat -s统计TCP接收缓冲溢出、丢包重传次数再用ss -i查看每个TCP连接的cwnd和RTT。如果确认是窗口限制可以调大net.ipv4.tcp_rmem和net.ipv4.tcp_wmem的范围或者确认双方都开启了TCP Window ScalingRFC 7323否则窗口最大就只有64KB传大文件必然卡在窗口上限。第二无线网络环境频繁掉速。无线链路丢包率高传统拥塞控制会把随机丢包当成拥塞信号动辄减半窗口。这种场景下改启用BBR或者CUBIC都能有效降低误判带来的吞吐量损失。第三长连接空闲后首请求特别慢。这个就是之前提到的tcp_slow_start_after_idle在作怪关闭这个内核参数同时在应用层做连接保活避免连接被中间设备回收。第四流量监控里看到大量重复ACK。这不一定代表拥塞也可能是乱序、网卡多队列导致、中间设备缓存的路径不同。排查时不要只盯着“重复ACK”这一个指标要结合重传数量、RTT变化和丢包率一起看。5.2 面试和考试最常问的几个点把高频考点整理成速查表考前过一遍很有帮助问题关键回答要点慢启动为什么叫“慢”名字有误导实际指数增长从1个MSS起步防止一开始就冲击网络ssthresh什么时候被改拥塞发生时ssthresh cwnd/2初始值由协议栈设定超时和3个重复ACK分别怎么处理超时cwnd1重新慢启动3个重复ACK快重传快恢复cwndssthresh快恢复为什么cwnd不归1因为重复ACK说明数据还能流动说明拥塞相对较轻没必要把窗口清零拥塞避免为什么每次只加1个MSS为了在接近网络容量上限时做“温和试探”避免再次引发拥塞流量控制和拥塞控制的区别流量控制保护接收端rwnd拥塞控制保护网络路径cwnd发送窗口取二者的较小值为什么TCP吞吐曲线是锯齿状AIMD策略加性增长探测带宽乘性减少释放压力周而复始真题里还有一种画图题给你一个初始ssthresh让你画出cwnd随RTT变化的折线。这种题最稳妥的做法是手推一步步的数值先标出慢启动的翻倍点再标出进入拥塞避免后的线性增长最后标出拥塞事件窗口的下坠。我个人在实际操作中的体会是TCP拥塞控制初学觉得算法多、状态复杂但把它当成一套“放探针→看反馈→调窗口”的闭环控制逻辑就好懂了。考试也好、排查故障也好记住一条主线——一切行为都是围绕“探测网络可用带宽并避免压垮网络”来设计的。另外在Linux上做性能调优时不要一上来就改算法先用ss -i和netstat -s定位瓶颈到底在窗口、在丢包还是在接收端再决定动哪个参数。最后再分享一个小技巧自己动手在本地起一个服务用tcpdump抓一次从建连到传输完整的包对照这章讲的四个切换点逐个标记比背十遍课本都管用。
返回列表