引言
在前面的文章中,我们学习了TCP的拥塞控制机制(慢启动、拥塞避免等)。今天我们来学习TCP如何应对数据丢失:重传机制(Retransmission)。
为什么需要重传?
- 网络是不可靠的:数据包可能丢失、损坏、重复、乱序
- TCP要提供可靠传输:保证数据正确、完整、有序地到达
- 重传是可靠性的核心保障
今天我们来理解:
- ✅ 超时重传(RTO)的计算方法
- ✅ 快速重传的触发条件
- ✅ SACK选择性确认机制
- ✅ 如何用Wireshark分析重传问题
第一性原理:如何检测数据丢失?
两种丢失检测方式
1. 超时检测(Timeout-based)
原理:发送方设置一个定时器,如果超时还没收到ACK,认为数据丢失
发送方 接收方
| seq=100(发送) |
|---------------------------------->|
| 启动定时器(RTO=1秒) |
| |
| 等待ACK... |
| | (数据包丢失)
| 1秒后,超时! |
| seq=100(重传) |
|---------------------------------->|
| ACK=101 |
|<----------------------------------|
优点:可以检测到所有丢包 缺点:超时时间较长,影响性能
2. 重复ACK检测(Duplicate ACK)
原理:接收方收到乱序数据时,发送重复ACK,发送方收到3个重复ACK后立即重传
发送方 接收方
| seq=100(到达) |
|---------------------------------->|
| ACK=101 |
|<----------------------------------|
| |
| seq=101(丢失!) |
|----X |
| |
| seq=102(到达) |
|---------------------------------->|
| ACK=101(重复ACK #1) |
|<----------------------------------|
| |
| seq=103(到达) |
|---------------------------------->|
| ACK=101(重复ACK #2) |
|<----------------------------------|
| |
| seq=104(到达) |
|---------------------------------->|
| ACK=101(重复ACK #3) |
|<----------------------------------|
| |
| 收到3个重复ACK,立即重传! |
| seq=101(快速重传) |
|---------------------------------->|
| ACK=105(确认101-104都收到了) |
|<----------------------------------|
优点:快速检测,不需要等待超时 缺点:需要后续数据包到达才能触发
超时重传(RTO)
RTO(Retransmission Timeout)
RTO:重传超时时间,发送方等待ACK的最长时间
设置原则:
- 太短:可能导致不必要的重传(ACK还在路上)
- 太长:丢包后恢复慢,影响性能
- 最佳:略大于RTT(往返时延)
问题:RTT是动态变化的,如何设置RTO?
RTO计算算法
1. 经典算法(RFC 793)
测量RTT:
RTT_sample = 发送时间 - 收到ACK的时间
计算平滑RTT(SRTT):
SRTT = (1 - α) × SRTT + α × RTT_sample
α = 1/8(权重)
示例:
初始SRTT = 100ms
第一次测量:RTT_sample = 120ms
SRTT = (1 - 1/8) × 100 + 1/8 × 120
= 7/8 × 100 + 1/8 × 120
= 87.5 + 15
= 102.5ms
计算RTO:
RTO = SRTT × 2
示例:
SRTT = 102.5ms
RTO = 102.5 × 2 = 205ms
问题:没有考虑RTT的波动
2. Jacobson算法(RFC 6298,当前标准)
增加RTT变化量(RTTVAR):
初始值:
SRTT = 0
RTTVAR = 0
第一次测量:
SRTT = RTT_sample
RTTVAR = RTT_sample / 2
RTO = SRTT + 4 × RTTVAR
后续测量:
RTTVAR = (1 - β) × RTTVAR + β × |SRTT - RTT_sample|
SRTT = (1 - α) × SRTT + α × RTT_sample
RTO = SRTT + 4 × RTTVAR
α = 1/8
β = 1/4
示例:
[第一次测量]
RTT_sample = 100ms
SRTT = 100ms
RTTVAR = 50ms
RTO = 100 + 4 × 50 = 300ms
[第二次测量]
RTT_sample = 120ms
RTTVAR = (1 - 1/4) × 50 + 1/4 × |100 - 120|
= 3/4 × 50 + 1/4 × 20
= 37.5 + 5
= 42.5ms
SRTT = (1 - 1/8) × 100 + 1/8 × 120
= 7/8 × 100 + 1/8 × 120
= 87.5 + 15
= 102.5ms
RTO = 102.5 + 4 × 42.5 = 272.5ms
[第三次测量(RTT突然增大)]
RTT_sample = 200ms
RTTVAR = 3/4 × 42.5 + 1/4 × |102.5 - 200|
= 31.875 + 24.375
= 56.25ms
SRTT = 7/8 × 102.5 + 1/8 × 200
= 89.6875 + 25
= 114.6875ms
RTO = 114.6875 + 4 × 56.25 = 339.6875ms
关键点:
4 × RTTVAR:增加安全边界,应对RTT波动- RTT波动大时,RTO增大
- RTT稳定时,RTO减小
RTO的限制
最小值:
RTO_min = 200ms(Linux默认)
# 查看
sysctl net.ipv4.tcp_rto_min
# 输出:net.ipv4.tcp_rto_min = 200
最大值:
RTO_max = 120秒(Linux默认)
# 查看
sysctl net.ipv4.tcp_rto_max
# 输出:net.ipv4.tcp_rto_max = 120000(单位:毫秒)
RTO退避(Exponential Backoff)
问题:重传后又超时,说明网络拥塞严重
策略:指数退避,每次重传都加倍RTO
[第1次超时]
RTO = 300ms
重传
[第2次超时]
RTO = 300 × 2 = 600ms
重传
[第3次超时]
RTO = 600 × 2 = 1200ms
重传
[第4次超时]
RTO = 1200 × 2 = 2400ms
重传
...
最多重传15次(Linux默认)
配置重传次数:
# 查看
sysctl net.ipv4.tcp_retries2
# 输出:net.ipv4.tcp_retries2 = 15
# 调整(减少重传次数,加快连接失败检测)
sudo sysctl -w net.ipv4.tcp_retries2=5
快速重传(Fast Retransmit)
触发条件
收到3个重复ACK:
为什么是3个?
- 乱序:数据包可能因为路由不同而乱序到达,不一定丢失
- 第1个重复ACK:可能是乱序,不要着急重传
- 第2个重复ACK:可能还是乱序
- 第3个重复ACK:几乎可以确定丢包了,立即重传
示例:
发送方 接收方
| seq=1-10(10个报文段) |
|---------------------------------->|
| |
| seq=1(到达) |
| ACK=2 |
|<----------------------------------|
| |
| seq=2(丢失!) |
| |
| seq=3(到达,但期望seq=2) |
| ACK=2(重复ACK #1) |
|<----------------------------------|
| |
| seq=4(到达,但期望seq=2) |
| ACK=2(重复ACK #2) |
|<----------------------------------|
| |
| seq=5(到达,但期望seq=2) |
| ACK=2(重复ACK #3) |
|<----------------------------------|
| |
| ✅ 收到3个重复ACK,快速重传! |
| seq=2(重传) |
|---------------------------------->|
| ACK=6(确认2-5都收到了) |
|<----------------------------------|
快速重传 vs 超时重传
| 特性 | 快速重传 | 超时重传 |
|---|---|---|
| 检测方式 | 3个重复ACK | 超时 |
| 速度 | 快(几个RTT) | 慢(RTO通常几百毫秒) |
| 触发条件 | 需要后续数据包到达 | 任何情况都能触发 |
| cwnd变化 | 减半(快速恢复) | 降到初始值(慢启动) |
| 适用场景 | 单个数据包丢失 | 多个数据包丢失或连接断开 |
SACK:选择性确认
传统ACK的问题
问题:累积确认(Cumulative ACK)
发送方发送:seq=1, 2, 3, 4, 5
接收方收到:seq=1, 3, 4, 5(seq=2丢失)
传统ACK:
接收方只能发送ACK=2(期望下一个是2)
问题:
- 发送方不知道3、4、5已经到达
- 发送方可能重传2、3、4、5(浪费)
SACK机制
SACK(Selective Acknowledgment):选择性确认,告诉发送方哪些数据已经收到
SACK示例:
接收方收到:seq=1, 3, 4, 5(seq=2丢失)
SACK报文:
ACK=2(期望seq=2)
SACK: [3-6](告诉发送方:3、4、5已经收到)
发送方收到后:
- 知道只有seq=2丢失
- 只重传seq=2,不重传3、4、5
- 节省带宽
SACK的TCP选项格式
在三次握手时协商:
[第一次握手]
客户端 → 服务器: SYN, Options: [SACK Permitted]
含义:"我支持SACK"
[第二次握手]
服务器 → 客户端: SYN-ACK, Options: [SACK Permitted]
含义:"我也支持SACK"
[连接建立后]
双方都可以使用SACK
SACK选项格式:
TCP Options:
Kind: 5 (SACK)
Length: 10 bytes(每个SACK块8字节)
Left Edge: 3(起始序列号)
Right Edge: 6(结束序列号)
含义:序列号3-5已经收到(右边界不包含)
多个SACK块:
SACK: [3-6, 8-10, 12-15]
含义:
- 3、4、5已收到
- 8、9已收到
- 12、13、14已收到
缺失:
- 1、2、6、7、10、11尚未收到
启用SACK
Linux默认启用:
# 查看
sysctl net.ipv4.tcp_sack
# 输出:net.ipv4.tcp_sack = 1(启用)
# 如果禁用了,启用它
sudo sysctl -w net.ipv4.tcp_sack=1
SACK的优点和缺点
优点:
- ✅ 减少不必要的重传
- ✅ 提高带宽利用率
- ✅ 加快丢包恢复
缺点:
- ❌ 增加TCP头部开销(SACK选项)
- ❌ 需要双方都支持
- ❌ 实现复杂度增加
实战案例:Wireshark分析重传问题
案例1:用Wireshark分析超时重传
抓包:
# 启动抓包
sudo tcpdump -i any port 8080 -w /tmp/capture.pcap
# 模拟丢包(在另一个终端)
# ...进行网络操作...
# 停止抓包
# Ctrl+C
# 用Wireshark打开
wireshark /tmp/capture.pcap
Wireshark过滤器:
tcp.analysis.retransmission
# 只显示重传的数据包
分析重传:
No. Time Source Destination Info
100 0.000 192.168.1.100 192.168.1.200 seq=1000, len=1460
101 0.001 192.168.1.100 192.168.1.200 seq=2460, len=1460
102 0.002 192.168.1.100 192.168.1.200 seq=3920, len=1460
...
150 1.000 192.168.1.100 192.168.1.200 [TCP Retransmission] seq=2460, len=1460
↑Wireshark标注为重传↑
关键信息:
- No.101发送seq=2460
- No.150重传seq=2460
- 时间差:1.000秒(接近RTO)
- 结论:超时重传
案例2:用Wireshark分析快速重传
过滤器:
tcp.analysis.fast_retransmission
# 只显示快速重传的数据包
分析快速重传:
No. Time Source Destination Info
100 0.000 192.168.1.100 192.168.1.200 seq=1000, len=1460
101 0.001 192.168.1.100 192.168.1.200 seq=2460, len=1460(丢失)
102 0.002 192.168.1.100 192.168.1.200 seq=3920, len=1460
103 0.003 192.168.1.200 192.168.1.100 ACK=2460(期望seq=2460)
104 0.004 192.168.1.100 192.168.1.200 seq=5380, len=1460
105 0.005 192.168.1.200 192.168.1.100 ACK=2460(重复ACK #1)
106 0.006 192.168.1.100 192.168.1.200 seq=6840, len=1460
107 0.007 192.168.1.200 192.168.1.100 ACK=2460(重复ACK #2)
108 0.008 192.168.1.100 192.168.1.200 seq=8300, len=1460
109 0.009 192.168.1.200 192.168.1.100 ACK=2460(重复ACK #3)
110 0.010 192.168.1.100 192.168.1.200 [TCP Fast Retransmission] seq=2460, len=1460
↑快速重传↑
关键信息:
- 收到3个重复ACK(105、107、109)
- 立即重传seq=2460(110)
- 时间差:0.010秒(10ms,远小于RTO)
- 结论:快速重传
案例3:用Wireshark分析SACK
过滤器:
tcp.options.sack
# 只显示包含SACK选项的数据包
分析SACK:
No. Time Source Destination Info
105 0.005 192.168.1.200 192.168.1.100 ACK=2460, SACK: [3920-5380]
107 0.007 192.168.1.200 192.168.1.100 ACK=2460, SACK: [3920-6840]
109 0.009 192.168.1.200 192.168.1.100 ACK=2460, SACK: [3920-8300]
含义:
- ACK=2460:期望收到seq=2460
- SACK: [3920-5380]:seq=3920-5379已收到
- SACK: [3920-6840]:seq=3920-6839已收到
- SACK: [3920-8300]:seq=3920-8299已收到
缺失:seq=2460-3919
发送方收到后,只重传2460-3919,不重传后面的数据
查看SACK详细信息:
Wireshark → 右键点击数据包 → Protocol Preferences → TCP → Calculate conversation timestamps
可以看到:
- SACK Left Edge: 3920
- SACK Right Edge: 5380
- SACK Length: 1460
微服务场景的重传问题排查
问题1:大量重传导致接口慢
现象:订单接口响应时间从50ms增加到500ms
排查步骤:
步骤1:查看重传统计
# 查看重传次数
netstat -s | grep retransmit
# 输出
12345 segments retransmitted
123 fast retransmits
12222 retransmits in slow start
# 发现:大量重传!
步骤2:抓包分析
# 抓包5分钟
sudo tcpdump -i any port 8080 -w /tmp/order-service.pcap -W 5 -G 60
# 用Wireshark分析
wireshark /tmp/order-service.pcap
# 过滤重传
tcp.analysis.retransmission
步骤3:分析原因
发现:
- 大量超时重传(不是快速重传)
- RTO = 200ms(最小值)
- 网络延迟很高:RTT > 200ms
原因:
- 跨地域调用(北京 → 上海),RTT = 50ms
- 但网络抖动时,RTT突增到300ms
- RTO设置为200ms太小,导致不必要的超时重传
步骤4:解决方案
# 方案1:增大RTO最小值(谨慎!)
sudo ip route change default via <gateway> rto_min 300ms
# 方案2:优化网络路径(使用专线)
# 方案3:启用SACK和快速重传
sudo sysctl -w net.ipv4.tcp_sack=1
# 方案4:增加重传阈值(减少误判)
# 通过调整TCP参数
问题2:重传导致消息重复
现象:订单服务收到重复的支付回调,导致重复扣款
原因:
支付网关 订单服务
| 支付成功回调(seq=100) |
|--------------------------------->|
| | 处理慢(200ms)
| |
| 超时(150ms),重传! |
| 支付成功回调(seq=100,重传) |
|--------------------------------->|
| | 重复处理!
| | ❌ 重复扣款
解决方案:
// 应用层去重:使用幂等性设计
@Transactional
public void handlePaymentCallback(PaymentCallback callback) {
// 1. 检查是否已处理(使用唯一键)
String idempotencyKey = callback.getTransactionId();
if (paymentRepository.existsByTransactionId(idempotencyKey)) {
log.info("重复回调,忽略:{}", idempotencyKey);
return; // ✅ 幂等性保护
}
// 2. 处理支付
Order order = orderService.getById(callback.getOrderId());
order.setStatus(OrderStatus.PAID);
orderRepository.save(order);
// 3. 记录处理状态(防止重复)
PaymentRecord record = new PaymentRecord();
record.setTransactionId(idempotencyKey);
record.setProcessedAt(new Date());
paymentRepository.save(record);
}
关键点:
- TCP重传是协议层的行为,应用层无法避免
- 必须在应用层实现幂等性
- 使用唯一键(事务ID、请求ID)去重
重传优化建议
场景1:内网低延迟
# 特点:RTT < 1ms,丢包率极低
# 建议:降低RTO最小值,加快重传
net.ipv4.tcp_rto_min = 100 # 降到100ms(谨慎!)
net.ipv4.tcp_retries2 = 5 # 减少重传次数
场景2:跨地域高延迟
# 特点:RTT = 50-200ms,丢包率中等
# 建议:增大RTO最小值,启用SACK
net.ipv4.tcp_rto_min = 300 # 增大到300ms
net.ipv4.tcp_sack = 1 # 启用SACK
场景3:不稳定网络(移动网络)
# 特点:RTT波动大,丢包率高
# 建议:启用SACK,容忍丢包
net.ipv4.tcp_sack = 1 # 启用SACK
net.ipv4.tcp_reordering = 10 # 容忍乱序
总结
核心要点
重传的两种方式:
- 超时重传:RTO超时后重传,慢但可靠
- 快速重传:收到3个重复ACK后重传,快速
RTO计算:
- 基于SRTT(平滑RTT)和RTTVAR(RTT变化量)
- RTO = SRTT + 4 × RTTVAR
- 最小值200ms,最大值120秒
SACK选择性确认:
- 告诉发送方哪些数据已收到
- 减少不必要的重传
- 提高带宽利用率
Wireshark分析:
- 过滤器:
tcp.analysis.retransmission - 区分超时重传和快速重传
- 分析SACK选项
- 过滤器:
与下一篇的关联
本文讲解了TCP的重传机制(超时重传、快速重传、SACK)。至此,我们已经完成了TCP协议的核心机制。下一篇我们将学习UDP协议原理,理解:
- UDP的极简设计
- 为什么UDP是不可靠的?
- UDP的优势场景
- UDP vs TCP性能对比
思考题
- 为什么快速重传需要3个重复ACK,而不是1个或2个?
- RTO的最小值为什么是200ms?能设置得更小吗?
- SACK如何减少重传?举例说明
- 在什么场景下,应用层必须实现幂等性?
下一篇预告:《UDP协议原理:极简设计与高效传输》