引言

在前面的文章中,我们学习了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. 乱序:数据包可能因为路由不同而乱序到达,不一定丢失
  2. 第1个重复ACK:可能是乱序,不要着急重传
  3. 第2个重复ACK:可能还是乱序
  4. 第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    # 容忍乱序

总结

核心要点

  1. 重传的两种方式

    • 超时重传:RTO超时后重传,慢但可靠
    • 快速重传:收到3个重复ACK后重传,快速
  2. RTO计算

    • 基于SRTT(平滑RTT)和RTTVAR(RTT变化量)
    • RTO = SRTT + 4 × RTTVAR
    • 最小值200ms,最大值120秒
  3. SACK选择性确认

    • 告诉发送方哪些数据已收到
    • 减少不必要的重传
    • 提高带宽利用率
  4. Wireshark分析

    • 过滤器:tcp.analysis.retransmission
    • 区分超时重传和快速重传
    • 分析SACK选项

与下一篇的关联

本文讲解了TCP的重传机制(超时重传、快速重传、SACK)。至此,我们已经完成了TCP协议的核心机制。下一篇我们将学习UDP协议原理,理解:

  • UDP的极简设计
  • 为什么UDP是不可靠的?
  • UDP的优势场景
  • UDP vs TCP性能对比

思考题

  1. 为什么快速重传需要3个重复ACK,而不是1个或2个?
  2. RTO的最小值为什么是200ms?能设置得更小吗?
  3. SACK如何减少重传?举例说明
  4. 在什么场景下,应用层必须实现幂等性?

下一篇预告:《UDP协议原理:极简设计与高效传输》