3. 重传类型:RTO、快速重传、SACK 与 TLP

重传是 TCP 保证可靠传输的最后一道防线。但重传不是只有一种方式,不同的场景需要不同的重传策略。我见过不少工程师把重传简单理解为“超时了就重发”,结果线上问题定位时一头雾水。这一章,我们把这四种重传类型彻底讲透。

核心观点:重传策略的选择,本质是在“尽早恢复”和“避免误判”之间做权衡。没有一种重传是万能的。

TCP 重传类型决策树 数据包丢失 RTO 超时重传 非超时触发 快速重传(3次DupACK) TLP 尾部丢失探测 SACK 选择性确认 说明: RTO 是最后手段,代价最大;快速重传依赖重复 ACK 触发; SACK 是快速重传的增强,告知发送端哪些段已收到;TLP 用于探测尾部丢失。

3.1 超时重传(RTO)—— 最后的保险丝

RTO(Retransmission Timeout)是最原始的重传机制。发送端发出一个数据段后,启动一个定时器。如果定时器到期还没收到 ACK,就认为这个段丢了,直接重传。

RTO 的核心在于超时时间的计算。TCP 会动态估算 RTT(往返时间),然后根据 RTT 的波动来设置 RTO。标准算法是:

SRTT = (1 - α) × SRTT + α × RTT_sample
RTTVAR = (1 - β) × RTTVAR + β × |SRTT - RTT_sample|
RTO = SRTT + 4 × RTTVAR

其中 α 通常取 1/8,β 取 1/4。说白了,RTO 就是“平均 RTT 加上 4 倍抖动”。

我曾经踩过的坑:有一次线上服务间歇性卡顿,抓包发现 RTO 频繁触发。排查到最后,是中间网络设备引入了巨大的 RTT 抖动。RTT 从 10ms 突然跳到 500ms,RTO 估算跟不上,导致大量超时重传。后来我们调整了内核的 tcp_rto_min 参数,才稳住局面。

RTO 的缺点很明显:等待时间太长。如果 RTO 是 200ms,这 200ms 内发送端什么都不做,白白浪费了带宽。所以,RTO 是最后的手段,不到万不得已不用。

3.2 快速重传 —— 不等超时,主动出击

快速重传(Fast Retransmit)解决了 RTO 等待时间过长的问题。它的触发条件是:发送端收到 3 个或更多重复的 ACK(DupACK)。

为什么是 3 个?因为网络包乱序也可能导致重复 ACK。1 个 DupACK 可能是乱序,2 个也可能是乱序,但连续 3 个 DupACK 基本可以确认是丢包了。这个阈值是经验值,Linux 内核里写死的。

我的个人习惯:在排查重传问题时,我会先用 tcpdump 抓包,然后过滤出 DupACK 的数量。如果看到连续 3 个以上的 DupACK,基本可以断定是快速重传在起作用。这时候我会关注乱序和丢包的比例,而不是直接怀疑 RTO。

快速重传的流程:

  1. 发送端收到 3 个 DupACK
  2. 立即重传丢失的段(不等 RTO)
  3. 进入拥塞避免阶段(降低发送速率)

但快速重传有个问题:它不知道丢失了哪些段。如果一次丢了多个段,快速重传只能恢复第一个丢失的段,后面的段还得等 RTO。这就引出了 SACK。

3.3 选择性确认(SACK)—— 精确打击

SACK(Selective Acknowledgment)是快速重传的增强版。它在 TCP 选项字段中携带一个 SACK Block,告诉发送端:“我收到了哪些不连续的数据”。

举个例子:发送端发了 1、2、3、4、5 五个段,接收端只收到了 1、2、5。没有 SACK 时,接收端只能 ACK 到 2,发送端不知道 3、4 丢了还是 5 丢了。有了 SACK,接收端可以明确告诉发送端:“我收到了 5,但 3 和 4 没收到”。

特性 快速重传(无 SACK) 快速重传 + SACK
丢失信息 只知道第一个丢失的段 知道所有丢失的段
重传效率 低,可能多次触发 高,一次重传所有丢失段
适用场景 单包丢失 多包丢失、突发丢包
内核支持 默认开启 需双方协商开启

避坑指南:我曾经遇到一个案例,两台服务器之间的 TCP 连接性能极差,但抓包发现快速重传很少触发。排查到最后,发现接收端的内核版本太老,不支持 SACK。发送端发了大量数据,接收端只能 ACK 到连续的部分,导致发送端频繁进入 RTO。升级内核后,SACK 协商成功,性能提升了 3 倍。

开启 SACK 的方法:Linux 下默认是开启的,可以通过 sysctl net.ipv4.tcp_sack 查看。如果为 0,手动开启即可。

3.4 TLP(尾部丢失探测)—— 提前发现尾部丢包

TLP(Tail Loss Probe)是相对较新的机制,Linux 3.10 以后才引入。它解决的是“尾部丢包”问题。

什么是尾部丢包?发送端发完一批数据后,最后几个段丢了。这时候接收端不会发 DupACK(因为没有后续数据触发),发送端只能等 RTO。RTO 通常几百毫秒,对于尾部丢包来说太长了。

TLP 的做法是:在发送完一批数据后,如果在一定时间内(通常是 2 个 RTT)没有收到 ACK,就主动发送一个探测包(Probe)。这个探测包可以是新的数据,也可以是重传最后一个未确认的段。

如果探测包触发了 ACK,发送端就知道尾部没丢,继续正常发送。如果探测包也丢了,或者触发了 DupACK,发送端就立即进入快速重传或 RTO。

我建议:如果你的业务场景是短连接、小数据量传输(比如 RPC 调用),TLP 的效果非常明显。我优化过一个广告竞价系统,每次请求的数据量只有几百字节,但尾部丢包导致 P99 延迟从 50ms 飙升到 300ms。开启 TLP 后,P99 降到了 80ms 以内。

TLP 的配置参数:

  • net.ipv4.tcp_early_retrans:控制 TLP 的启用级别,2 表示完全启用
  • net.ipv4.tcp_tlp_time:探测包的发送延迟,默认 2 个 RTT

3.5 四种重传的对比与选择

四种重传各有各的适用场景。我整理了一张对比表,方便你快速决策:

重传类型 触发条件 延迟 适用场景 代价
RTO 定时器超时 高(200ms+) 所有丢包的最后兜底 高,会降低发送速率
快速重传 3 个 DupACK 低(1 RTT) 单包丢失、突发丢包 中,触发拥塞控制
SACK 快速重传 + SACK 选项 低(1 RTT) 多包丢失、高带宽延迟 低,精确重传
TLP 尾部数据无 ACK 中(2 RTT) 短连接、小数据量 低,探测包开销小

嗯,这里要注意:这四种重传不是互斥的,而是协同工作的。TLP 先尝试探测尾部丢包,如果失败就触发快速重传或 RTO。SACK 是快速重传的辅助,让重传更精确。RTO 是最后的保险丝,确保不会无限等待。

实战建议:在性能调优时,我一般会先看快速重传和 SACK 的比例。如果快速重传占比高,说明网络丢包率较高,需要排查链路质量。如果 RTO 占比高,说明尾部丢包严重或者 RTT 抖动大,可以考虑开启 TLP 或调整 RTO 参数。如果 SACK 没有协商成功,优先解决 SACK 的兼容性问题。