TCP拥塞控制详解

明明网络很稳定,速度却忽快忽慢?其实TCP一直在暗中和网络讨价还价。

为什么需要拥塞控制

互联网是一种共享资源,许多连接会经过相同的路由器和链路。如果每个发送方都以最大速度不加节制地发送数据,路由器缓冲区就会溢出,导致丢包激增、重传增多,最终可能引发拥塞崩溃,让整个网络的吞吐量急剧下降。拥塞控制正是TCP在这种情况发生之前主动调节发送速度的机制。

拥塞窗口(cwnd)是什么

拥塞窗口表示发送方在收到确认(ACK)之前可以发送的数据量。窗口越大,一次能发送的数据越多,吞吐量也越高,但如果过大,网络就会处理不过来,导致丢包。几乎所有拥塞控制算法,归根结底都是一套如何增大或缩小这个数值的规则。

慢启动:逐步试探网络容量

连接刚建立时,TCP对网络状况一无所知,因此会从很小的窗口开始,每次成功收到确认就将窗口翻倍。这种指数级增长能快速摸索出网络大致能承受的速度,直到发生丢包或达到预设阈值,慢启动结束,进入更谨慎的拥塞避免阶段。

拥塞避免:出现丢包就降速

传统TCP把丢包视为网络拥塞的信号。一旦检测到丢包,就会大幅缩小窗口(通常减半),此后只要不再丢包,就缓慢线性地增大窗口。这种反复缩小又增大的过程,在吞吐量曲线上会呈现出典型的锯齿状形态。

Reno与New Reno:经典的基于丢包的算法

诞生于上世纪90年代的TCP Reno,结合了慢启动、拥塞避免以及快速重传、快速恢复机制,能在丢包后迅速做出反应并恢复。New Reno改进了同一窗口内多个数据包同时丢失时的恢复效率。但在带宽极高的链路上,两者丢包后恢复速度都偏慢,难以充分利用带宽。

Cubic:Linux默认算法,专为高带宽环境优化

Cubic是当前Linux内核的默认拥塞控制算法,它让窗口大小随时间按三次函数(立方)曲线变化。丢包后会先快速恢复到接近之前的水平,随后逐渐放缓增速,并在接近上次丢包点时重新变得谨慎。这种方式让Cubic在高带宽、长延迟的链路上比Reno系算法能更高效地利用带宽。

BBR:直接测量带宽和延迟的新思路

像Reno、Cubic这类基于丢包的算法,只有在发生丢包之后才会做出反应,而这时路由器缓冲区往往已经被填满(这与缓冲区膨胀问题密切相关)。谷歌开发的BBR则不同,它持续实时估算瓶颈链路的真实带宽和近期最短的往返时延,力图在丢包发生之前就维持最优的发送速率。BBR已应用于谷歌自身的多项服务,在Linux内核中也可以选用。

来历:上世纪80年代的拥塞崩溃与Van Jacobson的改进

1980年代中期的早期互联网出现过“拥塞崩溃”:发送方不断重传丢失的数据,反而让拥塞更加严重,部分链路的吞吐量骤降。1988年,Van Jacobson发表论文《Congestion Avoidance and Control》,提出慢启动和拥塞避免的思想,成为今天TCP行为的基础。RFC 5681等规范以标准形式描述了这些机制。

慢启动阈值(ssthresh)

ssthresh是TCP停止翻倍增长、转为线性增长的窗口大小分界点。发生丢包后,它通常被设为当时窗口的一半左右,让连接先快速恢复到一个较为安全的水平,然后再谨慎推进。可以把它理解为TCP对“这条路径上大致从哪里开始出问题”的记忆。

快速重传与快速恢复

接收方收到乱序的报文段时,会重复发送对最后一个按序到达字节的确认。连续收到三个重复确认,就被视为某个报文段丢失而后面的报文段已经到达。发送方不再等待定时器超时,而是立即重传,并把窗口减半而不是降到最小,因此吞吐量的下降远小于发生超时的情况。

带宽时延积:管道里能装多少数据

要让一条链路充分利用,需要在途数据量等于链路带宽乘以往返时延。例如,带宽100 Mbit/s、往返时延50毫秒的路径,在途数据约为625,000字节(每秒1亿比特除以8,再乘以0.05秒)。如果窗口小于这个值,发送方就会空等确认,这也是远距离高速链路需要很大窗口、旧算法在这类场景下表现不佳的原因。

初始窗口:一开始发多少个包

刚建立的连接对网络一无所知,所以只发少量报文段。2013年发布的RFC 6928允许初始窗口最多为10个报文段,使小网页和API响应可以在一两个往返内完成。不过,如果许多发送方同时发出十个包,小缓冲区仍可能被挤爆,这是一种取舍。

缓冲区膨胀与主动队列管理

路由器和调制解调器如果缓冲区很大,就会把包存起来而不是丢弃,基于丢包的算法要等到队列已经很长才会察觉,共享链路的所有人延迟都会显著增加。CoDel、fq_codel等主动队列管理方法会提前丢弃或标记数据包,使链路繁忙时延迟也能保持较低。如果有人上传大文件时你的视频通话开始卡顿,缓冲区膨胀很可能是原因之一。

