TCP 流量控制与拥塞避免
与 UDP 不同,TCP 协议为了提高利用率,设计了流量控制与拥塞控制机制。两者均使用类似的窗口算法,但它们的目的上存在本质上的不同。流量控制解决发送方与接收方之间的速度不匹配问题,是一个端到端的问题。而拥塞控制解决发送方和网络之间能力不匹配的问题,是一个全局性问题。
流量控制
流量控制用于防止发送方发送的数据过多、过快,而接收方的接收、处理能力有限,导致接收方缓冲区溢出的问题。
流量控制,是通过滑动窗口协议来实现的。这个窗口决定发送方能发送多少未收到 ACK 确认的数据。

如上图 TCP 包结构,112~127 位的 2 字节定义了 Window 字段。接收方发送 ACK 报文时,该字段被设置为剩余缓冲区长度。这就被称为接收窗口 RWND,发送方会根据收到的接收窗口协调数据发送。
在发送方连接中,还维持一个发送窗口,这个窗口控制发送方可发送的数据量。这个窗口受到接收方的接收窗口限制,不会超出收到的最新接收窗口通告范围。
极端情况下,当接收方处理速度极慢,可能通告一个零大小的接收窗口,此时发送方不应该再发送任何应用数据。当接收缓冲区有了新的空间,需要重新发送 ACK 报文向发送方更新接收窗口。通过这一机制,接收窗口归零后仍具有恢复能力。
如果这个更新接收窗口的 ACK 数据包出现丢包,则发送方保持发送停止。出现发送方等待接收方更新接收窗口、接收方等待发送方发送数据,然而实际上发送方有数据待发送、接收方有缓冲区空间待接收的死锁状态。为了解决这个死锁问题,发送方维持一个持续计时器,在进入零窗口状态时启动定时器,并在定时器超时后发送一个超出接收窗口的小数据包(比如1字节)进行探测。接收方收到这个探测数据后,无论是否有缓冲区空间,都需要发送 ACK 报文进行确认,并携带最新的接收窗口大小。通过这个定时器机制,保证了在零窗口下接收窗口仍能定期更新,打破死锁。
另外,为了避免极小数据包在网络中大量传输,在数据发送和接收窗口更新时存在一套算法,将这类小报文合并后进行发送或更新。
拥塞控制
拥塞控制用于防止发送的数据过多、过快,而网络的处理能力不足导致网络拥塞并出现丢包、延迟等情况。丢包、延迟的频繁出现会使得网络中存在更多的重传等报文,进一步降低网络效率并可能导致网络全局崩溃。为了减少这种情况,需要发送方根据网络情况对自己的发送速率进行控制。
由于网络本身没有提供关于能力的信息,为了充分利用网络能力的同时减少网络拥塞,拥塞控制算法通过反馈来推测网络情况。为了发现网络能力的极限,拥塞控制算法通过逐步加大发送速率来达成对网络能力的充分利用。为了发现网络拥塞,拥塞控制算法通过观察数据包的延迟、丢包情况来判定网络是否拥塞。为了减少拥堵,拥塞控制算法在发现网络拥堵后降低发送速率。为了充分利用网络能力,拥塞控制算法在网络拥塞解决后重新加速。拥塞控制算法整体就通过这样的“提高发送速率-网络拥塞-降低发送速率-拥塞解决-提高发送速率”的循环反馈中达成动态平衡。
为了达成这样的循环反馈,引入拥塞窗口与流量控制接收窗口协同形成发送窗口,对发送方的发送速率进行控制。拥塞窗口 CWND 代表不引发网络拥堵的前提下发送方一批发送的最大字节数。最终发送时取流量控制中的接收窗口和拥塞控制窗口中的最小值作为发送窗口,限制发送速率。
Reno 拥塞控制算法
在经典的 Reno 拥塞控制算法中,可将整个拥塞控制分成四种阶段:慢开始、拥塞避免、快速重传、快速恢复,四个阶段在整个生命周期中协同工作。
慢开始
在新连接建立或发生超时重传时,Reno 进入慢开始阶段。该阶段中,每收到对一个包的 ACK 确认报文,CWND 增长一个 MSS。也就是每经过一轮 RTT,CWND 翻一倍,这是一个指数增长的过程。
在慢启动阶段,当 CWND 增长到 SSTHRESH(慢启动阈值)时,慢启动阶段转为拥塞避免阶段。
在新连接中,CWND 通常设置为 1 MSS,而 SSTHRESH 设置为较大值。
拥塞避免
在接近网络容量时,进入拥塞避免阶段。这一阶段谨慎地、线性地增加 CWND,以避免发生网络拥塞。
对网络容量的预估就体现在 SSTHRESH 上,当 CWND >= SSTHRESH,就认为已接近网络容量。
在这一阶段,每收到一个包的 ACK 报文,CWND 增加 1/CWND 个 MSS。也就是每经过一个 RTT,CWND 增加一个 MSS。
当检测到丢包时,拥塞避免阶段退出。丢包检测有两种触发方式,一是重传计数器超时,二是收到重复 ACK 确认报文。这两种方式实际上体现了两种不同的拥塞情况。
发生重传计数器超时,意味着此前发送的一批报文都没有到达接收方,或已到达接收方,但返回的 ACK 报文没有被发送方收到。这种表现可以推测为网络发生了较为严重的拥塞,大量报文被丢弃或延迟。
这种情况下,Reno 拥塞控制算法将 SSTHRESH 设置为当前 CWND 的一半,并将 CWND 重置为 1 MSS,重新进入慢启动阶段。以此给网络设备留出更多的时间消化堆积的报文。
快速重传
收到重复 ACK 确认报文,则意味着此前发送的一批报文发生了乱序到达或部分报文丢失。这种情况可以认为网络只是发生了波动或轻微拥塞,因为能收到重复 ACK 确认报文就表明至少有 2 个报文成功到达接收方,且产生的对应 ACK 报文也能成功送达发送方。
Reno 快速重传阶段就是处理这种情况的。在收到 3 个重复的 ACK 报文时,Reno 进入快速重传阶段,将重复的 ACK 报文后的预期第一个丢包的报文进行重传。通过这种机制,无需等待超时重传计时器即可将缺失数据包进行重传,降低重传延迟。
快速恢复
发生快速重传后,进入快速恢复阶段。此时将 SSTHRESH 减半,调整对网络容量的预期。
收到重复的 3 个 ACK 包,表明有 3 个数据包已到达接收方,因此将 CWND 增加 3(可在已发送的数据包另外发送 3 个 MSS) 。此后每收到一个重复 ACK,就将 CWND 增加 1 (可额外发送 1 个 MSS)。通过对 CWND 的线性增长,维持了连接管道内的数据流动,保持对 ACK 的触发,保证快速恢复阶段的可持续性。
当收到对新数据的 ACK 时,表明重传的报文已成功到达接收方,网络可能已恢复。此时将 CWND 设置为 SSTHRESH,重新进入拥塞避免阶段。
在快速恢复阶段内部,对网络中新注入的数据包都由网络中反馈的成功消息触发,没有给网络额外施加压力。同时,也没有粗暴地将 CWND 重置为 1 进入慢启动阶段,而是将窗口减半并维持来降低网络负载的同时保持一定的吞吐量。
Reno 拥塞控制算法示意图

