资讯详情

TCP拥塞控制进化史:从Tahoe、Reno到CUBIC与BBR

📅 2026/9/16 9:15:58 | 华诺云谱 👁 阅读
TCP拥塞控制进化史:从Tahoe、Reno到CUBIC与BBR
1. 拥塞控制到底在管什么1.1 拥塞控制解决了网络里最常被误解的那个问题先说一个现象很多人应该都有同感宽带明明升到了千兆平时下载大文件速度也还行但只要一到晚高峰视频会议开始卡顿、网页加载转圈而路由器后台看着流量并不高。问题不在带宽而在“拥塞”。拥塞控制就是TCP协议里负责处理这种局面的一套机制。它解决的核心矛盾是发送端不知道网络什么时候会堵但如果不控制发送速度数据就会在路由器或交换机的缓冲区里堆积成山轻则延迟飙高重则直接丢包。说到底拥塞控制是一套“发送端自我约束”的规则让网络里的每条TCP流既尽量占满可用带宽又不要冲垮中间设备的缓存。我经常用一个比喻把网络链路看作一条高架桥拥塞控制就是给每个司机装上节奏器。桥上不堵时加速跑一旦前方车流变慢丢包或者RTT增大就主动减速避让。没有这套机制大家都不踩刹车桥瞬间就堵死了。这篇文章要聊的就是从早期Tahoe到Reno再到当前Linux默认的CUBIC以及谷歌开源的BBR这四代拥塞控制策略各自的思路、优劣和演进逻辑。适合刚接触TCP协议的开发者、做网络调优的运维以及所有想搞明白“为什么网速忽快忽慢”的技术爱好者。1.2 三个绕不开的基本参数与一条核心曲线在展开具体算法之前必须先交代几个TCP拥塞控制领域的基础参数。不管哪种算法最终操作的都是这三个量cwnd拥塞窗口发送端允许同时在途的数据量单位通常是MSS最大报文段大小cwnd越大同一时间网络上就能有越多数据。ssthresh慢启动阈值慢启动阶段和拥塞避免阶段的分界线cwnd低于它时指数增长超过以后切换成更保守的增长方式。RTT往返时延一个数据包从发出到收到确认ACK所花的时间它决定了窗口大小和实际吞吐之间的关系。还有一条贯穿所有算法的核心曲线就是AIMD加性增乘性减。没发生拥塞时每经过一个RTTcwnd增加一个固定值线性增长一旦探到拥塞信号早期是丢包就把cwnd按比例砍掉。这就像开车路况好时慢慢踩油门遇到状况猛踩一脚刹车。这几代算法最大的区别就在于“什么时候判断拥塞”以及“踩多深的刹车”。Tahoe和Reno依赖丢包CUBIC把时间因素引入决策BBR干脆直接建模测带宽和时延。看完后文你会明白这不仅仅是参数调整的问题而是看待网络这件事的视角都变了。1.3 为什么一代接一代换信号来源的变迁我把拥塞控制的发展史概括成一句话从“被动响应”到“主动建模”。第一代算法把丢包当作唯一的拥塞信号。这种设计在早期互联网很合理因为当时路由器缓存小网络确实是一堵就丢。但随着网络设备的缓存越做越大出现了“Bufferbloat”现象即使链路已经过载路由器也不会立刻丢包而是先疯狂排队导致延迟暴涨。这时候还坚持“见丢包才减速”就会把队列打成一片水漫金山的停车场。第二代CUBIC的改进思路是引入时间维度。它发现既然丢包不可靠那不如把窗口恢复过程设计成一个基于时间的函数不管RTT怎么变化都能比较规律地向上爬升。这让它在高带宽长链路网络上比Reno好得多。第三代BBR更像是换了一个提问方式不再问“什么时候会拥塞”而是直接测量链路的最大带宽和最小RTT算出最合理的在途数据量然后按这个量精准发送。这套逻辑在谷歌内部网络中表现极好传到Linux社区后也让很多实际场景看到了质变。2. 第一代策略Tahoe 到 Reno靠丢包当信号的时代2.1 Tahoe慢启动加拥塞避免丢包就从头再来Tahoe是1988年Van Jacobson在解决拥塞崩溃问题时提出的方案它引入的概念直到今天都还在用慢启动、拥塞避免、快速重传。慢启动阶段cwnd从1个MSS开始每收到一个ACKcwnd就加1。也就是说每个RTT内如果所有包都被确认了相当于cwnd翻倍。这看起来像是指数爆炸但因为起始值很小实际增长种过程非常克制。等到cwnd达到ssthresh就切换为拥塞避免每经过一个RTTcwnd只加1变成线性爬坡。Tahoe处理拥塞的方式简单粗暴一旦发生丢包超时或收到3个重复ACK立即把ssthresh设为当前窗口的一半并且把cwnd重置回1然后重新走慢启动。这种“刹车踩到底”的做法的优点是稳定、公平任何一条流都能在丢包后迅速让出带宽。但缺点也很明显一次丢包就让好不容易爬上去的窗口归零恢复需要好几个RTT这在高带宽网络里代价极高。我当年学TCP的时候一直记不住Tahoe究竟“快”在哪里。其实它的“快”指的是快速重传收到3个重复ACK就立刻重传丢掉的报文段而不用苦等RTO超时。这个机制配合上面说的重头再来就是Tahoe的全部家当。2.2 Reno引入快速恢复把“重来一次”改成“就地降速”Reno在Tahoe基础上只增加了一个功能快速恢复。但它带来的效果是相当大的。快速恢复的思路是这样既然能收到3个重复ACK说明网络还在传输数据后面有包到达了网络并不是完全瘫痪没必要把cwnd归1。于是Reno在收到3个重复ACK时把ssthresh设为当时cwnd的一半cwnd也降到ssthresh然后进入快速恢复状态。在这个状态里每收到一个重复ACK就临时把cwnd加1目的是为那个还没到达的包腾出空间继续发送。等重传的包被确认、收到新的ACK时再退出快速恢复转入拥塞避免阶段。用一句话总结Tahoe到Reno的变化Tahoe丢包后从头再来Reno丢包后原地减速继续开。这个改动让单丢包场景下的吞吐量提升很明显尤其在RTT较大的链路上省下的恢复时间非常可观。2.3 NewReno 和 Reno 的工程边界多丢包场景的真实表现经典的Reno有一个被很多人吐槽的问题如果一个窗口内丢了多个包Reno只能通过一次快速恢复处理第一个丢包剩下的丢包只能等超时又要重走慢启动。这就导致Reno在高误码率、多丢包的真实网络里表现不佳。Linux后续默认使用的基本是NewReno。NewReno的核心改进是在一个拥塞窗口内发生多次丢包时不退出快速恢复而是通过部分确认信息继续重传直到所有丢包都被恢复。这样能把原本一次丢包后的“超时重传”概率大幅降低。不过NewReno并没有从根本上解决高带宽长时延网络下的问题。这里有一个经典的数学关系可以用来估算Reno的吞吐上限吞吐率 ≈ 1.22 × MSS / (RTT × √丢包率)这个公式想说明的是对一个RTT为100毫秒、MSS为1460字节的链路如果丢包率是0.1%吞吐上限大概只有 1.22 × 1460 / (0.1 × 0.0316) ≈ 560 KB/s换算成带宽就是4.5Mbps左右。也就是说Reno想跑满100Mbps需要把丢包率压到极低才行。这在当时低带宽、小缓存的网络里够用但在现代高速链路上越来越力不从心。3. 第二代策略CUBIC用一条三次曲线锁住高带宽3.1 CUBIC的来源二分搜索窗口的激进实验要讲CUBIC得先提它爸爸BIC。BIC的思路很特别它把拥塞窗口的增长当成一个二分查找问题每次丢包会留下一个“上次导致拥塞的窗口上限W_max”和一个“拥塞避免开始的下限”BIC就在这两个值之间取中点试探能成功就继续向中点靠近再丢包就把上限往下压。这种二分增长方式在窗口很大时增长极快遇到拥塞后又很快收敛所以对高带宽网络非常友好。问题是它对RTT很不公平RTT小的连接能以更高频率做二分尝试抢占更多带宽导致RTT大的连接被饿死。另外二分查找在窗口接近W_max时锯齿状抖动明显影响稳定性。CUBIC的出现就是为了继承BIC的高带宽利用率同时削弱它对RTT的依赖和窗口抖动。CUBIC全称是Cubic TCP2006年进入Linux 2.6.19内核之后的十几年一直是Linux默认拥塞控制算法也是当前绝大多数Linux服务器、Android设备实际运行使用的算法。3.2 CUBIC的核心公式怎么看CUBIC的窗口增长函数长这样W(t) C × (t - K)³ W_max其中W_max是发生丢包时窗口的大小C是常数Linux默认0.4t是距上次丢包经历的时间K是一个中间变量用来确保丢包后cwnd先回落再逐渐爬回W_max。K的计算是K ∛( W_max × (1 - β) / C )这里的β是乘性减因子在CUBIC里默认是0.7。注意这个0.7和Reno的0.5不同Reno丢包后窗口砍一半CUBIC只砍到原来的70%本身就比Reno激进一点。从三次函数曲线能看出CUBIC的设计巧思丢包刚发生时窗口被降到W_max × (1-β)也就是60%~70%的位置然后曲线快速上升很快就能回到W_max附近在W_max附近会有一个近似水平的平台期让窗口稳住一段时间避免立刻又触发丢包最后如果链路确实还有余量曲线再次上扬继续探测更高的带宽。这个“快速恢复→平稳保持→再次探测”的节奏本质上是给窗口增长加了一个时间层面的整形。它不再像Reno那样完全依赖ACK反馈增长而是丢包后按时间表恢复因此CUBIC在高带宽、大延迟网络里能比Reno快很多地重新占满带宽。3.3 CUBIC在Linux默认位上的十多年CUBIC能被Linux选为默认算法核心原因是它足够稳。它不像BIC那样有严重的RTT不公平问题也不像Reno那样在高带宽场景里疲软。它是当时少有的在扩展性、公平性、实现复杂度之间取得平衡的方案。谷歌后来做过大量测量即使在4G移动网络里CUBIC也比Reno好不少。但CUBIC并非完美。它虽然不直接依赖RTT做窗口增长却仍然把“丢包”作为拥塞的判断依据。在浅缓冲链路中CUBIC会把管道填得满满的稍微冒进就丢包长链路下丢包恢复又慢在深缓冲链路中它又倾向于持续填充缓冲区造成明显的Bufferbloat。换句话说CUBIC是对“丢包在网络里普遍存在且能反映拥塞”这一假设的改良而不是推翻。现在很多桌面Linux发行版和Android设备默认就是CUBIC它依然是当前互联网传输的绝对主力。但BBR出现以后越来越多人开始在服务器上做切换测试后面会细说。4. 第三代策略BBR从反应式转向模型式4.1 BBR的出发点丢包不能完全代表拥塞谷歌在2016年左右开源了BBRBottleneck Bandwidth and Round-trip propagation time核心思路非常反传统不再把丢包当作拥塞的主要信号而是通过测量链路的两个物理极限来指导发送。这两个极限就是名字里的两个词瓶颈带宽Bottleneck Bandwidth和往返传播时间Round-trip propagation time。它们相乘得出的BDP带宽延迟积正好是“链路中能塞下的最优数据量”。如果实际在途数据低于BDP带宽空闲浪费高于BDP多出来的数据全部排在路由器缓冲区里只会增加延迟不会增加吞吐。BBR做的事情就是用最少的排队把发送速率精确控制在一个BDP左右。你可以把它理解成“按需供水”的水厂先搞清楚管道有多粗带宽、水厂到你家有多远RTT然后算好管道里应该保持的水量之后一直保持这个量而不是等水池溢出了再关阀门。4.2 BBR是怎么测量带宽和RTT的BBR在运行过程中会维护两个关键参数max_bw最大带宽和min_rtt最小RTT。max_bw取的是最近10个RTT窗口里的最大实际传输速率靠不停探测得出的。min_rtt则是过去10秒内测到的最小RTT因为只有链路缓冲区为空、没有排队时测出的RTT才是真实的传播时延。有了这两个参数BBR就把pacing rate发送速率设成max_bw的某个倍数。发送端按照这个速率匀速发包而不是像传统拥塞控制那样受“丢包就调整cwnd”的事件驱动。BBR的出发点是只要发送速率接近瓶颈带宽就不会产生排队也不会丢包网络就能一直工作在最佳状态。这套机制最直观的效果是在高带宽高延迟链路上BBR能比CUBIC更快、更稳地跑满带宽同时延迟还更低。我自己在跨省传输大文件的场景里测过同样一根链路调成BBR后吞吐提升经常在20%以上而RTT抖动反而变小了。4.3 BBR的四个状态与8相位探测BBR内部是一个状态机核心状态有四个Startup、Drain、ProbeBw、ProbeRtt。刚启动时进入Startup用增益系数2.885加速探测带宽这个阶段类似传统慢启动但目标不是倍增cwnd而是快速找到max_bw的近似值。一旦判定带宽增长不再线性提升就进入Drain把因为过度发送积压的队列排空。之后的大部分时间都待在ProbeBw状态它内部有8个相位循环每个相位的pacing增益依次是1.25、0.75、1、1、1、1、1、1。1.25相位用来短暂试探更高带宽0.75相位用来排空可能产生的队列其余相位保持基准速率。这个节奏保证了BBR一边探测新带宽一边不把队列养肥。ProbeRtt是状态机里专门用来刷新min_rtt的状态。如果过去一段时间没测到更小的RTTBBR会把发送窗口临时收缩到4个MSS持续大约200毫秒逼出排队延迟重新测得一个干净的min_rtt。这四个状态分工明确各干各的事。这也是BBR被称为“模型驱动”的原因它把拥塞控制从“对丢包做反应”变成了“按测量模型持续运行”。4.4 BBR的实际表现争议与v2修正BBR在谷歌内部和全球公开测试里表现惊艳但它并非没有争议。首先BBR对丢包几乎不敏感如果与CUBIC/Reno流共享同一瓶颈链路BBR会持续占住带宽导致传统算法流被明显压制。这在实际公网里会造成公平性问题。其次BBR对RTT公平性也不理想。因为它是按“测量到的max_bw”来决定发送量的不同RTT的连接若同时竞争RTT较小的流更容易快速测到高带宽从而抢占更多资源。另外在浅缓冲链路中BBR的1.25增益相位还是会周期性制造微小丢包虽然影响不大但不是完全无丢包。针对这些问题谷歌后续一直在推进BBR v2主要改进方向包括对丢包和ECN显式拥塞通知做出响应、减少ProbeBw阶段对RTT公平性的破坏、以及在轻度拥塞时主动降速。BBR仍然是这个领域最值得关注的方向但生产环境里大规模替换CUBIC前一定要先做公平性测试尤其是共享链路场景。5. 四代算法横向对比与选型建议5.1 一张表看明白四种算法的差异很多朋友容易把这几个算法搞混下面这张表是我按“核心思想、拥塞信号、丢包处理、公平性、适用场景”梳理的建议直接收藏算法核心思想拥塞信号丢包后窗口表现主要劣势典型场景Tahoe慢启动 拥塞避免丢包回到1重新慢启动恢复慢浪费带宽早期低速网络Reno/NewReno加性增乘性减丢包减半后快速恢复高带宽低吞吐、多丢包恢复差传统可靠网络环境CUBIC基于时间的三次函数增长丢包衰减到70%后按时间恢复浅缓冲下延迟高、BufferbloatLinux默认通用互联网BBR测量带宽和RTT的模型驱动带宽/RTT模型几乎不因丢包调整共享链路下挤压传统算法流高带宽长RTT、跨国链路这张表里可以从上往下看到一个清晰的演进脉络从依赖丢包到依赖延迟模型从反应式到主动型。Tahoe和Reno已经很难在现代网络里跑出好效果CUBIC依然是默认安全牌BBR则是追求极致吞吐时的拔高选项。5.2 如何查看与切换Linux的拥塞控制算法实际操作层面Linux上的操作并不复杂。先查看当前系统的拥塞控制算法sysctl net.ipv4.tcp_congestion_control查看内核支持的算法sysctl net.ipv4.tcp_available_congestion_control临时切换成BBRsudo sysctl -w net.ipv4.tcp_congestion_controlbbr但这里有个几乎每个人都会踩的坑如果没有先加载tcp_bbr内核模块执行上面的命令会直接失败。所以切BBR前要先执行sudo modprobe tcp_bbr要永久生效把以下两行写入/etc/sysctl.confnet.core.default_qdisc fq net.ipv4.tcp_congestion_control bbrfq是BBR配合使用的公平队列调度器推荐一起启用。保存后执行sudo sysctl -p让配置生效再检查一下算法名是否已经改变。如果是Reno或CUBIC模块Reno几乎内建、无需额外加载CUBIC在绝大多数现代内核里也已经内建。5.3 不同业务场景的选型建议算法的选型不能只凭跑分要看业务的实际流量特征。对普通Web服务和绝大多数云服务器来说保持默认的CUBIC是稳妥选择。它和当前互联网上的其他TCP实现兼容性好不容易引发公平性问题。但如果你主要做跨区域、跨国的大文件传输比如CDN回源、数据备份、视频转码素材拉取BBR在长RTT链路上的优势会非常明显。对物联网和工业场景像MODBUS TCP这类协议通常每个TCP包都很小数据量不大但实时性要求高。此时拥塞控制算法的吞吐优势体现不出来更关键的是连接建立和低时延确认。保持默认算法就行不要为了“提速”盲目切BBR因为工业网关的老旧系统内核可能根本不支持。对socket编程人员还有一点可以参考Linux允许用setsockopt的TCP_CONGESTION选项为单条TCP连接单独指定拥塞控制算法。例如同一个服务里下载大文件的长连接用BBR指令类的短连接保持CUBIC这种精细化控制在实际项目里是完全可行的。6. 切换算法时踩过的坑与排查经验6.1 设置bbr没生效多半是模块没加载我见过很多人执行完sysctl -w net.ipv4.tcp_congestion_controlbbr返回成功但sysctl net.ipv4.tcp_congestion_control一看还是cubic。问题几乎都出在缺少tcp_bbr模块。先确认内核版本BBR在Linux 4.9之后才可用4.13以后才比较稳定。如果你的内核太老不论怎么配置都不会生效。然后是模块加载我的习惯是先执行modprobe tcp_bbr再写sysctl最后重启网络服务或者直接重启验证。如果用的是发行版自带内核而不是自己编译的可能还需要安装kernel-modules-extra之类的包。判断模块是否加载成功可以查看lsmod | grep bbr有输出就说明没问题。6.2 带宽高却速度慢BDP与接收窗口调完了算法下载速度还是上不去这时候问题多半不在拥塞控制而在应用层或接收窗口。回忆一下前面说的BDP概念。一条100Mbps链路、RTT是50毫秒那么BDP大概是 100Mbps × 0.05s ≈ 0.625MB也就是说接收端窗口至少要大于625KB单条连接才能跑满。很多老程序默认的socket接收缓冲区只有64KB这会让发送端无论如何都只能发64KB在途数据带宽再高也白搭。解决方法是调大接收缓冲区Linux下可以在socket编程里用setsockopt设置SO_RCVBUF或者对全局限流场景调整net.core.rmem_max和net.ipv4.tcp_rmem参数。这类问题容易被误判成拥塞控制没生效实际是应用层的窗口瓶颈。6.3 下载快但游戏卡Bufferbloat还有一种很气人的情况下载速度跑满但玩游戏时延迟飚到几百毫秒。这基本可以确认是Bufferbloat即路由器缓冲区被大流量塞满小包也排队排到天荒地老。CUBIC的窗口增长机制容易导致这个现象因为它会持续填充缓冲区直到发生丢包。按需给路由器或者光猫开启SQM智能队列管理能显著改善另外让交互型流量走的连接使用fq或者cake队列规则也能把交互包优先排出去。如果是在自己的Linux服务器上切换把net.core.default_qdisc设为fq对缓解Bufferbloat也有帮助。注意切换BBR同样能改善Bufferbloat问题因为它本身就尽量不排队这也是不少玩家和运维钟情它的原因。6.4 长连接、短连接场景下算法表现差异关于TCP长连接与短连接需要单独提醒一下。拥塞控制是按连接维护独立状态的短连接往往在慢启动阶段就结束了算法优势完全发挥不出来。所以不要指望通过换算法来优化大量新生短连接的场景比如频繁发起的小请求接口。长连接场景则相反连接存活时间越久拥塞控制算法的影响就越明显。比如数据库连接池里的长连接做大量数据传输时BBR的收益就很可观。不过长连接空闲久了有些内核版本会重置拥塞窗口重新进入慢启动这在服务端代码里也能通过TCP套接字选项做一定调整。我在实际调优时最常用的套路是先用ss -i查看一条连接当前的cwnd、rtt和发送速率判断卡在哪一层再去动算法参数。千万不要一上来就换算法先看数据再操作否则连问题在哪都不知道。最后说点个人的体会。做网络调优这么多年我越来越觉得拥塞控制不是越激进越好也不是越新越好。TCP协议的伟大之处恰恰在于它能在互不信任、彼此不知道对方状态的网络里用几行算法维持住秩序。Tahoe到Reno是补课Reno到CUBIC是改良CUBIC到BBR则是换了一个思考框架。理解这条演进线比记住任何一张参数表都重要。下次遇到网络卡顿你可以先看一眼ss -i里的rtt和cwnd再结合这篇文章里的思路去判断该换算法、调窗口还是直接找路由器开SQM。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。