3. 超时重传(RTO):RTO的计算原理,指数退避算法,重传定时器管理

超时重传,说白了就是TCP的“最后一道防线”。

快速重传靠的是重复ACK来触发,但万一网络里ACK丢了,或者对端真的没收到数据呢?这时候就得靠RTO(Retransmission Timeout)来兜底。我刚开始调TCP栈的时候,总觉得RTO就是个固定超时,后来被线上问题狠狠教育了一顿——固定超时在弱网环境下要么频繁误判,要么死等不重传。

3.1 RTO的计算原理

RTO不能设得太短,也不能太长。太短会导致大量不必要的重传,浪费带宽;太长则让应用层感知到明显的卡顿。那到底怎么算才合理?

核心思路是:根据网络当前的RTT(往返时间)动态调整。TCP维护两个关键变量:

  • SRTT(平滑后的RTT):对实测RTT做加权平均,过滤掉突发抖动。
  • RTTVAR(RTT偏差):衡量RTT的波动程度。

标准算法(RFC 6298)的计算步骤如下:

  1. 首次测量:拿到第一个RTT样本后,SRTT = RTT,RTTVAR = RTT / 2。
  2. 后续更新:每收到一个ACK,更新SRTT和RTTVAR。
    RTTVAR = (3/4) * RTTVAR + (1/4) * |SRTT - R'|
    SRTT   = (7/8) * SRTT   + (1/8) * R'
    其中R'是本次实测的RTT值。
  3. 计算RTO:
    RTO = SRTT + max(G, 4 * RTTVAR)
    G是时钟粒度(比如Linux下通常是1ms或10ms)。

关键点:RTO的下限通常是200ms(RFC 6298建议),上限是60秒。低于下限的RTO会被强制钳位到200ms,防止在局域网等低延迟场景下过于激进。

我在项目中遇到过一个问题:某个内网服务延迟只有0.5ms,但RTO被算出来只有几毫秒,结果网络稍微抖动一下就疯狂重传。后来查了内核代码才发现,Linux把RTO_MIN设成了TCP_RTO_MIN(200ms),这才避免了误伤。

3.2 指数退避算法

重传一次没收到ACK,怎么办?再等同样的时间?不行。如果网络真的拥堵了,你越急它越堵。

指数退避的思路很简单:每次重传后,把RTO翻倍。

重传次数 RTO值(初始200ms) 说明
第1次 200ms 正常超时
第2次 400ms 翻倍
第3次 800ms 再翻倍
第4次 1.6s 继续翻倍
第5次 3.2s 直到上限60s

为什么要这么做?你想想看,如果网络已经拥塞了,你还在拼命发数据,只会让拥塞更严重。指数退避相当于给网络一个“喘息”的机会。等退避到一定程度,网络可能已经恢复了,这时候再重传成功率更高。

避坑指南:我曾经调试过一个视频流服务,发现RTO退避到60秒后,用户端直接卡死。原因是应用层没有做超时兜底,TCP重传次数耗尽后连接断开,但应用层还在傻等。后来我们在应用层加了一个5秒的心跳检测,配合TCP的keepalive,才解决了这个问题。

3.3 重传定时器管理

定时器管理是RTO落地的关键。TCP为每个连接维护一个重传定时器,它的状态机其实不复杂:

重传定时器状态机 空闲(无数据在途) 运行(等待ACK) 超时(触发重传) 退避(RTO翻倍) 连接关闭(重传耗尽) 发送数据 收到ACK 定时器到期 RTO翻倍,重新计时 收到ACK,恢复计时 重传次数达上限 说明: 定时器在收到ACK后重置为当前RTO值;若进入退避状态,RTO翻倍并重新启动定时器。

定时器的核心操作只有三个:

  • 启动:发送数据包时,如果定时器没在跑,就启动它,超时时间设为当前RTO。
  • 停止:收到ACK确认所有数据都已到达,停止定时器。
  • 重启:收到部分ACK(比如选择性确认SACK),但还有数据在途,则重启定时器。

注意:不要每次收到ACK都重启定时器。只有确认了新的数据被对端接收时,才需要重置。否则,如果网络延迟突然变大,频繁重启会导致RTO永远无法触发,这就是所谓的“定时器饿死”问题。

我记得有一次排查一个下载慢的问题,发现客户端RTO定时器一直在重启,但实际数据根本没传过去。原因是服务端发送了窗口更新,客户端误以为有新数据到达,就把定时器重置了。结果就是:数据丢了,但定时器永远不超时,客户端一直傻等。后来我们在代码里加了一个判断——只有确认新数据序号增长时,才重置定时器。

3.4 实践中的调优建议

RTO的默认参数在大多数场景下够用,但如果你在做实时性要求高的应用(比如游戏、视频会议),可以考虑以下调整:

场景 建议调整 风险
低延迟内网 降低RTO_MIN到50ms 外网抖动时可能误重传
高丢包无线网络 增加重传次数上限(默认15次) 连接断开变慢
实时音视频 关闭指数退避,使用固定RTO 可能加剧网络拥塞

我的习惯:在Linux上,我一般通过sysctl调整net.ipv4.tcp_rto_min和net.ipv4.tcp_retries2。但注意,这些是全局参数,会影响所有连接。如果只想针对特定连接,可以用setsockopt设置TCP_NODELAY和TCP_QUICKACK,间接影响RTO行为。

嗯,RTO这块其实还有很多细节,比如RTT的采样时机(Karn算法)、重传后的RTO计算(避免歧义性ACK)。但掌握了上面这些,你已经能应对90%的RTO相关问题了。剩下的,等遇到具体坑的时候再深挖也不迟。