土法炼钢兴趣小组的算法知识备份

【网络工程】TCP 可靠传输:序列号、确认与重传机制

文章导航

分类入口
network
标签入口
#tcp#reliability#retransmission#sack#rto

目录

TCP 可靠传输:序列号、确认与重传机制

TCP 的可靠传输不是一个单一特性——它是序列号、确认、重传定时器、选择性确认等多个机制协同工作的结果。这些机制在教科书中通常被一笔带过,但在生产环境中,每一个细节都可能成为性能瓶颈或故障根因。为什么重传率只有 0.1% 的连接延迟却翻倍了?为什么 SACK 被禁用后长距离传输吞吐量骤降?RTO 的计算为什么用了两个 RFC 才定稿?

本文从工程视角剖析 TCP 可靠传输的每一个环节——不是”怎么工作”,而是”为什么这样设计”以及”出问题时怎么诊断”。

一、序列号与确认的精确语义

序列号不是”包的编号”

TCP 的序列号(Sequence Number)是字节流的编号,不是包的编号。这个区别很重要:

# 假设发送 3000 字节的数据,MSS=1000
# 包 1: seq=1000, len=1000 → 包含字节 1000-1999
# 包 2: seq=2000, len=1000 → 包含字节 2000-2999
# 包 3: seq=3000, len=1000 → 包含字节 3000-3999

# 注意:seq 指的是这个包中第一个字节的编号
# len 是数据长度(不含 TCP 头)

确认号(ACK Number)的含义是”我期望收到的下一个字节的序列号”——这是一个累积确认:

# 接收方收到包 1(seq=1000, len=1000)
# 发送 ACK=2000,意思是"1999 及之前的字节我都收到了,下一个给我 2000"

# 如果包 2 丢失,包 3 先到达:
# 接收方仍然发送 ACK=2000——因为 2000-2999 还没收到
# 即使 3000-3999 已经到了,也不能确认
# 这就是"累积确认"的局限性——SACK 就是为了解决这个问题

用 tcpdump 观察序列号

# 抓取一次 HTTP 请求的完整 TCP 交互
tcpdump -i eth0 -nn -S 'host 10.0.0.2 and port 80' -c 30

# -S 选项显示绝对序列号(默认是相对序列号)
# 输出示例:
# 10:00:00.000 10.0.0.1.54321 > 10.0.0.2.80: S 3218452761:3218452761(0)
# 10:00:00.001 10.0.0.2.80 > 10.0.0.1.54321: S 1876543210:1876543210(0) ack 3218452762
# 10:00:00.001 10.0.0.1.54321 > 10.0.0.2.80: . ack 1876543211
# 10:00:00.002 10.0.0.1.54321 > 10.0.0.2.80: P 3218452762:3218452862(100) ack 1876543211
# 10:00:00.010 10.0.0.2.80 > 10.0.0.1.54321: . ack 3218452862
# 10:00:00.012 10.0.0.2.80 > 10.0.0.1.54321: P 1876543211:1876545211(2000) ack 3218452862
# ...

# 不带 -S 时显示相对序列号(更易读):
# 10:00:00.000 .54321 > .80: S 0:0(0)
# 10:00:00.001 .80 > .54321: S 0:0(0) ack 1
# 10:00:00.002 .54321 > .80: P 1:101(100) ack 1
# 10:00:00.010 .80 > .54321: . ack 101
# 10:00:00.012 .80 > .54321: P 1:2001(2000) ack 101

序列号回绕(Sequence Number Wrap)

TCP 序列号是 32 位无符号整数,范围 0 到 4,294,967,295。在高速网络中,序列号会回绕:

# 10 Gbps 链路:
# 4GB / 10 Gbps ≈ 3.4 秒就会回绕!
# 这意味着在高速网络中,可能同时存在两个具有相同序列号的包

# 解决方案:TCP 时间戳(Timestamps)
# RFC 7323 定义的 PAWS(Protection Against Wrapped Sequences)
# 用时间戳区分新旧包——即使序列号相同,时间戳不同就能区分

# 验证 TCP 时间戳是否启用
sysctl net.ipv4.tcp_timestamps
# 默认 1(启用)
# 不要关闭——在高速网络中关闭时间戳会导致严重问题

# 用 tcpdump 看时间戳
tcpdump -i eth0 -nn 'tcp' -c 5 -v 2>&1 | grep -o 'TS val [0-9]* ecr [0-9]*'
# TS val 1234567 ecr 7654321
# val = 发送方的时间戳
# ecr = 回显的对端时间戳(用于 RTT 计算)

