一次API请求,平均延迟50毫秒,P99却飙到了800毫秒。排查应用代码、数据库查询、缓存命中,一切正常。最后发现问题出在TCP层——一次重传超时。

这不是个例。Microsoft对全球四个生产应用的研究显示,在印度地区,TCP重传超时贡献了15%-20%的性能差异;而在95分位延迟上,美国用户的响应速度是印度用户的三倍。问题的根源不是应用代码,而是TCP在丢包面前的行为模式。

两类重传,两种命运

TCP检测丢包有两种方式,它们的延迟代价天差地别。

超时重传(RTO):发送方为每个发出的数据包启动一个计时器,如果超时未收到确认,就认为数据包丢失,重新发送。问题是,这个超时值(RTO)通常在数百毫秒到数秒之间——对于一次普通的HTTP请求来说,这已经是灾难性的延迟。

快速重传(Fast Retransmit):接收方每收到一个失序的包,就会发送一个重复确认(Duplicate ACK)。当发送方连续收到三个重复确认时,不需要等待超时,立即重传丢失的包。这种方式可以在一个往返时间(RTT)内完成恢复,延迟代价要小得多。

关键区别在于时间尺度:快速重传在RTT级别完成,超时重传则要等待RTO——而RTO往往是RTT的数倍甚至数十倍。

sequenceDiagram
    participant Sender
    participant Network
    participant Receiver
    
    Sender->>Network: Segment 1 (seq=1000)
    Network->>Receiver: Segment 1 delivered
    Receiver->>Sender: ACK 2000
    
    Sender->>Network: Segment 2 (seq=2000) LOST
    Sender->>Network: Segment 3 (seq=3000)
    Network->>Receiver: Segment 3 delivered
    Receiver->>Sender: DupACK 2000 (期待2000)
    
    Sender->>Network: Segment 4 (seq=4000)
    Network->>Receiver: Segment 4 delivered
    Receiver->>Sender: DupACK 2000
    
    Sender->>Network: Segment 5 (seq=5000)
    Network->>Receiver: Segment 5 delivered
    Receiver->>Sender: DupACK 2000 (第3个重复ACK)
    
    Note over Sender: 收到3个DupACK,触发快速重传
    Sender->>Network: Retransmit Segment 2
    Network->>Receiver: Segment 2 delivered
    Receiver->>Sender: ACK 6000 (确认所有数据)

但现实是残酷的:如果丢失的是数据传输末尾的包,或者丢失的包数量不足以触发三个重复确认,快速重传就无法启动,只能等待RTO超时。这就是所谓的"尾部丢失"问题,也是长尾延迟的主要来源。

RTO是如何计算的

RTO的计算经历了三十多年的演进,核心思想来自Van Jacobson在1988年发表的经典论文《Congestion Avoidance and Control》。

Jacobson观察到,当时的TCP实现存在一个致命缺陷:RTT估计器只跟踪均值,不考虑方差。当网络负载从0升到75%时,RTT和RTT方差都会增长约四倍。如果超时值固定为均值的某个倍数,在高负载时就会出现大量伪超时——数据包只是被延迟了,却被错误地认为丢失。

Jacobson提出的算法同时估计RTT的均值(SRTT)和方差(RTTVAR):

SRTT = (1 - α) × SRTT + α × R
RTTVAR = (1 - β) × RTTVAR + β × |SRTT - R|
RTO = SRTT + max(G, 4 × RTTVAR)

其中R是新测量的RTT样本,α=1/8,β=1/4,G是时钟粒度。

这个公式的直觉是:超时值应该等于均值加上四倍方差。四倍系数的来源是,在慢启动阶段窗口每轮翻倍,RTT也会翻倍,所以R+4V > 2R,足以覆盖下一轮的RTT增长。

RFC 6298在2011年将这套算法标准化,并规定初始RTO为1秒(早期版本是3秒),最小RTO也是1秒。为什么是1秒而不是更小?因为研究表明,约97.5%的连接RTT小于1秒,较小的超时值可以减少真正的丢包恢复延迟,同时只有约2.5%的连接会因为RTT>1秒而产生伪重传。

Karn算法:避免重传歧义

还有一个问题:当发生重传时,收到的ACK是对应原始传输还是重传?这就是"重传歧义"问题。

Phil Karn和Craig Partridge在1987年提出的解决方案很简单:不使用重传数据包的RTT样本。只有那些未被重传过的数据包,其RTT测量才用于更新SRTT和RTTVAR。

