TCP拥塞控制进化史:从Tahoe、Reno到CUBIC与BBR
2026/9/16 9:15:53 网站建设 项目流程

1. 拥塞控制到底在管什么

1.1 拥塞控制解决了网络里最常被误解的那个问题

先说一个现象,很多人应该都有同感:宽带明明升到了千兆,平时下载大文件速度也还行,但只要一到晚高峰,视频会议开始卡顿、网页加载转圈,而路由器后台看着流量并不高。问题不在带宽,而在“拥塞”。

拥塞控制就是TCP协议里负责处理这种局面的一套机制。它解决的核心矛盾是:发送端不知道网络什么时候会堵,但如果不控制发送速度,数据就会在路由器或交换机的缓冲区里堆积成山,轻则延迟飙高,重则直接丢包。说到底,拥塞控制是一套“发送端自我约束”的规则,让网络里的每条TCP流既尽量占满可用带宽,又不要冲垮中间设备的缓存。

我经常用一个比喻:把网络链路看作一条高架桥,拥塞控制就是给每个司机装上节奏器。桥上不堵时加速跑,一旦前方车流变慢(丢包或者RTT增大),就主动减速避让。没有这套机制,大家都不踩刹车,桥瞬间就堵死了。

这篇文章要聊的,就是从早期Tahoe到Reno,再到当前Linux默认的CUBIC,以及谷歌开源的BBR,这四代拥塞控制策略各自的思路、优劣和演进逻辑。适合刚接触TCP协议的开发者、做网络调优的运维,以及所有想搞明白“为什么网速忽快忽慢”的技术爱好者。

1.2 三个绕不开的基本参数与一条核心曲线

在展开具体算法之前,必须先交代几个TCP拥塞控制领域的基础参数。不管哪种算法,最终操作的都是这三个量:

  • cwnd(拥塞窗口):发送端允许同时在途的数据量,单位通常是MSS(最大报文段大小),cwnd越大,同一时间网络上就能有越多数据。
  • ssthresh(慢启动阈值):慢启动阶段和拥塞避免阶段的分界线,cwnd低于它时指数增长,超过以后切换成更保守的增长方式。
  • RTT(往返时延):一个数据包从发出到收到确认ACK所花的时间,它决定了窗口大小和实际吞吐之间的关系。

还有一条贯穿所有算法的核心曲线,就是AIMD,加性增乘性减。没发生拥塞时,每经过一个RTT,cwnd增加一个固定值(线性增长);一旦探到拥塞信号(早期是丢包),就把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开始,每收到一个ACK,cwnd就加1。也就是说,每个RTT内如果所有包都被确认了,相当于cwnd翻倍。这看起来像是指数爆炸,但因为起始值很小,实际增长种过程非常克制。等到cwnd达到ssthresh,就切换为拥塞避免:每经过一个RTT,cwnd只加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 TCP,2006年进入Linux 2.6.19内核,之后的十几年一直是Linux默认拥塞控制算法,也是当前绝大多数Linux服务器、Android设备实际运行使用的算法。

3.2 CUBIC的核心公式怎么看

CUBIC的窗口增长函数长这样:

W(t) = C × (t - K)³ + W_max

其中W_max是发生丢包时窗口的大小,C是常数(Linux默认0.4),t是距上次丢包经历的时间,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年左右开源了BBR(Bottleneck 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的状态。如果过去一段时间没测到更小的RTT,BBR会把发送窗口临时收缩到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

临时切换成BBR:

sudo sysctl -w net.ipv4.tcp_congestion_control=bbr

但这里有个几乎每个人都会踩的坑:如果没有先加载tcp_bbr内核模块,执行上面的命令会直接失败。所以切BBR前要先执行:

sudo modprobe tcp_bbr

要永久生效,把以下两行写入/etc/sysctl.conf

net.core.default_qdisc = fq net.ipv4.tcp_congestion_control = bbr

fq是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_control=bbr返回成功,但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_maxnet.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。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询