二、RTO 计算:重传定时器的数学

问题:RTO 应该设多长?

如果 RTO 太短,正常的包还没来得及确认就触发重传——浪费带宽、加重拥塞。如果 RTO 太长,真正丢包时要等很久才重传——延迟暴增。RTO 必须根据网络的实际 RTT 动态调整。

Jacobson/Karels 算法(RFC 6298)

当前 Linux 使用的 RTO 计算算法:

# 每次收到 ACK 时,用新的 RTT 采样更新:

SRTT = (1 - α) × SRTT + α × RTT_sample     # 平滑 RTT
RTTVAR = (1 - β) × RTTVAR + β × |SRTT - RTT_sample|  # RTT 方差

RTO = SRTT + max(G, 4 × RTTVAR)

# 其中:
# α = 1/8 (EWMA 系数,新采样占 12.5%)
# β = 1/4
# G = 时钟粒度(Linux 上为 1ms)
# RTO 下限 = 200ms (Linux 默认)
# RTO 上限 = 120s  (Linux 默认)

# 直觉理解:
# - SRTT 追踪"平均 RTT 是多少"
# - RTTVAR 追踪"RTT 波动有多大"
# - RTO = 平均 RTT + 4倍波动 → 覆盖大部分正常情况

为什么是 4 倍方差?在正态分布假设下,均值 + 4σ 覆盖 99.997% 的情况。选 4 而不是 2 或 3 是因为 TCP 不能承受假阳性——错误的重传(称为”虚假重传”)会触发拥塞控制,大幅降低吞吐量。

# 用 ss -ti 观察实际的 RTO 和 RTT
ss -ti dst 10.0.0.2
# 输出中的关键字段:
# rto:204        ← 当前 RTO(ms)
# rtt:1.5/0.5    ← SRTT / RTTVAR(ms)
#
# 验证:RTO = 1.5 + max(1, 4 × 0.5) = 1.5 + 2.0 = 3.5ms
# 但 Linux 的 RTO 下限是 200ms,所以 RTO = max(3.5, 200) = 200ms

# RTO 下限 200ms 的争议:
# 对于数据中心内部(RTT < 1ms)的连接,200ms 的 RTO 下限太高了
# 一次丢包就需要等 200ms 才能重传
# Google 的 BBR 论文指出这是数据中心尾延迟的重要来源
# 一些数据中心操作系统将 RTO 下限降到 10ms 或更低

Karn 算法:不用重传包的 RTT 来更新

# 问题:如果一个包被重传了,收到的 ACK 到底是对原始包的还是对重传包的?
#
# 原始包 →  [在网络中]
# 重传包 →  [在网络中]
# ACK    ←  这个 ACK 是对哪个包的?
#
# 如果认为是对原始包的:RTT = 实际值 + 重传等待时间 → SRTT 被大幅高估
# 如果认为是对重传包的:RTT 可能被低估

# Karn 算法的解决方案:
# 1. 不使用重传包的 RTT 采样来更新 SRTT/RTTVAR
# 2. 每次重传时,RTO 翻倍(指数退避)
# 3. 只有收到非重传包的 ACK 时,才恢复正常的 RTO 计算

# 指数退避的时间序列:
# 第 1 次重传: RTO
# 第 2 次重传: 2 × RTO
# 第 3 次重传: 4 × RTO
# 第 4 次重传: 8 × RTO
# ...
# 直到 RTO 达到上限(120 秒)

TCP 时间戳的 RTT 优势

有了 TCP 时间戳,RTT 测量变得更精确:

# 没有时间戳时:
# 每个 RTT 窗口只能采样一次(Karn 算法的限制)
# 因为你只能在发送时记录时间,收到 ACK 时计算差值
# 但如果窗口内发了多个包,你不知道 ACK 对应哪个

# 有时间戳时:
# 每个 ACK 都回显了发送方的时间戳
# 可以精确计算每个 ACK 对应的 RTT
# 采样频率从"每 RTT 一次"变成"每 ACK 一次"

# 这就是为什么不应该关闭 tcp_timestamps

三、快速重传与重复 ACK

超时重传太慢了

超时重传(基于 RTO)的问题是——200ms 或更长的等待时间在很多场景下不可接受。快速重传提供了一种不依赖超时的重传触发方式。

# 快速重传的触发条件:收到 3 个重复 ACK