Cubic 拥塞控制算法
Reno 拥塞控制算法奠定了现代拥塞控制的基础模型,其 AIMD(加性增、乘性减)的模型被认为是公平和收敛的。但在现代高速网络下,这种缓慢的加性增、激进的乘性减使得吞吐量出现巨大波动,且恢复所需的 RTT 数极大,几乎不可用。
当前 Linux 内核默认采用的 Cubic 拥塞控制算法针对 Reno 存在的问题进行了一些改进。
在“乘性减”方面,Cubic 定义一个乘性减系数 β ,这个系数默认取值为 0.7 或 0.8 ,比相当于取值为 0.5 的 Reno 更保守,减少了吞吐量的波动。在对 CWND 和 SSTHRESH 进行调整时,都会采用这个系统。因此,发现拥塞事件时,SSTHRESH 的缩小程度都会小于 Reno 。快速恢复阶段 CWND 的起点也就高于 Reno 。
Cubic 作用的核心则在于替换了拥塞避免部分的窗口增长算法。从针对丢包进行反应思路转换到通过数学模型指导拥塞窗口的增长。
Cubic 的拥塞窗口通过
$$ CWND = C * (t -K) ^ 3 + W_{max} $$
来计算。其中,C 是控制窗口增长激进程度的常量。t 是距离上次发生拥塞事件的时间,这使得拥塞窗口的增长不再依赖于 ACK 到达,而是基于系统时钟。W_max 是上次发生拥堵事件时的拥塞窗口大小。K 是在没有新的丢包产生的情况下,恢复到发生拥堵事件时的窗口所需时间。计算方式为
$$ K = \sqrt[3]{\frac{W_{max} * \beta}{C}} $$
使用这种三次函数的形式,发生拥塞事件后的拥塞控制阶段中,拥塞窗口越靠近上次发生拥塞事件时的窗口大小,其变化越慢。这实际上体现了“上次发生拥塞的拥塞窗口可能体现了网络能力的极限”的思路。在发生拥塞后的恢复阶段,开始时离网络能力极限还比较远,可以大胆地增长拥塞窗口。恢复到上次水平附近时,可能已接近网络能力极限,需要谨慎地增长拥塞窗口。较大程度地超越了上次水平时,表明网络能力可能发生了较大的变化,可以大胆地增长以试探新的网络能力极限。
BBR 拥塞控制算法
在 Reno、Cubic 拥塞控制算法中,对网络拥塞情况的判断都以丢包为基础。在吞吐量提高时,发包不断填充路由器缓冲区,导致转发延迟提高;在吞吐量低时,路由器转发压力低,转发延迟也低。带来吞吐量的矛盾,要么高吞吐高延迟,要么低延迟低吞吐。
而 BBR 将网络的表现建模为最大带宽和最低延迟两个本质特征。在这样的网络建模下,可以推测最优操作点为带宽时延积(Bandwidth-Delay Product,Bandwidth-Delay Product),因为这表示了网络中正在传输的数据量。当窗口 > BDP 时,数据在缓冲区堆积,提高延迟;当窗口 < BDP 时,不能充分利用带宽,降低吞吐量。BBR 算法的目标就是让在途数据量保持在 BDP 附近,从而在不额外增加延迟的情况下提高吞吐量。
BBR 算法的实现中,维护两个核心变量:瓶颈带宽(BtlBw = max(delivery_rate)、往返传播时延(RTprop = min(rtt))作为两个本质特征的体现。并在四个阶段的循环中探测和维护两个变量,保持其时效性、有效性。
STARTUP(启动阶段)使用类似慢启动的算法,指数增长发送速率,快速探测 BtlBw ,直到带宽增长 < 25% ,判定为发现带宽瓶颈 BtlBw 。
DRAIN(排空阶段)降低发送速率到 BDP 水平,持续约 1 RTT 来排空 STARTUP 阶段产生的队列。
PROBE_BW(带宽探测阶段)维持 8 个周期,1 周期发送速率 1.25 BtlBw 向上探测带宽,2 周期发送速率 0.75 BtlBw 排空队列,3-8 周期维持 BDP 水平发送速率。在大部分时间中,连接处于该阶段。
PROBE_RTT(时延探测阶段)在 10 秒内未测量到更低 RTT 时触发,将在途数据包降至 4 个报文,持续 200 毫秒,以精确测量 RTprop 。
BBR 早期算法没有对丢包的直接反应,因此在有随机丢包的网络中有更好的稳定性。且能充分利用网络带宽,同时维持低延迟。在高带宽高延迟、有随机丢包的广域网环境下,BBR 算法适应性较好。这些特性反而导致在与 CUBIC 传统拥塞控制算法共存时过于强势,带来公平性问题。在 BBR 的后续迭代版本 v2 中引入了 ECN 机制等一系列措施,提高了公平性。
ECN (Explicit Congestion Notification)
经典的拥塞控制体系中,对拥塞的感知都通过丢包来间接反映,而这就带来了更多的滞后性,且丢包本身就是对网络性能的损失。使用 ECN 机制,网络中的路由设备可以主动、显示通告网络拥塞情况,提前缓解拥塞情况。
在路由器检测到早期拥塞(缓冲区未满,但队列长度达到阈值)时,在 IP 包头部设置 ECN 标志。接收方发现该标志后,在返回的 ACK 包 TCP 头部保留字段中设置 ECE(ECN-Echo) 标志,通告发送端网络发生了拥堵。发送端收到该标志就判定为发生网络拥堵,对拥塞控制窗口(CWND)、慢开始阈值(SSTHRESH)进行调整。同时在发送的 TCP 头部设置 ECR(ECN-Reduced)标志,通知接收方停止发送 ECE 标志。
从 ECN 工作的整个流程可以看出,它需要发送端、网络设备、接收端全链路支持,且需要 IP 层、TCP 层协同工作,因此对整个链路有一定要求。
总结
TCP 的一系列机制中,流量控制协调发送方发送速率和接收方处理速率,拥塞控制协调发送方发送速率与网络处理能力,它们共同限制发送方的发送速率。流量控制通过 ACK 响应中的 WINDOW 字段限制发送方的发送范围。拥塞控制通过各种拥塞控制算法在发送方内部维护拥塞窗口限制发送速率。经典的 Reno 算法通过慢启动、拥塞控制、快速重传与快速恢复机制实现拥塞控制,提高吞吐量并规避网络拥塞。Linux 当前默认的 Cubic 算法通过三次函数建模,将恢复过程与 RTT 解绑,并通过参数化提高了可配置性。新型的 BBR 算法将网络建模为带宽和时延两个本质特征,对两个特征进行主动探测,并将发送控制在带宽时延积这一理论最优操作点附近,改变了基于丢包来判断网络拥塞的反应性思路。