1986年10月,美国劳伦斯伯克利实验室(LBL)到加州大学伯克利分校之间的网络连接发生了一件怪事。这两个地理上相距仅400码、中间只隔两个IMP跳站的站点,数据吞吐量从正常的32 Kbps骤降到40 bps——下降了近1000倍。这不是网络故障,而是人类历史上第一次记录到的"拥塞崩溃"(Congestion Collapse)。

Van Jacobson在调查这一现象时发现,问题的根源不在网络本身,而在TCP的实现方式。当时的TCP协议设计假设网络是可靠的,发送方会以最快的速度向网络注入数据。当网络变得拥堵时,TCP的响应方式恰恰让情况变得更糟:数据包丢失触发重传,重传加剧拥塞,更多数据包丢失,更多重传。一个正向反馈循环就此形成。

这场危机催生了TCP拥塞控制机制,而其中最核心也最棘手的问题就是:发送方如何判断一个数据包已经丢失?

一个看似简单的问题

在理想世界中,丢包检测很简单:发送一个数据包,等待确认(ACK)。如果超时未收到确认,就认为数据包丢失,重新发送。

但现实远比这复杂。网络延迟是动态变化的,一次往返时间(RTT)可能从几十毫秒到几百毫秒不等。如果把超时时间设得太短,数据包可能只是在途中延迟了,却被误判为丢失,导致不必要的重传——这就是"虚假重传"(Spurious Retransmission)。如果把超时时间设得太长,真正的丢包又要等很久才能被发现,严重影响传输效率。

Jacobson在1988年的经典论文《Congestion Avoidance and Control》中提出了一个深刻的洞见:重传超时(RTO)的计算必须同时考虑RTT的平均值和变化幅度。他给出的公式成为了TCP实现的基石:

SRTT = (1 - α) × SRTT + α × R      // α = 1/8
RTTVAR = (1 - β) × RTTVAR + β × |SRTT - R|   // β = 1/4
RTO = SRTT + 4 × RTTVAR

其中SRTT是平滑往返时间,RTTVAR是往返时间变化量,R是新测量的RTT样本。这个公式的精妙之处在于:当网络延迟稳定时,RTTVAR很小,RTO接近SRTT;当延迟波动剧烈时,RTTVAR增大,RTO自动扩展,为延迟峰值预留空间。

然而,这个算法有一个致命的缺陷。

Karn算法的诞生:重传二义性

1987年,Phil Karn和Craig Partridge在SIGCOMM会议上发表了一篇题为"Improving Round-Trip Time Estimates in Reliable Transport Protocols"的论文,指出了一个被称为"重传二义性"(Retransmission Ambiguity)的问题。

考虑这个场景:发送方发送了数据包A,超时后重传为A’。随后收到了一个确认包。问题是:这个确认是对原始包A的确认,还是对重传包A’的确认?

如果是对A的确认,实际RTT很长;如果是对A’的确认,实际RTT很短。但发送方无法区分这两种情况。如果用错误的RTT样本更新RTO估计,可能导致灾难性的后果:低估RTT会触发更多虚假重传,高估RTT会降低传输效率。

Karn提出的解决方案简洁而优雅:不要用重传过的数据包来测量RTT。如果发生了重传,直接将RTO加倍(指数退避),直到获得一个新的、未重传数据包的RTT样本。这个算法在RFC 6298中被标准化,至今仍是TCP实现的基础。

但是,Karn算法也有代价:当网络延迟突然增加时(比如移动网络切换基站),发送方无法获得新的RTT样本,RTO会持续指数增长,可能造成长时间的传输停滞。

快速重传:不等超时的智能响应

超时重传有一个根本性的缺陷:RTO通常至少是RTT的2倍,在某些实现中甚至有1秒的下限。这意味着每次丢包都要等待很长时间才能被发现。

1988年,Jacobson提出了一个关键观察:接收方收到乱序数据包时会立即发送重复ACK。假设发送方连续发送数据包1、2、3、4、5。如果数据包2丢失,接收方收到3时会发送ACK 2(表示期待数据包2),收到4时发送ACK 2,收到5时发送ACK 2。这些"重复ACK"实际上是在告诉发送方:“我收到了你发的3、4、5,但我还在等2”。

快速重传(Fast Retransmit)算法利用这个信号:当发送方收到三个重复ACK时,立即重传丢失的数据包,而不必等待超时

为什么是三个?这是对网络乱序的容忍度。Jacobson在论文中指出,少量重复ACK(1-2个)可能只是网络中暂时的乱序,不一定是丢包。三个重复ACK提供了更高的置信度。

这个算法在TCP Reno中首次实现,成为了TCP演进的重要里程碑。但它也暴露了一个新问题。

多丢包困境:Reno的软肋

快速重传优雅地解决了单丢包问题,但当同一窗口内有多个数据包丢失时,Reno的实现就显得力不从心了。