发送方                              接收方
  |  seq=1000, len=1000              |
  | --------------------------------> |
  |  seq=2000, len=1000  ← 丢失!    |
  | --------X                        |
  |  seq=3000, len=1000              |
  | --------------------------------> |  ← 收到乱序,发 dup ACK
  |            ACK=2000              |
  | <-------------------------------- |  ← 第 1 个重复 ACK
  |  seq=4000, len=1000              |
  | --------------------------------> |
  |            ACK=2000              |
  | <-------------------------------- |  ← 第 2 个重复 ACK
  |  seq=5000, len=1000              |
  | --------------------------------> |
  |            ACK=2000              |
  | <-------------------------------- |  ← 第 3 个重复 ACK → 触发快速重传!
  |                                   |
  |  seq=2000, len=1000  ← 重传      |
  | --------------------------------> |
  |            ACK=6000              |
  | <-------------------------------- |  ← 累积确认到 6000

为什么是 3 个重复 ACK 而不是 1 个?

网络中的包可能因为不同路径而乱序到达(reordering),这会产生重复 ACK。1-2 个重复 ACK 很可能只是乱序,不是丢包。3 个重复 ACK 大概率是丢包——但在高度乱序的网络中,这个阈值可能需要调整。

# Linux 的重复 ACK 阈值(可调)
sysctl net.ipv4.tcp_reordering
# 默认 3
# 收到 tcp_reordering 个重复 ACK 后触发快速重传
# 内核会根据实际观察到的乱序程度动态调整这个值

# 查看实际的乱序统计
nstat -az | grep -i reorder
# TcpExtTCPRenoReorder    ← Reno 算法检测到的乱序
# TcpExtTCPSACKReorder    ← SACK 检测到的乱序
# TcpExtTCPTSReorder      ← 时间戳检测到的乱序

快速恢复(Fast Recovery)

快速重传之后,TCP 不会回到慢启动——而是进入”快速恢复”:

# 快速重传 + 快速恢复的行为:
# 1. 收到 3 个重复 ACK
# 2. ssthresh = cwnd / 2
# 3. 重传丢失的包
# 4. cwnd = ssthresh + 3(3 个重复 ACK 说明有 3 个包已经离开网络)
# 5. 每收到一个额外的重复 ACK,cwnd += 1
# 6. 收到新数据的 ACK 时,cwnd = ssthresh → 进入拥塞避免阶段

# 这比超时重传好得多:
# - 超时重传后:cwnd 回到 1,慢启动重新开始 → 吞吐量暴跌
# - 快速恢复后:cwnd 减半,但不归零 → 吞吐量下降 50% 后逐步恢复

四、SACK:选择性确认的工程价值

累积确认的局限

没有 SACK 时,TCP 只能做累积确认。这意味着:

# 假设窗口中有 10 个包(seq 1000-10000),包 2 和包 5 丢失
# 接收方收到了 1, 3, 4, 6, 7, 8, 9, 10
# 但只能 ACK 到 2000(因为 2000-2999 没收到)

# 发送方知道"2000 之前的都收到了",但不知道 3, 4, 6, 7, 8, 9, 10 是否收到
# 重传包 2 后,收到 ACK=5000
# 现在知道"5000 之前都收到了",还得重传包 5
# 每次 RTT 只能发现一个丢失的包 → 恢复时间 = 丢包数 × RTT

# 如果丢了 5 个包,需要 5 个 RTT 才能全部恢复
# 在跨大洋链路(RTT=150ms)上,这意味着 750ms 的恢复时间

SACK 的工作方式

SACK 让接收方告诉发送方”哪些字节已经收到了”:

# SACK 的 TCP 选项格式
# 在 ACK 包中附带已收到的非连续字节范围

# 例如:ACK=2000, SACK=3000-5000, 6000-10000
# 含义:"2000 之前的都收到了,另外 3000-4999 和 6000-9999 也收到了"
# 发送方立即知道只需要重传 2000-2999 和 5000-5999

# 用 tcpdump 观察 SACK
tcpdump -i eth0 -nn 'tcp' -v 2>&1 | grep -i sack
# 输出示例:
# .54321 > .80: . ack 2000 win 65535 <nop,nop,sack 3000:5000 6000:10000>

SACK 的工程影响

# 对比实测数据(10% 随机丢包,RTT=100ms,BDP=1MB 的链路)

# 无 SACK(Go-Back-N 式恢复):
# 吞吐量: ~2 Mbps
# 恢复时间: 丢包数 × RTT

# 有 SACK:
# 吞吐量: ~8 Mbps
# 恢复时间: 约 1-2 RTT(不管丢了多少包)

# SACK 在以下场景价值最大:
# 1. 高 BDP(大带宽 × 大延迟)链路——窗口大,丢包恢复慢
# 2. 随机丢包环境——多个包同时丢失的概率更高
# 3. 突发丢包——路由器队列溢出时丢掉连续的多个包

