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定时器的逻辑其实不复杂:
- 发送一个段,启动RTO定时器
- 收到ACK,停止定时器
- 如果定时器到期还没收到ACK,重传并加倍RTO(指数退避)
- 重传后,重新启动定时器
指数退避的规则是:每次超时,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重传的核心逻辑串起来了。从发送方到接收方,从定时器到重传方式,一目了然。
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的“分析 → 专家信息”能自动标注重传包,省很多事。