考虑这个场景:发送方发送数据包1-10,其中数据包2和5丢失。接收方收到3、4、6、7、8、9、10后都会发送重复ACK(确认号都是2)。当发送方收到三个重复ACK后,快速重传数据包2。数据包2到达后,接收方发送ACK 5(表示期待数据包5)。

问题来了:发送方只收到一个确认号为5的ACK,不足以触发另一次快速重传。它必须等待超时才能重传数据包5。这就是著名的"部分确认"(Partial ACK)问题。

NewReno:一个状态的革命

1999年,RFC 2582定义了NewReno,对Reno的快速恢复机制进行了关键改进。NewReno的核心创新是在快速恢复阶段引入了一个新的概念:“恢复点”(Recovery Point)。

当收到三个重复ACK触发快速重传时,发送方记录当前发送的最大序列号为恢复点,然后进入快速恢复状态。在这个状态下,每当收到一个部分确认(确认了一些数据但未到达恢复点),发送方立即重传下一个未确认的数据包,而不是退出快速恢复。

这个看似简单的改变,使得NewReno能够在一次快速恢复中处理同一窗口内的多个丢包,大幅提升了丢包恢复效率。

RFC 6582在2012年将NewReno标准化为提议标准(Proposed Standard),至今仍是TCP实现的基准。

SACK:突破累积确认的枷锁

快速重传和NewReno都依赖于一个假设:接收方能够通过重复ACK告知发送方哪些数据包缺失。但TCP的确认机制是"累积"的——ACK号表示"我已收到该序号之前的所有数据"。这意味着接收方无法精确告诉发送方:“我收到了数据包1、3、5、7,缺少2、4、6”。

1996年,RFC 2018定义了选择性确认(Selective Acknowledgment,SACK)选项。SACK允许接收方在ACK包中携带多个数据块的范围,明确告知发送方已经收到了哪些不连续的数据。

SACK选项格式如下:

+--------+--------+
| Kind=5 | Length |
+--------+--------+--------+--------+
|     Left Edge of 1st Block       |
+--------+--------+--------+--------+
|    Right Edge of 1st Block       |
+--------+--------+--------+--------+
|            ...                    |

每个TCP选项最多可携带4个SACK块(与Timestamp选项共用时为3个)。通过这些信息,发送方可以精确知道哪些数据包已到达、哪些丢失,从而只重传真正丢失的数据包。

2003年,RFC 3517定义了基于SACK的保守丢包恢复算法,将SACK与快速重传/快速恢复正式整合。SACK显著提升了高延迟、高丢包网络中的TCP性能,特别是卫星链路和无线网络。

DSACK:检测虚假重传

SACK解决了精确报告接收数据的问题,但它还有另一个用途:检测虚假重传。

2000年,RFC 2883扩展了SACK选项,允许接收方报告重复接收的数据块(DSACK,Duplicate SACK)。如果发送方重传了一个数据包,但随后通过DSACK发现原始数据包其实已经到达,它就知道这是一次虚假重传。

DSACK的价值在于提供了反馈机制:发送方可以据此调整自己的丢包检测策略,避免在未来犯同样的错误。RFC 3708详细描述了如何利用DSACK检测虚假重传,这是Eifel算法和F-RTO算法的重要基础。

尾部丢包:被忽视的盲区

即使有了快速重传、NewReno和SACK,TCP的丢包检测仍有一个顽固的盲区:尾部丢包(Tail Loss)。

当数据传输接近尾声时,发送方发送最后几个数据包就停止了。如果其中某些数据包丢失,由于后续没有新数据包发送,接收方不会产生新的重复ACK,发送方也无法收到触发快速重传所需的三个重复ACK。最终,发送方只能等待超时。

对于短连接(如HTTP请求)来说,尾部丢包的影响尤其严重。一次RTO超时可能意味着数百毫秒甚至数秒的延迟。

2013年,Google的Nandita Dukkipati等人提出了尾部丢包探测(Tail Loss Probe,TLP)算法。TLP的核心思想很简单:在数据传输结束后的短暂延迟后,发送一个探测包

这个探测包要么是重传最后一个未确认的数据包,要么是发送一个新的数据包(如果发送窗口允许)。探测包的目的是触发接收方的ACK响应,从而获取足够的SACK信息来判断是否有丢包。

TLP的超时时间被设计为远小于RTO(通常是2个RTT),这样即使探测失败退化为RTO,损失也有限。根据Google的测试,TLP将99%的尾部丢包场景从RTO超时改善为快速恢复,平均节省数百毫秒的延迟。

RACK:从计数到时间的范式转变

尽管TLP解决了尾部丢包问题,TCP的丢包检测仍有一个根本性的缺陷:它依赖于固定的"重复ACK阈值"(DupThresh)——通常是3个。

这个设计有两个问题。第一,在高速网络中,数据包可能被大量重排序。如果重排序程度超过3个数据包,就会触发虚假快速重传。第二,当发送窗口较小(如短连接)时,可能根本没有足够的未确认数据包来产生3个重复ACK。