# 检查 SACK 是否启用
sysctl net.ipv4.tcp_sack
# 默认 1(启用)—— 不要关闭

# 查看 SACK 相关统计
nstat -az | grep -i sack
# TcpExtTCPSackRecovery       ← 通过 SACK 触发的恢复次数
# TcpExtTCPSackShifted        ← SACK 块合并次数
# TcpExtTCPSackShiftFallback  ← SACK 合并失败次数
# TcpExtTCPSACKReorder        ← 通过 SACK 检测到的乱序

SACK 记分板(Scoreboard)

发送方维护一个”记分板”来追踪哪些字节已被 SACK 确认:

# 记分板示例(窗口内 10 个段):
# seg 1: ACKed       ← 被累积确认
# seg 2: LOST        ← 需要重传
# seg 3: SACKed      ← 被 SACK 确认
# seg 4: SACKed      ← 被 SACK 确认
# seg 5: LOST        ← 需要重传
# seg 6: SACKed      ← 被 SACK 确认
# seg 7: SACKed      ← 被 SACK 确认
# seg 8: SACKed      ← 被 SACK 确认
# seg 9: IN_FLIGHT   ← 已发送,未确认
# seg 10: IN_FLIGHT  ← 已发送,未确认

# 发送方立即知道需要重传 seg 2 和 seg 5
# 不需要等多个 RTT 逐步发现

# 用 ss -ti 查看 SACK 状态
ss -ti dst 10.0.0.2
# sack 相关字段:
# sacked:5     ← 被 SACK 确认的段数
# lost:2       ← 被标记为丢失的段数
# retrans:0/2  ← 当前重传中/总共重传的段数

五、DSACK:重复检测的利器

DSACK 的工程含义

DSACK(Duplicate SACK,RFC 2883)用 SACK 的格式告诉发送方”哪些数据我收到了重复的”。这看起来是小事,但工程价值巨大:

# DSACK 的使用场景

# 场景 1:检测虚假重传
# 发送方因为 RTO 误判而重传了已经被收到的包
# 接收方收到重复的数据后,用 DSACK 回报
# 发送方知道"这次重传是多余的"→ 可以调整 RTO 计算
# Linux 会自动 undo 拥塞窗口的减小(避免误判导致的性能下降)

# 场景 2:检测包乱序
# 如果包只是乱序到达(不是真正丢失),快速重传触发后
# 原始包最终到达,产生重复
# DSACK 帮助发送方区分"真正丢包"和"网络乱序"

# 场景 3:检测 ACK 丢失
# 数据包到达了,ACK 丢失了
# 发送方超时重传,接收方收到重复数据
# DSACK 报告重复 → 发送方知道"数据到了,是 ACK 丢了"

# 启用 DSACK
sysctl net.ipv4.tcp_dsack
# 默认 1(启用)

# DSACK 统计
nstat -az | grep -i dsack
# TcpExtTCPDSACKOldSent      ← 发送的 DSACK(对旧数据的重复)
# TcpExtTCPDSACKOfoSent      ← 发送的 DSACK(对乱序数据的重复)
# TcpExtTCPDSACKRecv         ← 收到的 DSACK
# TcpExtTCPDSACKOfoRecv      ← 收到的乱序 DSACK
# TcpExtTCPDSACKIgnoredOld   ← 被忽略的 DSACK(太旧)
# TcpExtTCPDSACKIgnoredNoUndo ← 被忽略的 DSACK(无法 undo)

DSACK 在虚假重传检测中的应用

# 虚假重传的检测与恢复(F-RTO,RFC 5682)

# Linux 实现了 F-RTO 算法:
# 1. RTO 超时后,重传第一个未确认的段
# 2. 如果收到的 ACK 推进了窗口(确认了新数据)
#    → 可能是虚假超时,尝试恢复 cwnd
# 3. 如果收到的 ACK 没有推进窗口
#    → 确实是真正的丢包,继续正常的超时恢复

# F-RTO 统计
nstat -az | grep -i frto
# TcpExtTCPSpuriousRTOs    ← 检测到的虚假超时次数
# TcpExtTCPLossUndo        ← 因为检测到虚假超时而撤销的拥塞控制调整

# 如果 TcpExtTCPSpuriousRTOs 数量很高,说明 RTO 设得太短
# 或者网络延迟抖动很大

六、重传对延迟的放大效应

尾延迟(Tail Latency)的元凶

重传是 TCP 尾延迟的头号元凶。即使丢包率只有 0.01%,对 P99 延迟的影响也可能是数量级的:

