4. TCP 重传机制详解:超时重传、快速重传、SACK 与 DupACK
TCP 重传,说白了就是网络世界的「后悔药」。数据包丢了,就得重发。但怎么发、什么时候发、发哪些——这里面门道很深。我做了这么多年网络排障,见过太多因为重传机制配置不当导致的性能雪崩。今天咱们就把这四种核心机制掰开揉碎了讲清楚。
核心要点:TCP 重传不是单一机制,而是由超时重传(RTO)、快速重传、选择性确认(SACK)和重复确认(DupACK)协同工作的组合拳。理解它们的触发条件和交互逻辑,是定位丢包问题的基本功。
4.1 超时重传(RTO)—— 最后的保底方案
超时重传是最原始、也是最无奈的重传方式。发送方发了一个数据包,然后启动一个定时器。如果定时器到期还没收到 ACK,就认为包丢了,直接重传。
这个定时器的时长就是 RTO(Retransmission TimeOut)。它不是固定的,而是动态计算的。我记得刚入行时,有个老工程师跟我说:「RTO 算不好,网络就废了一半。」后来我踩过坑才明白这句话的分量。
RTO 的计算逻辑
RTO 基于 RTT(Round Trip Time)计算。Linux 内核里有一套经典的算法:
SRTT = (1 - α) × SRTT + α × RTT // 平滑 RTT
RTTVAR = (1 - β) × RTTVAR + β × |SRTT - RTT| // RTT 偏差
RTO = SRTT + 4 × RTTVAR // 最终 RTO
α 通常取 1/8,β 取 1/4。说白了,RTO 就是「平均往返时间 + 4 倍抖动余量」。为什么要加 4 倍?因为网络抖动是常态,留够余量才能避免不必要的重传。
我曾经踩过的坑:有一次线上服务间歇性卡顿,抓包发现大量 RTO 重传。排查了半天,发现是中间防火墙启用了 TCP 流量整形,人为增加了 RTT 抖动。RTO 计算跟不上抖动变化,导致频繁超时。最后调整了防火墙策略才解决。
RTO 的底线
RFC 6298 规定 RTO 最小 1 秒,最大 60 秒。为什么最小是 1 秒?因为早期的网络环境差,太小的 RTO 会导致大量误重传。但在现代数据中心内部,RTT 往往只有几百微秒,1 秒的 RTO 下限就显得太保守了。
不过要注意,每次 RTO 超时后,下一次 RTO 会翻倍(指数退避)。这是为了防止网络拥塞时雪上加霜。
| 重传次数 | RTO 值(初始 1s) | 说明 |
|---|---|---|
| 第 1 次 | 1.0 s | 初始超时 |
| 第 2 次 | 2.0 s | 指数退避 |
| 第 3 次 | 4.0 s | 继续翻倍 |
| 第 4 次 | 8.0 s | 继续翻倍 |
| 第 5 次 | 16.0 s | 继续翻倍 |
| 第 6 次 | 32.0 s | 接近上限 |
| 第 7 次 | 60.0 s | 达到上限,不再增长 |
4.2 重复确认(DupACK)—— 接收方的「喊话」
超时重传太慢了。你想想看,如果 RTT 只有 10ms,却要等 1 秒才重传,这效率得多低?所以 TCP 设计了另一种机制:重复确认。
接收方收到乱序包时,会怎么做?它不会直接丢弃,而是把数据暂存在缓冲区里,然后重复发送「我期望的下一个序号」。这个重复的 ACK 就是 DupACK(Duplicate Acknowledgment)。
举个例子:
- 接收方已收到 Seq=1,期望 Seq=2
- 结果来了 Seq=3(乱序),接收方回复 ACK=2(还是期望 2)
- 接着来了 Seq=4,接收方继续回复 ACK=2
- 来了 Seq=5,还是回复 ACK=2
这三个连续的 ACK=2 就是 DupACK。发送方看到 3 个 DupACK,就知道 Seq=2 大概率丢了。
个人经验:抓包时看到 DupACK 不要慌。少量的 DupACK 是正常的,说明网络有轻微乱序。但如果连续出现几十上百个 DupACK,那基本可以断定有丢包了。
4.3 快速重传 —— 不等超时,立刻行动
快速重传的逻辑很简单:发送方收到 3 个 DupACK 后,不等 RTO 超时,立即重传丢失的报文段。
为什么是 3 个?不是 2 个也不是 4 个?
这是经验值。2 个 DupACK 可能只是网络轻微乱序(比如包 A 和包 B 交换了顺序),但 3 个 DupACK 基本可以确认是丢包。我见过有些系统把这个阈值改成 2,结果乱序场景下误重传率飙升。
// 快速重传的伪代码逻辑
if (dupacks_count >= 3) {
retransmit(lost_segment);
// 进入拥塞避免阶段
ssthresh = cwnd / 2;
cwnd = ssthresh + 3 * MSS;
}
注意看,快速重传后还会调整拥塞窗口。这是 TCP 的「自我修复」机制——既然丢了包,说明网络可能有点挤,那就降速。
4.4 选择性确认(SACK)—— 精准打击,不浪费子弹
快速重传有个大问题:它只知道某个包丢了,但不知道后面哪些包到了、哪些没到。所以它只能重传一个包,然后等 ACK。如果还有丢包,就得等下一个 3 个 DupACK 触发下一次快速重传。
这就好比打靶——你只知道第一枪没中,但不知道后面几枪的情况,只能一枪一枪试。
SACK 解决了这个问题。接收方在 ACK 里带上 SACK 选项,告诉发送方:「我收到了哪些数据块,哪些是空缺的。」
SACK 选项的格式长这样:
TCP Option - SACK:
Kind: 5
Length: 10
Left Edge: 1000 // 已收到数据块的左边界
Right Edge: 2000 // 已收到数据块的右边界
一个 SACK 选项可以携带最多 4 个数据块(因为 TCP 选项总长度限制 40 字节)。
| 特性 | 无 SACK | 有 SACK |
|---|---|---|
| 重传粒度 | 一次只能重传一个包 | 一次可重传多个丢失包 |
| 重传效率 | 低,需要多次 RTT | 高,一次 RTT 搞定 |
| 对乱序的容忍度 | 低,容易误判丢包 | 高,能区分乱序和丢包 |
| Linux 默认开启 | 否(旧内核) | 是(2.6.17+) |
实战建议:我个人习惯在服务器上强制开启 SACK。虽然会增加一点点 CPU 开销(解析 SACK 选项),但带来的重传效率提升是巨大的。尤其是在高延迟、高丢包率的链路上,SACK 几乎是必选项。
4.5 四种机制的协同工作
这四种机制不是孤立的,它们是一个整体。我画个场景你就明白了:
- 正常情况:发送方发数据,接收方回 ACK,一切正常。
- 轻微乱序:接收方发 1-2 个 DupACK,发送方不理睬,等乱序恢复。
- 丢包发生:接收方发 3 个 DupACK,发送方触发快速重传。
- 批量丢包:如果 SACK 开启,发送方根据 SACK 信息一次性重传所有丢失包。
- 极端情况:如果所有 DupACK 都丢了(或者接收方不支持 SACK),最终 RTO 超时兜底。
你看,这是一个层层递进的关系。快速重传比 RTO 快,SACK 比快速重传更精准。但 RTO 永远是最后的保险丝。
我曾经踩过的坑:有一次排查一个跨国链路的性能问题,发现大量 RTO 重传,但 DupACK 很少。后来抓包分析,发现中间链路的 MTU 有问题,导致大包被分片后部分丢失。接收方收不到完整包,自然不会发 ACK,更不会发 DupACK。发送方只能等 RTO 超时。这个案例告诉我:重传机制只是表象,根因可能藏在链路层。
4.6 抓包实战:如何识别这四种机制
在 Wireshark 里,识别这些机制其实很简单:
- RTO 重传:Wireshark 会标记为
[TCP Retransmission],并且时间戳显示距离上一个包超过 RTO 时间。 - 快速重传:标记为
[TCP Fast Retransmission],前面通常跟着 3 个 DupACK。 - DupACK:标记为
[TCP Dup ACK],并且 ACK 号与之前的 ACK 相同。 - SACK:在 TCP 选项里可以看到
SACK_PERM(协商阶段)和SACK(数据传输阶段)。
我个人习惯在 Wireshark 里设置一个过滤表达式:
tcp.analysis.retransmission || tcp.analysis.fast_retransmission || tcp.analysis.duplicate_ack
这个过滤器能一次性抓出所有重传相关的事件,是排障的第一步。
小技巧:在 Wireshark 的「Statistics → TCP Stream Graph → Time-Sequence Graph (Stevens)」里,可以直观看到重传的位置。如果图上出现「阶梯状」下降,基本就是快速重传;如果出现长时间的水平线后突然下降,那就是 RTO 超时。