
1. 拥塞控制到底在解决什么问题1.1 网络拥塞是天然的路况问题有一次我在内网传一个几十个G的备份包交换机是千兆的服务器网卡也是千兆的理论速度能跑到100多MB/s结果实际传输只有20多MB/s。我一开始怀疑是磁盘IO瓶颈查了半天没找到问题最后抓包才发现是TCP的拥塞控制算法在主动限速。这件事让我对TCP拥塞控制的认知彻底改变了——它不是教科书上那种只存在于考卷里的概念而是每个做网络编程、运维、后端开发的人迟早会撞上的现实问题。计算机网络里最反直觉的一件事就是发送方的网卡明明能发得很快但它必须主动克制自己不能一口气把数据全塞进网络。原因很简单网络是个共享资源从你的电脑到目标服务器之间要经过交换机、路由器、运营商骨干网一大堆节点每个节点的缓存和处理能力都是有限的。所有人都拼命发数据中间节点的缓存就会被打满新到的数据包只能被丢弃。在TCP协议里丢包意味着发送方要重传重传又加重了网络负担导致更多丢包最后整个网络就像早高峰的环路——谁也别想走。这就是拥塞congestion的本质网络中的流量负载超过了中间节点的处理能力。拥塞控制要做的就是让每个发送方自觉控制自己的发送速率既能尽量榨干可用带宽又不要把网络堵死。这跟堵车时交警限流是一个道理——你一个人的车能开多快取决于整条路能承载多少车而不是你的油门踏板能踩多深。很多人容易把拥塞控制和流量控制搞混。流量控制是端到端的防止发送方太快、接收方处理不过来它看的是接收方的窗口大小拥塞控制是全局的防止所有发送方加起来太快、网络中间节点扛不住它维护的是一个独立的拥塞窗口cwnd。实际发送速率受两者共同约束发送方每次最多发min(接收窗口拥塞窗口)的数据量因此一个窗口满了就算另一个还有余量传输也得停下来。1.2 丢包、延迟与带宽的三方博弈理解拥塞控制之前得先建立一条基础认知TCP判断网络是否拥塞最核心的信号是丢包而丢包的主要来源有两个一个是网络拥塞导致的缓存溢出另一个是传输错误或链路质量问题。早期设计里几乎默认丢包拥塞后来的现代算法才逐渐把两者区分开。还有一个关键概念叫带宽时延积BDPBandwidth-Delay Product它等于链路带宽乘以往返时延RTT含义是网络上同时在途的数据量上限。举个例子假设链路带宽是100MbpsRTT是100ms那么BDP就是100Mbps×0.1s10Mb约1.25MB。这意味着发送方在收到第一个ACK之前理论上最多可以发1.25MB数据而不会浪费带宽。如果拥塞窗口比BDP小链路就喂不饱带宽白白浪费如果拥塞窗口比BDP大太多多余的数据就会堆积在中间节点缓存里一旦缓存溢出就只能丢包。拥塞控制本质上是让cwnd无线逼近当前网络的BDP同时跟随网络状态的变化动态调整。这也是为什么不同场景对拥塞控制的诉求差别很大。数据中心内部RTT可能只有几百微秒跨省长途传输RTT可能有几十毫秒跨国线路甚至上百毫秒。同样的算法在局域网里表现优异的参数到了长肥网络高带宽、高延迟链路上可能就是灾难。所以说拥塞控制不是一套死板的规则而是整个传输层协议栈里最依赖因地制宜的部分。2. 四大经典算法逐个过一遍2.1 慢启动指数的探路阶段TCP建立连接之后发送方并不知道当前网络到底有多大的可用带宽如果一上来就按满速发大概率直接把网络打崩。所以TCP设计了一个由慢到快、逐步试探的过程这就是慢启动Slow Start。注意慢启动这个名字有迷惑性实际它一点也不慢。慢启动的规则是每收到一个ACKcwnd就增加一个MSS最大报文段长度。由于一个RTT内收到的ACK数量等于之前发出的数据包数量所以每个RTT结束时cwnd近似翻倍。假设初始cwnd是10个MSSRFC 6928之后把初始窗口从传统的1~2个MSS调整到了10个那么第1个RTT后变成20第2个RTT后变成40第3个RTT后变成80。指数增长几个RTT就能从10涨到几百上千。我见过有些初学者以为慢启动就是传输慢恰恰相反它是用指数增长快速探测可用带宽的启动阶段。慢启动不会永远增长下去它有两个终止条件。第一发生超时重传说明网络已经吃不消了直接把cwnd重置为初始值重新来过。第二cwnd达到慢启动阈值ssthresh这时候算法切换为拥塞避免阶段进入线性增长。ssthresh在连接建立时一般被设为一个较大的值比如初始窗口的若干倍之后每次发生丢包它都会被更新为丢包时cwnd的一半。这个减半规则在全网被广泛使用是TCP自己摸索出的一个经验值既给了网络喘息的空间又不至于让吞吐量断崖式下跌。实际做测试时可以用抓包工具观察慢启动过程。tcpdump抓取一个新建连接的前几百个包你会发现发送方发出的数据包数量呈明显的波峰-波谷规律每隔一个RTT翻一倍直到出现第一个丢包或达到阈值才趋于平稳。我习惯在本地起一个nginx然后用curl下载一段大文件配合tcpdump加Wireshark看时间序列图整个拥塞控制过程一眼就能看明白比看任何教材都直观。2.2 拥塞避免线性增长的谨慎期当cwnd超过ssthresh之后TCP进入拥塞避免Congestion Avoidance阶段。这个阶段的增长规则从每RTT翻倍变成每RTT增加1个MSS也就是线性增长也叫加法增大Additive Increase。为什么突然从指数变线性因为此时已经逼近网络带宽的极限了再按指数往上冲下一个RTT就会越过临界点引发大量丢包。线性增长虽然保守但能把试探的粒度做得更细让发送速率尽量贴近实际可用带宽又不过冲。拥塞避免阶段要是发生了超时重传处理方式比慢启动激进得多ssthresh更新为当前cwnd的一半cwnd直接降到初始值通常是10个MSS然后重新走慢启动。这一个操作就是著名的乘法减小Multiplicative Decrease加上前面说的加法增大合称AIMDAdditive Increase Multiplicative Decrease策略。AIMD是整个TCP拥塞控制最底层的设计哲学温和地试探上限遇到问题时果断地把负载降下来。这个思想后来也被大量应用在别的领域比如流控算法、负载均衡算法甚至一些业务系统的限流设计里本质上都是慢增快降。这里有个容易混淆的细节超时重传和快速重传虽然都代表丢包了但拥塞控制对它们的响应程度不同。超时重传的触发条件更严重因为超时意味着发送方在RTO重传超时时间内完全没收到任何ACK网络状况可能已经非常糟糕所以必须把cwnd打到最低从零开始。快速重传则意味着接收方还在正常回复ACK只是中间丢了一两个包网络还没完全瘫掉所以响应可以温和一些。具体怎么温和下一节细说。2.3 快速重传与快速恢复把损失降到最低经典的TCP拥塞控制由四个算法组成慢启动、拥塞避免、快速重传Fast Retransmit、快速恢复Fast Recovery。前两个负责常规状态下的窗口调整后两个负责丢包之后的紧急处理。快速重传的触发条件是收到3个重复的ACK。TCP接收方对乱序到达的数据包会回复对最后一个按序到达字节的ACK也就是重复ACK。当一个数据包丢失后续即使有大量数据包到达接收方也只能反复回复同一个ACK。发送方连续收到3个重复ACK基本可以断定某个包丢了于是不等超时立刻重传那个丢失的包。这比傻等RTO超时快得多能显著降低重传延迟。快速恢复的作用更大。传统方案里一旦发现丢包就把cwnd降为初始值重走慢启动这在网络状况尚可的情况下太激进白白浪费带宽。快速恢复采取折中ssthresh和cwnd都减半乘法减小然后从这个减半后的cwnd继续增长而不是打回起点。用大白话说就是发现丢包我承认网络很堵减半以示敬意但我不清零因为我还有重复ACK证明链路还能通继续线性增长试探。快速重传和快速恢复是配合使用的但它们只对单个包丢失的情况表现良好。如果一趟传输里丢了多个数据包Reno算法就有点抓瞎了它每收到一个重复ACK就重传对应的包但多个包丢失时无法精确定位所有丢失的包往往还是得靠超时兜底。这个问题直接催生了NewReno算法——它在快速恢复期间记录下已经重传的最高序列号只要收到对应ACK就认为这个包被成功接收然后继续处理下一个可能的丢包。NewReno把每次RTT最多恢复一个丢包改进为一个快速恢复周期内可以恢复多个丢包对无线网络这类随机丢包率较高的场景改善明显。3. 现代TCP是如何进化的3.1 Reno到NewReno修复一个经典缺陷看到这里你应该也能感觉到Reno这套1980年代末设计的算法放到今天的高带宽、高延迟环境下有点力不从心。Reno最大的问题有两个一是丢包后恢复速度太慢导致吞吐量剧烈波动二是把所有丢包都归结为拥塞在无线网络这种随机丢包率较高的链路上表现得极差。NewReno算是Reno的直接改进版它解决了我上面说的多包丢失问题。但NewReno仍有一个短板恢复过程是按RTT逐个恢复丢失包的如果一个窗口内丢了几十个包整个恢复周期会很漫长。在RTT较大的链路上这个恢复期间吞吐量几乎为零严重影响用户体验。我在一个跨洋链路上测试过高峰期网络质量抖动Reno连接的有效吞吐量能掉到正常值的十分之一抓包看到几乎全是快速恢复状态下的走走停停。后来出现的一些方案尝试在首次检测到丢包时就把处理窗口处理得更彻底比如某些变体会在快速恢复期间把cwnd设为ssthresh而不是减半后继续减减少了恢复期的带宽浪费。不过这些改进都还在Reno框架内打转真正带来质变的是下一代的CUBIC和BBR。3.2 CUBIC高带宽长肥网络的默认选择CUBIC被称为三次方算法因为它的窗口增长曲线是一个三次函数。它在2008年左右被Linux内核接纳并成为默认的拥塞控制算法一直用到现在。设计CUBIC的核心动机是解决Reno在高BDP网络上的一个致命缺陷Reno的窗口增长取决于RTTRTT越大窗口恢复到丢包前水平需要的时间越长高带宽资源就被白白浪费。CUBIC的窗口增长与RTT无关它只看距上次丢包事件过去了多少时间。具体机制可以简单理解为丢包发生后CUBIC记录下当时的cwnd值Wmax然后按照一个三次函数曲线增长。特点是在接近Wmax时增长放缓因为这附近被认为接近网络瓶颈要谨慎试探突破Wmax之后进入快速增长区因为不确定网络负载是否已释放可以通过快速试探找到新的平衡点到达峰值后再放缓。这个先快后慢再快的过程让CUBIC在高速长距离链路上能迅速找回丢包前的高吞吐量同时保持良好稳定性。如果是考试复习CUBIC这个名字至少要知道是Linux默认算法以及它的核心思想基于时间的窗口恢复即可。但作为工程实践者我建议你多了解一点在普通数据中心或家庭宽带场景CUBIC和Reno的表现差距其实没有想象中那么大因为那里的RTT短、队列深度有限CUBIC的优势主要在RTT超过二三十毫秒的高带宽链路上才明显。3.3 BBR不再拿丢包当唯一信号如果说CUBIC是对Reno的修补那Google的BBRBottleneck Bandwidth and Round-trip propagation time就是从根上换了一套思路。BBR的核心观察是在深队列网络里丢包并不是一个可靠的拥塞信号。很多商用网络设备会配置较大的缓冲队列来吸收突发流量在这些队列被填满之前丢包不会发生但排队延迟已经急剧上升——这就是所谓的Bufferbloat缓冲膨胀问题。BBR认为与其等丢包了才降速不如主动测量瓶颈带宽和最小RTT以这两个值为依据直接算出一个最优发送速率。BBR的工作方式是周期性探测先按估算带宽发送数据测量实际带宽然后主动提高速率看带宽是否还能增长如果不再增长就说明已经到达瓶颈退回到估算带宽附近。同时它持续监听最小RTT当RTT明显变大时即使没丢包也会降速以减少网络中的排队。这套机制的优点是对丢包不太敏感抗抖动能力强尤其在跨国链路、卫星链路、无线网络这些场景下BBR吞吐量常常能比CUBIC高出一大截。我用BBR跑过一次跨国传输测试同一条链路上CUBIC稳定在2~3MB/s切换到BBR后直接跳到8MB/s以上。代价是BBR对网络公平性有一定影响——它占用的带宽份额可能比传统算法更大在共享链路上可能会挤占其他TCP流。因此在实际生产环境里混用不同拥塞控制算法的网络需要做公平性测试不能无脑切换。Linux内核在4.9版本之后带了BBR你只需要一条sysctl命令就能启用。4. 动手实践在Linux里实际调一次4.1 查看当前TCP状态与拥塞控制算法纸上谈兵再多不如动手看一次实际效果。第一步先看看你的Linux系统当前用的是哪个拥塞控制算法sysctl net.ipv4.tcp_congestion_control输出一般是net.ipv4.tcp_congestion_control cubic。这是绝大多数发行版的默认配置。想查看内核支持哪些算法sysctl net.ipv4.tcp_available_congestion_control如果你的内核版本较老或者没加载相应模块might只能看到reno和cubic。想加载BBR需要检查模块是否存在modprobe tcp_bbr lsmod | grep tcp_bbr需要说明的是拥塞控制算法是per-socket粒度的也就是每个TCP连接都可以用不同的算法。系统级默认值只是新建连接没特别指定时用的算法。所以调优时可以全局改也可以针对特定应用设置。4.2 修改拥塞控制算法的两种方式临时修改重启失效sysctl -w net.ipv4.tcp_congestion_controlbbr持久化修改写入配置文件echo net.ipv4.tcp_congestion_controlbbr /etc/sysctl.conf sysctl -p第二种方式在服务器重启后依然生效生产环境推荐这么做。不过要提醒一句在共享同一条出口带宽的多台服务器间尽量不要只改其中一台的算法可能出现一台机器抢占了大量带宽、其他机器饿死的公平性问题。你在内网测试环境怎么折腾都行上生产之前务必评估整体影响。除了切换算法还有几个跟拥塞控制相关的参数值得关注sysctl net.ipv4.tcp_slow_start_after_idle这个参数默认为1含义是TCP连接空闲一段时间后把cwnd重置回初始值。对短连接为主的Web服务来说每次请求之间都有空闲这个重置其实没什么影响但对于长连接做连续传输的场景比如数据库同步、日志采集空闲几秒再传输时被重置窗口会白白损失吞吐量。设置成0可以保持已有窗口sysctl -w net.ipv4.tcp_slow_start_after_idle0还有初始窗口大小Linux默认是10个MSS也就是10×1448字节大概14KB。对大多数场景这个值够用但如果你做的是单连接传大文件的场景适当调大初始窗口可以加快启动阶段的探测速度。这个参数不直接暴露在sysctl里需要用ip route命令设置ip route change default initcwnd 20 initrwnd 20注意initrwnd是接收窗口一般不用动主要调initcwnd。4.3 实际调优场景内网海量文件传输给你讲一个我自己调优的真实案例。之前给客户做数据迁移两台服务器之间要走专线同步几十TB的数据RTT大概是5ms带宽是万兆。刚开始用默认CUBIC理论上应该能跑到接近1000MB/s实际稳定在300MB/s左右。为什么因为CUBIC的曲线恢复需要时间而文件传输过程中不可避免地会偶发丢包哪怕万兆光模块偶尔抖动一下每次丢包后的恢复期间吞吐都会掉一大截。我的处理步骤是先抓包确认丢包率很低用ss -i看每个连接的retr统计结论是偶尔丢一两个包。把拥塞控制算法切成BBR试跑吞吐立刻涨到700MB/s以上。再把tcp_slow_start_after_idle设为0进一步稳定长连接传输效率。同步调整发送方socket buffer大小通过setsockopt设置SO_SNDBUF/SO_RCVBUF确保内核socket缓冲区足以容纳整个BDP的数据量而不是成为新的瓶颈。BDP在这里是10Gbps×0.005s6.25MB如果socket缓冲区只有几MB发送窗口永远开不大。很多人光盯拥塞算法忽略了socket缓冲区这个隐形瓶颈实际调整后效果立竿见影。另外一个容易踩的坑是服务器做文件传输时最好配合增大TCP窗口缩放因子也就是net.ipv4.tcp_window_scalingLinux默认开启但某些防火墙或中间设备可能禁用了这个特性导致TCP最大窗口被限制在64KB那不管你怎么调算法都没用。排查时先确认两端是否都启用了窗口缩放。5. 常见故障排查与面试高频考点5.1 常见的TCP连接类报错怎么定位开发后端服务的人几乎每天都能在各种日志里看到TCP相关报错。网上搜计算机网络相关内容时高频出现的一堆报错错误其实大多不是拥塞控制的问题但很多新手会往这上面想。这里我把几种典型的报错整理一下报错信息常见原因排查方向bind: only one usage of each socket address端口被占用或TIME_WAIT状态堆积检查监听端口、netstat查看TIME_WAIT数量connect timeout网络不通、防火墙丢弃SYN包ping/telnet测试、检查iptables/安全组connection reset by peer对端主动RST可能是服务崩溃或应用层拒绝查看对端服务日志、抓包确认RST来源dial tcp ... connection refused目标端口未监听确认服务进程是否存活、端口是否冲突connection closed连接被服务端正常关闭检查应用层keepalive、服务端空闲超时设置这些报错和拥塞控制基本无关真正跟拥塞控制相关的信号在抓包和内核统计里ss -i输出的retr重传次数、cwnd当前拥塞窗口、rtt数值是更直接的观察指标。如果retr持续增长说明有丢包这时候再沿着拥塞控制的方向排查才有意义。5.2 用tcpdump/ss把拥塞控制看出来很多教材讲拥塞控制都是画图但真实网络里怎么看我给你一个最简单的实践路径。先用ss观察单条TCP连接ss -i dport :80输出里有个cwnd字段会实时变化。对一个刚建立的连接你能看到cwnd从10开始指数增长然后变成线性增长中间如果跳变到很小值说明发生了丢包触发快速恢复。多做几次下载测试对比比背书有效多了。想更精细地看慢启动过程用tcpdump抓包tcpdump -i eth0 -w tcp_capture.pcap tcp port 80然后用Wireshark打开在Statistics里的TCP Stream Graphs下选Time-Sequence GraphStevens能直接画出cwnd和时间的关系图。曲线的陡峭段是慢启动平缓段是拥塞避免断崖式下跌是丢包后的窗口收缩。我让学生做拥塞控制实验时都会要求他们跑一次这个图比默写算法流程要深刻得多。5.3 面试和考试复习时怎么抓住重点网上关于TCP拥塞控制的热搜里大量是计算机网络期末复习、保研复习、408计算机网络这类关键词说明很多人是被考试、面试逼着来学这个知识点的。作为过来人我帮你梳理一下复习重点。期末/考研层面谢希仁《计算机网络》和《深入浅出计算机网络》里拥塞控制的核心考点集中在慢启动、拥塞避免、快速重传、快速恢复四个机制的流程以及ssthresh的更新规则。最容易考的是给你一个丢包事件让你画出cwnd随时间的变化图并标注出哪些阶段是慢启动、哪些是拥塞避免。这个一定要在纸上多画几遍千万别只对着电子书看不动手画等于白学。面试层面面试官通常不问流程细节而是问你实际调过拥塞控制吗、线上服务出现过TCP传输性能问题你怎么排查。这时候就算你没真调过也要说出CUBIC是Linux默认算法、高延迟高带宽链路可以尝试BBR、先用tcpdump抓包确认丢包率再决定方案这种实战维度的话。能说出这些比背出四个算法的英文缩写值钱得多。408层面真题基本不超纲把AIMD思想吃透、把ssthresh的更新逻辑理清就够了。特别注意一个高频陷阱题发生超时重传后cwnd和ssthresh各变成多少答案不是固定的减半而是ssthresh变成当前cwnd的一半cwnd重置为初始值一般是10个MSS。快速重传则不一样两者都是减半。这个区别每年都有人错。6. 拥塞控制的应用价值与学习建议6.1 从会考试到会排查的跨越每次带新人做项目我发现一个普遍现象能熟练背出拥塞控制流程的人遇到真实的传输性能问题往往无从下手。原因在于教材讲的是理想化的算法流程实际网络里混杂了接收窗口限制、socket缓冲区大小、中间设备队列策略、应用层读写模式一大堆因素。就像你学了交规不等于会处理真实事故现场。我建议每个做网络方向的人都亲手搭建一次拥塞控制实验台两台Linux机器虚拟机也行中间用tc命令制造延迟和丢包然后对比不同算法下的传输吞吐。命令很简单# 在路由器上一侧网口添加100ms延迟和1%丢包 tc qdisc add dev eth0 root netem delay 100ms loss 1%然后分别用sysctl切换cubic和bbr跑一次大文件传输记录耗时和ss -i里的重传数。这个实验做完你就能从背概念变成看现象再到排查问题时能快速形成假设——这个能力只能靠动手练出来。6.2 常见误区不要把TCP的锅甩给网速不好分享一个我常说的经验很多运维报网络不好其实根本不是网络问题。有一次客户报障说两套系统之间数据同步极慢怀疑是专线质量问题。我上去一看连接确实只有几百KB/s但ping延迟只有2ms丢包率为0。抓包发现tcpdump里满屏的零窗口Zero Window通告再一查对端应用根本没在消费数据接收窗口被应用层堵死了。这跟拥塞控制半毛钱关系没有纯粹是应用层读数据太慢导致接收缓存满TCP被迫通知发送方暂停。类似的常见误区还有把TCP三次握手慢归因于拥塞控制。实际握手阶段的延迟基本取决于RTT和中间设备的转发性能跟cwnd无关。拥塞控制影响的是握手之后的数据传输阶段。排查连接建立慢的问题应该重点查DNS解析、路由路径、防火墙策略而不是动拥塞算法的念头。一段时间以后另一个高频误判是把对端主动RST当成网络丢包。连接被RST后有些抓包分析工具会显示previous segment lost这只是一个猜测性的提示大概率不是真的乱序或丢包而是对端主动断开。遇到RST第一反应应该去看对端应用日志和服务状态而不是怀疑线路质量。6.3 学习资料怎么选更高效关于学习路径市面上的《计算机网络谢希仁版》和《计算机网络自顶向下方法》都是经典教材不值得为了电子书PDF版本纠结太多关键是看的时候别跳章节。拥塞控制这部分建议配合RFC 5681TCP拥塞控制、RFC 8312CUBIC、RFC 793TCP基础一起看RFC虽然条文枯燥但它是所有教材的源头遇到教材讲不清楚的细节查RFC最靠谱。视频资源方面湖科大教书匠和深入浅出系列的拥塞控制讲解都做得不错适合零基础入门。但对408和保研复习来说光看视频不够一定要做题尤其是历年真题里关于画出拥塞窗口变化曲线的题型至少刷三遍。这类题既考理解又考细节是最容易拉开分差的。我做评判一个知识是否真正掌握的标准很简单能不能用自己的话把慢启动和拥塞避免的区别讲给完全不懂网络的人听并且对方能听懂。屡试不爽很多觉得自己学明白了的人一开口就露馅——不是背错ssthresh的更新规则就是把快速重传的触发条件说成收到重复ACK就重传实际是3个重复ACK。能把概念讲得越简单说明你理解得越扎实。我个人在实际排查网络问题时的体会是TCP拥塞控制就像公路上的交通规则规则本身简单清晰但到了真实路况里什么时候该加速、什么时候该减速光靠背规则是开不好车的。多抓包、多看ss输出、多对比不同算法在同一链路下的表现差异才能逐渐形成手感。下次再遇到传输性能问题时你至少能分清这到底是不是拥塞控制的锅而不是上来就怀疑网线没插好。