1. TCP重传基础:协议栈回顾、重传机制原理、重传定时器详解

各位好,我是老赵。做网络故障排查十几年了,每次遇到TCP重传问题,我第一反应不是抓包,而是先问自己一句:重传到底是怎么触发的?

这个问题看似简单,但很多人栽跟头就栽在这里。今天咱们把TCP重传的底裤扒干净,从协议栈到定时器,一个细节都不放过。

1.1 TCP协议栈回顾:数据是怎么“流”起来的?

先快速过一遍TCP协议栈。说白了,TCP就是给IP包加了个“可靠传输”的保险。

我习惯把TCP协议栈分成三层来看:

  • 应用层:你写的程序,调用 send() 扔数据
  • 传输层:TCP把数据切成段,加上序列号、确认号、窗口大小
  • 网络层:IP只管把包扔出去,不保证到达

嗯,这里有个关键点:TCP的可靠性,是靠“确认+重传”这对组合拳实现的。发送方发出一个段,接收方必须回复ACK。如果发送方没收到ACK,就得重传。

核心概念:TCP不是“发一次就完事”,而是“发了就得等确认”。等不到,就再发一次。这就是重传的根源。

1.2 重传机制原理:三种武器,各有千秋

TCP重传不是只有一种方式。我遇到过不少工程师,以为重传就是“超时重传”,其实还有快速重传和选择性确认重传。咱们一个一个说。

1.2.1 超时重传(RTO)

这是最原始、最暴力的方式。发送方发了一个段,然后启动一个定时器。如果定时器到期还没收到ACK,就重传这个段。

定时器的时间不是固定的。TCP会动态计算RTT(往返时间),然后根据RTT算出一个RTO(重传超时时间)。

公式大概是这样:

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

α通常取0.125,β取0.25。说白了,RTO就是“平均RTT + 4倍抖动”。

注意:RTO有下限和上限。Linux内核里,RTO最小200ms,最大120s。我曾经遇到一个客户,内网延迟只有0.1ms,但RTO还是200ms起步,导致重传效率极低。后来调了内核参数才解决。

1.2.2 快速重传(Fast Retransmit)

超时重传有个问题:太慢了。200ms的RTO,在高速网络里简直是灾难。

快速重传的思路是:收到3个重复的ACK,就立即重传,不等定时器。

举个例子:

  • 发送方发了段1、2、3、4、5
  • 段2丢了,接收方收到段3、4、5,但只能回复ACK=2(期望收到段2)
  • 发送方连续收到3个ACK=2,就知道段2丢了,立即重传

这样,重传延迟从RTO级别降到了RTT级别。快了很多。

我的经验:快速重传是排查网络丢包的第一信号。抓包时看到连续3个重复ACK,基本可以断定有丢包了。我曾经用这个特征,半小时定位了一个跨洋链路的间歇性丢包问题。

1.2.3 选择性确认重传(SACK)

快速重传有个坑:它只能重传一个段。如果多个段丢了,它只能一个一个来,效率很低。

SACK解决了这个问题。接收方在ACK里带上SACK选项,告诉发送方:“我收到了哪些段,没收到哪些段”。发送方就能一次性重传所有丢失的段。

SACK的格式是这样的:

TCP Option - SACK:
  Kind: 5
  Length: 可变
  SACK Blocks:
    Block 1: [1000, 2000)  # 已收到的段范围
    Block 2: [3000, 4000)  # 已收到的段范围

嗯,这里要注意:SACK需要两端都支持。Linux默认开启,但有些嵌入式设备可能没开。

重传方式 触发条件 延迟 适用场景
超时重传 RTO定时器到期 高(200ms+) 严重拥塞、ACK全丢
快速重传 收到3个重复ACK 低(1个RTT) 单个段丢失
SACK重传 收到SACK选项 低(1个RTT) 多个段丢失

1.3 重传定时器详解:内核里的“闹钟”

TCP重传定时器,说白了就是内核里的一堆“闹钟”。每个TCP连接都有自己的一套定时器。

我习惯把重传相关的定时器分成三类:

  • RTO定时器:每个TCP连接只有一个,用于超时重传
  • 延迟ACK定时器:接收方用,延迟发送ACK,减少小包数量
  • 持续定时器:用于窗口探测,防止窗口更新丢失

咱们重点说RTO定时器。

1.3.1 RTO定时器的工作流程

RTO定时器的逻辑其实不复杂:

  1. 发送一个段,启动RTO定时器
  2. 收到ACK,停止定时器
  3. 如果定时器到期还没收到ACK,重传并加倍RTO(指数退避)
  4. 重传后,重新启动定时器

指数退避的规则是:每次超时,RTO翻倍。比如第一次RTO=200ms,第二次400ms,第三次800ms……直到上限120s。

为什么要指数退避?说白了,就是怕你重传把网络搞崩了。如果网络已经拥塞了,你还拼命重传,只会让情况更糟。指数退避是TCP的“绅士风度”——你堵车了,我就慢点开。

1.3.2 RTO计算的坑

RTO计算有个经典问题:重传二义性。

什么意思?你发了一个段,超时了,重传。然后收到一个ACK。这个ACK是对原始段的确认,还是对重传段的确认?

你分不清。如果RTT采样错了,RTO就会算错。

Linux内核的解决方案是:重传后,只采样第一个非重传段的RTT。说白了,就是“重传期间不采样,等稳定了再说”。

我曾经踩过的坑:有一次排查一个高延迟链路,发现RTO一直在跳,从200ms到2s不等。后来发现是内核版本太老,重传二义性处理有bug。升级内核后,RTO就稳定了。所以,遇到RTO异常,先检查内核版本。

1.4 本章知识体系:一张图看懂

下面这张图,把TCP重传的核心逻辑串起来了。从发送方到接收方,从定时器到重传方式,一目了然。

TCP重传核心逻辑图 TCP重传核心逻辑 发送方 接收方 发送数据段 回复ACK RTO定时器 超时 → 重传 指数退避 重传方式 超时重传 | 快速重传 SACK重传 接收方处理 生成ACK SACK选项 丢包检测 3个重复ACK / 超时 💡 核心要点: 重传是TCP可靠性的最后防线。理解RTO计算、快速重传触发条件、SACK工作机制,是排查重传问题的基本功。

1.5 实战:如何确认重传类型?

光说不练假把式。咱们看看实际抓包时,怎么区分这三种重传。

用tcpdump抓个包,重点关注这几个字段:

# 超时重传:看时间戳间隔
14:23:01.123456 IP A > B: Flags [S.], seq 1000, ack 2000
14:23:01.654321 IP A > B: Flags [.], seq 1000, ack 2000  # 间隔531ms,明显是RTO

# 快速重传:看重复ACK
14:23:02.000001 IP B > A: Flags [.], ack 1000
14:23:02.000002 IP B > A: Flags [.], ack 1000  # 重复ACK #1
14:23:02.000003 IP B > A: Flags [.], ack 1000  # 重复ACK #2
14:23:02.000004 IP B > A: Flags [.], ack 1000  # 重复ACK #3
14:23:02.000005 IP A > B: Flags [.], seq 1000  # 立即重传

# SACK重传:看SACK选项
14:23:03.000001 IP B > A: Flags [.], ack 1000, sack 2000-3000  # 告诉A:我收到了2000-3000

我的习惯:抓包时加上 -vvv 参数,能看到SACK选项的详细信息。另外,用Wireshark的“分析 → 专家信息”能自动标注重传包,省很多事。