3. 重传类型:RTO、快速重传、SACK 与 TLP
重传是 TCP 保证可靠传输的最后一道防线。但重传不是只有一种方式,不同的场景需要不同的重传策略。我见过不少工程师把重传简单理解为“超时了就重发”,结果线上问题定位时一头雾水。这一章,我们把这四种重传类型彻底讲透。
核心观点:重传策略的选择,本质是在“尽早恢复”和“避免误判”之间做权衡。没有一种重传是万能的。
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。
快速重传的流程:
- 发送端收到 3 个 DupACK
- 立即重传丢失的段(不等 RTO)
- 进入拥塞避免阶段(降低发送速率)
但快速重传有个问题:它不知道丢失了哪些段。如果一次丢了多个段,快速重传只能恢复第一个丢失的段,后面的段还得等 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 的兼容性问题。