这个规则在RFC 6298中被称为"Karn算法",至今仍是TCP实现的标配。

指数退避:当网络严重拥塞时

如果第一次重传仍然没有收到确认怎么办?TCP采用指数退避策略:每次超时后,RTO值翻倍。

第一次超时RTO=1秒,第二次=2秒,第三次=4秒,依此类推。Linux默认最多重试15次,总超时时间可达数分钟。这种设计是为了在网络严重拥塞时给网络喘息的时间,避免"火上浇油"。

但这也意味着,一旦触发超时重传,延迟会急剧增长。一个初始RTO为200毫秒的连接,三次超时后等待时间已经超过1.4秒。

长尾延迟的真实代价

Microsoft在2019年发表的研究提供了生产环境的实证数据。他们分析了Azure Front Door(Microsoft的CDN)上四个生产应用在不同地区的性能:

地区 整体丢包率 RTO占比
美国/欧洲 基准 40-70%的丢包连接可通过快速重传恢复
巴西 高30-40% RTO占比适中
印度 高30-40% 60-80%的丢包连接需要RTO恢复

关键发现:

  • 在75分位,慢速重传(RTO)贡献了印度与美国之间15%-20%的性能差异
  • 在95分位,印度用户的响应时间是美国的3倍
  • 即使排除所有发生RTO的请求,印度用户的95分位延迟仍比美国高30%

这说明TCP重传是性能差异的重要因素,但不是唯一因素。网络基础设施的整体差异同样重要。

为什么印度更依赖RTO?

研究指出,印度地区的网络特点是丢包率高、且丢包模式不利于快速重传。当丢失的是传输末尾的包,或者窗口较小导致发出的包不足以产生三个重复确认时,只能依赖RTO。

这种情况在短请求/响应模式下尤其常见——而这正是大量API调用的典型模式。

现代TCP的改进

过去十年,TCP社区发展出多项改进措施来减少重传延迟。

SACK:选择性确认

RFC 2018定义的选择性确认(SACK)允许接收方告知发送方具体哪些数据段已收到。这使得发送方能够精确重传丢失的数据,而不是从丢失点开始重传所有数据。

SACK是现代TCP实现的标准配置,但它的作用主要在于减少不必要的重传,而非加快丢包检测。

F-RTO:检测伪超时

RFC 4138定义的Forward RTO-Recovery算法用于检测"伪超时"——当网络突然延迟增加导致超时,但数据包实际上并未丢失时。

F-RTO的思路是:超时后重传第一个包,然后观察下一个ACK。如果ACK确认的是新数据(而非重传的数据),说明原始数据已经到达,这是一次伪超时。此时可以避免不必要的窗口减半。

TLP:尾部丢失探测

Tail Loss Probe(TLP)是解决尾部丢失问题的关键技术。当发送方完成数据发送后,不会被动等待RTO,而是在两个RTT后主动发送一个探测包。

这个探测包可以是新数据(如果应用层还有数据要发),也可以是最后一个包的重传。探测包的目的不是传输数据,而是触发ACK反馈——通过ACK,发送方可以推断哪些包丢失,然后进入快速重传而非RTO恢复。

研究表明,TLP可以将高速网络中的总传输时间减少38%,将重传等待时间减少81%。

RACK:基于时间的丢包检测

RFC 8985定义的RACK(Recent ACKnowledgment)算法是对传统重复ACK计数的重大改进。

传统方法的问题是:它依赖包计数,而非时间。当窗口大小变化、或存在重排序时,固定的"三个重复ACK"阈值可能过低(导致伪重传)或过高(延迟真正的重传)。

RACK的思路是:记录每个包的发送时间戳。如果某个包T1发送,而比它晚发送的包T2已经被确认,那么T1要么丢失,要么被严重重排序。RACK会等待一个"重排序窗口"(通常是RTT的一小部分),然后标记T1为丢失。

RACK的优势在于:

  • 不依赖窗口大小,适用于应用受限的流量
  • 可以检测重传包的再次丢失
  • 对重排序更有弹性

RACK-TLP已成为现代Linux内核的默认TCP丢包检测算法。

P99延迟的工程含义

对于在线服务,P99延迟往往比平均延迟更重要。原因在于分布式系统的"放大效应":如果一个服务有10个串行依赖,每个依赖的P99延迟是100毫秒,那么整体P99延迟可以高达数百毫秒——因为只要有一个依赖变慢,整体就会变慢。