显式拥塞通知(ECN)

支持ECN的路由器不必丢包来提示拥塞,而是给数据包打上标记,接收方再把标记反馈给发送方。发送方会像遇到丢包一样降速,但不需要重传数据。ECN需要两端以及中间路由器都支持,因此实际效果因网络而异。

为什么Wi-Fi和移动网络会让基于丢包的算法困惑

在无线链路上,数据包可能因干扰或信号弱而丢失,而不是因为拥塞。基于丢包的算法会把这种丢包当作拥塞而缩小窗口,导致链路能力没有被充分利用。无线链路通常有自己的重传和纠错层,可以掩盖大部分丢包,但无法完全消除。像BBR这样不把每次丢包都当作拥塞信号的算法,在这种环境下表现会有所不同。

公平性:礼貌地共享链路

共用同一个瓶颈的连接,理想情况下应当大致平分带宽。往返时延相近的Reno类连接会逐渐趋向公平,但往返时延更短的连接增长更快,会占到更大份额。不同算法混合运行时也可能出现不公平:有研究表明,在Cubic和BBR混合的流量中,根据缓冲区大小,其中一方可能占用超出应得份额的带宽,这也是大型运营方会仔细测试的原因。

拥塞控制只是TCP可靠性机制的一部分

除了拥塞控制,TCP还依靠序列号、确认应答和重传计时器来保证数据完整、有序地到达。拥塞控制专门解决的是整个网络的公平性和稳定性问题——如果没有它,少数激进的发送方就可能拖垮共享链路上所有人的体验。

你通常能查看正在使用的算法,却很少能真正切换

大多数消费级操作系统和浏览器都不会开放切换拥塞控制算法的选项,因为这一设置在服务器端的影响远大于客户端。Linux服务器通常可以通过内核参数查看并切换Cubic、BBR等可用算法,这也是为什么繁忙网站、视频平台和CDN的运维人员格外关心这个选择。

在Linux上查看当前设置

在Linux上,命令sysctl net.ipv4.tcp_congestion_control可以显示默认算法,net.ipv4.tcp_available_congestion_control则列出已加载的算法。ss命令加上-i选项,可以打印每条连接的拥塞窗口、往返时延和重传次数等信息,在排查传输变慢时很有用。修改默认算法通常需要管理员权限,并且作用于整个系统,所以应当先在非关键机器上测试,并用实际提供的流量进行测量。

运维人员该如何选择算法

Cubic多年来一直是Linux内核的默认算法,在很多条件下表现稳定,是合理的基准。BBR在洲际链路这类长距离、丢包多或时延大的路径上可能有优势,但结果取决于缓冲区大小以及共享瓶颈的其他流量,并且自2016年首次发布以来已经多次修订。可靠的办法不是看通用的基准测试,而是用真实流量做A/B测试,比较吞吐量、时延和重传率。

QUIC:拥塞控制转移到应用层

QUIC在2021年被标准化为RFC 9000,运行在UDP之上,并在应用的库中实现自己的丢包恢复和拥塞控制,而不是依赖操作系统。这使浏览器和服务器无需等待内核升级就能更新算法。规范中给出了类似New Reno的默认控制方式,具体实现通常也提供Cubic或BBR。对用户来说,本页介绍的原理同样适用于HTTP/3流量,只是代码所在的位置不同。

常见问题

我可以更换设备使用的拥塞控制算法吗?

在Linux系统上可以,内核提供了在Cubic、BBR等算法之间切换的设置。而Windows、macOS以及大多数移动操作系统都不会向普通用户开放这一设置,实际上连接中服务器一端对体感速度的影响通常比客户端更大。

通过UDP传输的视频通话或游戏流量也会受拥塞控制影响吗?

UDP本身并没有内置的拥塞控制机制,因此行为不规范的UDP应用可能在网络拥塞时依然持续发送数据,给共享同一链路的其他用户造成负担。构建在UDP之上的现代协议,例如QUIC,则实现了自己的拥塞控制逻辑,以更负责任的方式运作。

丢包一定意味着拥塞吗?

不一定。丢包可能来自拥塞,也可能来自无线干扰、线缆故障或设备过载。传统TCP假定丢包就是拥塞,所以在质量较差的无线链路上可能无谓地降速。

为什么下载刚开始很慢,后来才变快?

这是慢启动在起作用。连接从很小的窗口开始并快速增长,小文件可能还没跑满速度就已经传完了。这也是为什么许多短连接往往比少数长连接更慢,以及HTTP/2等协议强调连接复用的原因之一。

换更高速度的宽带套餐,单个下载就会变快吗?

不一定。单条连接可能受往返时延、窗口大小或路径上的丢包限制,而不是受套餐带宽限制。从遥远的服务器下载时,即使套餐很大也未必能跑满,而更近的服务器或CDN则可能做到。

吞吐量曲线中的锯齿形说明什么?

它是线性增长后在丢包时急剧下降,如此反复的结果。如果曲线更平滑,可能是使用了基于时延或实测带宽进行调整的其他算法,或者路径上有充足的余量。