4. TCP 重传机制详解:超时重传、快速重传、SACK 与 DupACK

TCP 重传,说白了就是网络世界的「后悔药」。数据包丢了,就得重发。但怎么发、什么时候发、发哪些——这里面门道很深。我做了这么多年网络排障,见过太多因为重传机制配置不当导致的性能雪崩。今天咱们就把这四种核心机制掰开揉碎了讲清楚。

核心要点:TCP 重传不是单一机制,而是由超时重传(RTO)、快速重传、选择性确认(SACK)和重复确认(DupACK)协同工作的组合拳。理解它们的触发条件和交互逻辑,是定位丢包问题的基本功。

TCP 重传机制决策流程 TCP 重传机制决策流程 数据包发送 收到 ACK? 正常传输 丢包/延迟 收到 DupACK? ≥3 个 DupACK 快速重传 RTO 超时重传 SACK 选择性重传 避坑提示 RTO 默认初始值 1 秒 快速重传需双方支持 SACK

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 四种机制的协同工作

这四种机制不是孤立的,它们是一个整体。我画个场景你就明白了:

  1. 正常情况:发送方发数据,接收方回 ACK,一切正常。
  2. 轻微乱序:接收方发 1-2 个 DupACK,发送方不理睬,等乱序恢复。
  3. 丢包发生:接收方发 3 个 DupACK,发送方触发快速重传。
  4. 批量丢包:如果 SACK 开启,发送方根据 SACK 信息一次性重传所有丢失包。
  5. 极端情况:如果所有 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 超时。