2018年,Google的Yuchung Cheng和Neal Cardwell在IETF提出了RACK(Recent ACKnowledgment)算法,2021年在RFC 8985中标准化。RACK的核心思想是:用时间代替计数来判断丢包

RACK维护每个数据包的发送时间戳。当收到一个ACK时,RACK更新一个"参考点"——最近被确认的数据包的发送时间和序列号。然后,对于每个未被确认的数据包,如果它的发送时间比参考点早超过一个"重排序窗口"(reordering window),就认为它已经丢失。

重排序窗口是自适应的。初始值为min_RTT/4,当检测到虚假重传(通过DSACK)时,窗口会线性增长;当连续16次恢复没有检测到虚假重传时,窗口重置。

RACK的优势在于:

  1. 对乱序更鲁棒:不是简单地数重复ACK个数,而是根据时间判断
  2. 支持小窗口:即使只有一个未确认数据包,也能判断是否丢失
  3. 统一框架:将快速重传、尾部丢包检测、RTO后的丢包检测都纳入同一个框架

RFC 8985将RACK和TLP结合,统称为RACK-TLP。这是目前TCP丢包检测的最新标准,已在Linux内核和BSD系统中实现。

从TCP到QUIC:经验的传承

2021年标准化的QUIC协议在设计丢包检测机制时,充分借鉴了TCP四十年演进的经验。RFC 9002定义的QUIC丢包检测算法与RACK-TLP高度相似:

  • 使用基于时间的丢包检测,而非计数
  • 维护每个包的发送时间戳
  • 采用自适应的重排序窗口
  • 使用探测超时(PTO)代替TLP

不同之处在于,QUIC从设计之初就内置了包序号单调递增(即使重传也使用新序号),消除了重传二义性问题。这使得QUIC的RTT测量更加精确,丢包检测也更加可靠。

永恒的权衡博弈

回顾TCP丢包检测机制的四十年演进,每一次进步都伴随着新的权衡:

  • 超时重传:简单可靠,但延迟高
  • 快速重传:响应快,但依赖固定阈值,对乱序敏感
  • SACK:精确报告,但需要选项协商,增加了复杂性
  • NewReno:处理多丢包,但仍可能退化为超时
  • TLP:解决尾部丢包,但增加了发送方的逻辑复杂度
  • RACK:更鲁棒,但依赖时间戳,在高速网络中需要更精细的时钟

没有完美的解决方案。每一次优化都是在对立目标间寻找平衡:更快响应 vs 更少虚假重传;更高吞吐量 vs 更低复杂度;更广泛适用 vs 更精细调优。

这正是网络协议设计的魅力所在。在这个充满不确定性的分布式系统中,每一次进步都是在与物理定律和工程现实的博弈中艰难前行的结果。从Jacobson那篇开创性的论文到今天的RACK-TLP,TCP丢包检测的演进史,是一部人类如何在极端约束下构建可靠系统的缩影。


参考文献

  1. Jacobson, V. (1988). Congestion Avoidance and Control. ACM SIGCOMM ‘88.
  2. Karn, P., & Partridge, C. (1987). Improving Round-Trip Time Estimates in Reliable Transport Protocols. ACM SIGCOMM ‘87.
  3. Paxson, V., & Allman, M. (2011). RFC 6298: Computing TCP’s Retransmission Timer.
  4. Mathis, M., et al. (1996). RFC 2018: TCP Selective Acknowledgment Options.
  5. Floyd, S., et al. (1999). RFC 2582: The NewReno Modification to TCP’s Fast Recovery Algorithm.
  6. Blanton, E., et al. (2012). RFC 6582: The NewReno Modification to TCP’s Fast Recovery Algorithm.
  7. Blanton, E., et al. (2003). RFC 3517: A Conservative Selective Acknowledgment (SACK)-based Loss Recovery Algorithm for TCP.
  8. Floyd, S., et al. (2000). RFC 2883: An Extension to the Selective Acknowledgement (SACK) Option for TCP.
  9. Cheng, Y., & Cardwell, N. (2021). RFC 8985: The RACK-TLP Loss Detection Algorithm for TCP.
  10. Dukkipati, N., et al. (2013). Tail Loss Probe (TLP): An Algorithm for Fast Recovery of Tail Losses. Google Technical Report.
  11. Iyengar, J., & Swett, I. (2021). RFC 9002: QUIC Loss Detection and Congestion Control.
  12. Sarolahti, P., & Kojo, M. (2004). On making TCP robust against spurious retransmissions. Computer Communications.
  13. Ludwig, R., & Katz, R. H. (2000). The Eifel Algorithm: Making TCP Robust Against Spurious Timeouts. ACM SIGCOMM.
  14. Sarolahti, P., et al. (2009). RFC 5682: Forward RTO-Recovery (F-RTO) for TCP.