# 延迟放大分析

# 正常传输一个 HTTP 请求:
# DNS: 5ms + TCP 握手: 10ms + TLS 握手: 15ms + 数据传输: 5ms = 35ms

# 如果数据传输阶段丢包:
# 快速重传(3 dup ACK): 额外 1 RTT ≈ 10ms → 总计 45ms
# RTO 超时重传: 额外 200ms(RTO 下限)→ 总计 235ms!

# P99 延迟计算:
# 假设丢包率 0.1%,每个请求传输 3 个 TCP 段
# 至少一个段丢包的概率 = 1 - (1-0.001)^3 ≈ 0.3%
# 也就是每 333 个请求中有 1 个遇到重传
# P99 ≈ 第 99 百分位,所以 P99 可能还正常
# 但 P99.9 ≈ 235ms(RTO 超时的那些请求)

# 这就是为什么:
# 1. Google 要把 RTO 下限降到更低的值
# 2. QUIC 没有 200ms 的 RTO 下限
# 3. 高频交易系统几乎不能容忍任何重传

延迟放大的量化分析

不同重传机制的延迟开销差异巨大:

重传机制 额外延迟(RTT=1ms) 额外延迟(RTT=100ms) 触发条件
快速重传 ~1ms ~100ms 3 个重复 ACK
TLP + 快速重传 ~3ms ~300ms 窗口尾部丢包
RTO 超时(第 1 次) 200ms 200ms+ 无法触发快速重传
RTO 超时(第 2 次) 600ms 600ms+ 第 1 次重传也丢了
RTO 超时(第 3 次) 1400ms 1400ms+ 连续丢包

数据中心内部(RTT=1ms)单次 RTO 超时就会导致 200 倍的延迟放大——这是 P99 尾延迟的主要来源。

重传率的诊断

# === 全局重传统计 ===
nstat -az | grep -E 'Retrans|Timeout'
# TcpRetransSegs           12345   ← 重传的段总数
# TcpExtTCPLostRetransmit  100     ← 重传包也丢失的次数
# TcpTimeouts              500     ← RTO 超时次数
# TcpExtTCPFastRetrans     800     ← 快速重传次数
# TcpExtTCPSlowStartRetrans 50     ← 慢启动阶段的重传

# 重传率 = TcpRetransSegs / TcpOutSegs
# < 0.01% 正常
# 0.01% - 0.1% 需要关注
# > 0.1% 需要排查

# === 单个连接的重传统计 ===
ss -ti dst 10.0.0.2
# retrans:0/5    ← 当前重传中的段数 / 总共重传过的段数
# lost:0         ← 当前被标记为丢失的段数
# rto:204        ← 当前 RTO(ms)

# === 持续监控重传 ===
# 方法 1:watch nstat
watch -n 1 'nstat -az | grep Retrans'

# 方法 2:用 tcpdump 抓重传包
tcpdump -i eth0 -nn 'tcp[13] & 0x04 != 0' -c 10
# 上面这个过滤器只抓 RST 包,不准确

# 更好的方法:用 tshark 的 TCP 分析功能
tshark -i eth0 -Y 'tcp.analysis.retransmission' -T fields \
    -e ip.src -e ip.dst -e tcp.srcport -e tcp.dstport -e tcp.stream
# 只显示被 Wireshark 识别为重传的包

# 方法 3:用 bcc 工具
# tcpretrans 实时显示每一次 TCP 重传
tcpretrans
# TIME     PID    LADDR:LPORT    RADDR:RPORT    STATE
# 10:00:01 1234   10.0.0.1:54321 10.0.0.2:443   ESTABLISHED
# 输出每次重传的时间、进程、地址和连接状态

重传风暴与连锁反应

重传本身会加重拥塞,形成恶性循环:

# 重传风暴的形成过程:
# 1. 网络拥塞 → 丢包
# 2. 丢包 → 触发重传
# 3. 重传 → 注入更多数据到已经拥塞的网络
# 4. 更多拥塞 → 更多丢包
# 5. 更多丢包 → 更多重传
# → 恶性循环

# 拥塞控制的作用就是打断这个循环:
# 检测到丢包 → 减小发送速率(cwnd 减半或归零)
# 但如果是虚假重传 → 不应该减速 → DSACK 和 F-RTO 的价值