TCP重传对P99的影响尤为显著:

  • 快速重传增加约1个RTT的延迟
  • RTO重传增加约1秒的延迟(初始值)
  • 多次超时重传会导致指数级延迟增长

在微服务架构中,一次TCP重传可能会被上层重试机制放大。如果超时设置不当,一次RTO可能触发多次重试,形成级联效应。

生产环境的调优策略

1. 监控TCP重传指标

Linux系统提供了丰富的TCP统计信息:

# 查看TCP重传统计
netstat -s | grep -i retrans
# 或使用nstat
nstat -az | grep -i retrans

# 关键指标:
# TcpRetransSegs: 重传段总数
# TcpExtTCPSlowStartRetrans: 慢启动期间的重传
# TcpExtTCPFastRetrans: 快速重传次数
# TcpExtTCPLostRetransmit: 重传包再次丢失

2. 调整TCP参数

对于特定场景,可以调整以下内核参数:

# 减少TCP重试次数(缩短故障恢复时间)
sysctl -w net.ipv4.tcp_retries2=5

# 启用F-RTO(Linux默认启用)
sysctl -w net.ipv4.tcp_frto=2

# 启用SACK(Linux默认启用)
sysctl -w net.ipv4.tcp_sack=1

# 降低最小RTO(谨慎使用)
# Linux默认最小RTO为200ms(HZ=1000时)

注意:降低tcp_retries2会缩短连接断开时间,但也可能导致在丢包率较高的网络中过早放弃。需要根据实际场景权衡。

3. 应用层超时设置

应用层超时应该大于TCP层的重传时间,否则会过早触发应用层重试。一个粗略的经验公式:

应用超时 > RTT + (tcp_retries2 对应的超时时间)

Linux的tcp_retries2=15对应约924秒的总超时(指数退避),这显然不现实。实践中,tcp_retries2=5对应约130秒,tcp_retries2=3对应约12秒。

4. 选择合适的拥塞控制算法

不同的拥塞控制算法对丢包的反应不同:

  • CUBIC(Linux默认):丢包时窗口减半,适合大多数场景
  • BBR:不依赖丢包信号,在丢包率较高的网络中表现更好,但可能导致更高的重传率
  • DCTCP(数据中心TCP):对延迟更敏感,适合数据中心内部

对于跨数据中心通信,BBR可能是更好的选择。但需要注意BBR v1在高丢包率下会产生更多重传。

TCP重传不是bug

理解TCP重传的关键在于认识到:它不是bug,而是设计使然。TCP的目标是可靠传输,重传是实现可靠性的核心机制。问题在于,这个设计假设了某些网络特性——适度的丢包率、相对稳定的RTT、充足的窗口大小——而这些假设在现代网络环境中并不总是成立。

当API延迟出现异常高的P99时,除了检查应用代码,也应该看看网络层。一次TCP重传超时可能意味着数秒的额外延迟,这对于用户体验来说是难以接受的。通过启用现代TCP特性(SACK、F-RTO、RACK-TLP)、合理设置超时参数、选择合适的拥塞控制算法,可以显著减少重传对延迟的影响。

但更重要的是,在系统设计层面,要对网络不可靠性有充分预期。重传会发生,延迟会波动,应用层应该有相应的容错和降级机制——这才是构建高可用系统的正确姿势。


参考资料

  1. RFC 6298: Computing TCP’s Retransmission Timer (June 2011)
  2. RFC 8985: The RACK-TLP Loss Detection Algorithm for TCP (February 2021)
  3. RFC 5682: Forward RTO-Recovery (F-RTO) (September 2009)
  4. RFC 2018: TCP Selective Acknowledgment Options (October 1996)
  5. Jacobson, V. & Karels, M. “Congestion Avoidance and Control”, SIGCOMM 1988
  6. Karn, P. & Partridge, C. “Improving Round-Trip Time Estimates in Reliable Transport Protocols”, SIGCOMM 1987
  7. Gao, R. et al. “Impact of TCP Loss on Regional Application Performance”, Microsoft Research, 2019
  8. Flach, T. et al. “Reducing Web Latency: the Virtue of Gentle Aggression”, Google Research
  9. Dukkipati, N. et al. “An Argument for Increasing TCP’s Initial Congestion Window”, Google Research