# 诊断连锁反应:
# 观察一段时间内的重传率变化
sar -n TCP 1 5
# 01:00:01  active/s  passive/s  iseg/s  oseg/s
# 01:00:02  50.00     100.00     5000    5500    ← 正常
# 01:00:03  45.00     95.00      4800    6200    ← 重传导致发送增加
# 01:00:04  30.00     60.00      3000    4500    ← 拥塞控制开始生效
# 01:00:05  40.00     80.00      4000    4200    ← 逐步恢复

七、Early Retransmit 与 TLP

Early Retransmit(RFC 5827)

当窗口中只剩少量未确认的包时,可能无法凑齐 3 个重复 ACK 来触发快速重传。这时只能等 RTO——很慢。

# 假设窗口中只有 2 个未确认的包,第 1 个丢了
# 包 2 到达后接收方发 dup ACK
# 但只有 1 个 dup ACK——不够触发快速重传
# 只能等 RTO 超时(200ms+)

# Early Retransmit 的解法:
# 如果未确认的段数 < 4(不够凑 3 个 dup ACK)
# 且已经收到了 (未确认段数 - 1) 个 dup ACK
# → 降低快速重传的阈值,提前触发

sysctl net.ipv4.tcp_early_retrans
# 默认 3
# 0 = 关闭
# 1 = 启用 Early Retransmit
# 2 = 启用 Early Retransmit(仅对慢启动)
# 3 = 启用 Early Retransmit + TLP
# 4 = 启用 TLP 但不启用 Early Retransmit

TLP(Tail Loss Probe)

TLP 解决的是”窗口尾部丢包”问题——如果发送的最后几个包丢了,没有后续的包来触发重复 ACK,只能等 RTO:

# TLP 的工作方式:
# 发送完一个突发数据后,如果在 2×SRTT 内没有收到 ACK
# 发送一个"探测包"(重传最后一个段或发送新数据)
# 这个探测包如果被收到,会触发 SACK 反馈
# 发送方就能通过 SACK 知道哪些包丢了 → 快速重传

# TLP vs RTO:
# TLP 在 2×SRTT 后触发(通常几毫秒到几十毫秒)
# RTO 在 200ms+ 后触发
# TLP 可以将尾延迟从 200ms+ 降到几个 RTT

# TLP 的效果(Google 的论文数据):
# 减少了约 15% 的 RTO 超时事件
# P99 延迟显著改善

# TLP 相关统计
nstat -az | grep TLP
# TcpExtTCPLossProbes      ← 发送的 TLP 探测包数量
# TcpExtTCPLossProbeRecovery ← 通过 TLP 成功恢复的次数

八、RACK:基于时间的丢包检测

RACK(Recent ACKnowledgment,RFC 8985)是 Linux 5.x 中 TCP 丢包检测的默认算法。它用时间而不是重复 ACK 数量来判断丢包。

# 传统方法(基于重复 ACK 计数)的问题:
# 1. 需要凑齐 3 个 dup ACK → 窗口小时凑不齐
# 2. 不考虑时间信息 → 对乱序敏感
# 3. 依赖 FACK(Forward ACK)→ 某些情况下不准确

# RACK 的核心思想:
# 如果一个包在"最近被 ACK 的包发送之后的合理时间"内仍未被 ACK,
# 就认为它丢失了

# RACK 的判定规则:
# 包 P 被认为丢失,如果:
# P.sent_time < most_recent_acked.sent_time - reordering_window
#
# 即:如果一个包的发送时间比最近被确认的包的发送时间
# 早了超过 reordering_window,就认为它丢失了

# 直觉:如果我 10ms 前发的包还没被确认,但 5ms 前发的包已经被确认了,
# 那 10ms 前的包大概率丢了

# RACK 的优势:
# 1. 不需要凑 dup ACK → 窗口再小也能检测
# 2. 基于时间 → 对乱序更鲁棒
# 3. 与 TLP 配合 → 尾延迟更低

# RACK 在 Linux 中默认启用,不需要手动配置
# 可以通过 ss -ti 看到 RACK 相关信息
ss -ti dst 10.0.0.2 | grep -o 'delivery_rate [^ ]*'
# RACK 使用 delivery_rate 来估计丢包

TCP 丢包检测机制演进对比

机制 触发条件 检测延迟 优势 局限性 RFC
RTO 超时 定时器超时 200ms+(RTO 下限) 最后防线,可靠 延迟极高 6298
快速重传 3 个重复 ACK ~1 RTT 比 RTO 快得多 窗口小时凑不齐 5681
Early Retransmit <4 段时降低阈值 ~1 RTT 小窗口也能触发 乱序时误判率高 5827
TLP 2×SRTT 无 ACK ~2 RTT 解决尾部丢包 增加探测包开销 8985
RACK 时间+SACK ~1 RTT 鲁棒、通用 需要 SACK 支持 8985
flowchart LR
    A["包发出"] --> B{"在 2×SRTT 内<br>收到 ACK?"}
    B -- "是" --> C["正常"]
    B -- "否" --> D["TLP 探测"]
    D --> E{"收到 SACK<br>反馈?"}
    E -- "是" --> F["RACK 判定<br>丢失的段"]
    F --> G["快速重传"]
    E -- "否" --> H{"收到 3 个<br>dup ACK?"}
    H -- "是" --> G
    H -- "否" --> I["等待 RTO<br>超时"]
    I --> J["超时重传<br>+ 回到慢启动"]

九、重传相关的内核参数完整指南

# === 重传次数控制 ===

sysctl net.ipv4.tcp_retries1
# 默认 3
# 发送方在重传 3 次后通知 IP 层"有问题"
# IP 层可能刷新路由表、选择新的下一跳
# 不会断开连接

sysctl net.ipv4.tcp_retries2
# 默认 15
# 超过 15 次重传后放弃,发 RST 断开连接
# 总等待时间约 13-30 分钟(取决于 RTO 的退避)

# 对于需要快速发现故障的场景,降低这个值
sysctl -w net.ipv4.tcp_retries2=5
# 约 25-30 秒后放弃

sysctl net.ipv4.tcp_orphan_retries
# 默认 0(实际上是 8)
# 对于"孤儿连接"(应用已 close,但还在传输数据)的重传次数
# FIN_WAIT 和 LAST_ACK 状态的连接

# === SACK 相关 ===
sysctl net.ipv4.tcp_sack        # SACK 启用(默认 1)
sysctl net.ipv4.tcp_dsack       # DSACK 启用(默认 1)
sysctl net.ipv4.tcp_fack        # FACK 启用(已在新内核中移除)

# === 重传检测算法 ===
sysctl net.ipv4.tcp_early_retrans  # Early Retransmit + TLP(默认 3)
sysctl net.ipv4.tcp_reordering     # 乱序容忍度(默认 3,内核自动调)

# === 时间戳 ===
sysctl net.ipv4.tcp_timestamps  # TCP 时间戳(默认 1)
# 必须开启——用于 PAWS、精确 RTT 测量、DSACK

参数调优建议

场景 调优建议 原因
数据中心内部 降低 tcp_retries2=5 快速发现故障,依赖上层重试
跨洲际链路 保持默认或增大 RTT 大,需要更多重试时间
高丢包无线网络 增大 tcp_reordering=10 减少虚假重传
高频交易 最小化 RTO(需要修改内核) 尾延迟极其敏感
移动网络 保持 SACK/DSACK 开启 丢包频繁,SACK 价值大

十、实战案例:重传问题诊断

案例:数据库连接间歇性超时

症状:应用连接 MySQL 时偶尔超时(约 5 秒),但 ping 延迟正常。

# 1. 确认重传统计
nstat -az | grep -E 'Retrans|Timeout'
# TcpRetransSegs    5234     ← 重传次数在增长
# TcpTimeouts       234      ← RTO 超时次数

# 2. 抓包分析
tcpdump -i eth0 -nn 'host MySQL_IP and port 3306' -w /tmp/mysql.pcap -c 5000
# 用 Wireshark 打开,过滤:
# tcp.analysis.retransmission || tcp.analysis.fast_retransmission

# 3. 发现模式:
# 查询结果较大时(>几 KB),最后几个包概率性丢失
# 因为是窗口尾部丢包,无法触发快速重传
# 只能等 RTO 超时(200ms × 指数退避 ≈ 数秒)

# 4. 检查 TLP 是否生效
nstat -az | grep TLP
# TcpExtTCPLossProbes 0    ← TLP 没有工作?

# 5. 检查内核版本
uname -r
# 3.10.xxx ← 太老了,不支持 TLP

# 6. 排查丢包位置
# 在 MySQL 服务端同时抓包,对比两端的包
# 发现包在服务端发出了,但客户端没收到
# → 中间网络设备(交换机/路由器)队列溢出

# 7. 解决方案:
# (a) 升级内核到 4.x+(支持 TLP 和 RACK)
# (b) 检查中间交换机的队列配置
# (c) 应用层设置更短的查询超时 + 自动重试

案例:长距离传输吞吐量低

症状:北京到纽约的文件传输,带宽 100Mbps 的链路只跑到 5Mbps。

# 1. 测量路径 RTT
ping -c 20 纽约服务器IP
# rtt avg = 200ms

# 2. 计算理论最大吞吐量(BDP)
# BDP = 带宽 × RTT = 100Mbps × 0.2s = 20Mbit = 2.5MB
# 需要至少 2.5MB 的 TCP 窗口才能填满带宽

# 3. 检查接收窗口
ss -ti dst 纽约IP
# rcv_space:29200  ← 约 29KB!远远不够
# wscale:7,7       ← 窗口缩放已启用

# 4. 检查缓冲区设置
sysctl net.ipv4.tcp_rmem
# 4096 131072 6291456  → 最大 6MB
sysctl net.core.rmem_max
# 212992 → 约 200KB!← 这是瓶颈!

# 5. 修复
sysctl -w net.core.rmem_max=16777216
sysctl -w net.core.wmem_max=16777216
sysctl -w net.ipv4.tcp_rmem="4096 131072 16777216"
sysctl -w net.ipv4.tcp_wmem="4096 65536 16777216"

# 6. 验证
# 重新建立连接后测试
iperf3 -c 纽约IP -t 30
# 吞吐量从 5Mbps 提升到 80Mbps+

# 7. 同时检查 SACK 是否生效
# 在 200ms RTT 的链路上,如果 SACK 被禁用
# 每次丢包恢复需要多个 RTT → 吞吐量大幅下降
sysctl net.ipv4.tcp_sack
# 确保为 1

十一、结论

TCP 的可靠传输机制经过了 40 多年的演进,从最初简单的超时重传发展到现在的 SACK + RACK + TLP 的组合。每一次改进都是在解决实际工程中遇到的性能问题。

核心理解:

  1. RTO 是最后的手段,不是首选。 快速重传(3 dup ACK)、TLP(2×SRTT 探测)和 RACK(基于时间的检测)都是为了避免等待 RTO 的 200ms+。如果你的重传统计中 TcpTimeouts 远大于 TcpExtTCPFastRetrans,说明有问题——可能是窗口太小、网络乱序严重或内核版本太旧。

  2. SACK 不是可选的——它是高性能传输的基础。 在任何丢包率 >0.01% 且 BDP >100KB 的链路上,禁用 SACK 的性能惩罚是数量级的。确保 tcp_sack=1tcp_dsack=1

  3. 重传率的解读需要上下文。 0.1% 的重传率在数据中心内网是严重问题(应该接近 0),在跨国链路上可能正常。关键不是绝对值,而是趋势——突然上升才需要排查。

  4. 尾延迟主要来自重传。 如果你的 P99 延迟是 P50 的 10 倍以上,大概率是 RTO 超时导致的。升级内核(获取 TLP/RACK 支持)和调优 RTO 相关参数是最有效的改善手段。

  5. 不要关闭 TCP 时间戳。 tcp_timestamps=1 是精确 RTT 测量、PAWS 保护、DSACK 虚假重传检测的基础。关闭时间戳在高速网络中可能导致序列号回绕带来的数据损坏。

我的建议是:不要背参数,要理解机制。知道”RTO 太长导致尾延迟高”比记住”tcp_retries2 默认是 15”重要得多。当你理解了序列号、确认、SACK、RTO 这些机制的协同工作方式,诊断重传问题就不再是”改参数碰运气”,而是”从现象推导根因”。

快速诊断清单

# 1. 全局重传率
nstat -az | grep -E 'TcpRetransSegs|TcpOutSegs'

# 2. RTO vs 快速重传的比例
nstat -az | grep -E 'TcpTimeouts|TCPFastRetrans'

# 3. SACK 是否生效
sysctl net.ipv4.tcp_sack && nstat -az | grep SACKRecovery

# 4. 虚假重传是否频繁
nstat -az | grep SpuriousRTOs

# 5. 特定连接的重传详情
ss -ti dst <目标IP> | grep -E 'retrans|lost|rto'

上一篇:TCP 连接管理:三次握手、四次挥手与状态机

下一篇:TCP 流量控制:滑动窗口的工程细节

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-04-22 · network

网络工程索引

汇总本站网络工程系列文章,覆盖分层模型、以太网、IP、TCP、DNS、TLS、HTTP/2/3、CDN、BGP 与故障诊断。

2025-07-30 · network

【网络工程】内核网络参数调优:sysctl 全景与实战

Linux 内核网络参数是系统网络性能的基础旋钮。本文从 /proc/sys/net/ 的参数体系出发,系统讲解收发缓冲区自动调优、TCP Backlog 队列、conntrack 连接追踪表、SYN Flood 防护参数、TIME_WAIT 管理,以及参数调优的系统化方法论——先基准、再调整、后验证。


By .