4. 快速重传与恢复:从丢包到自愈的完整路径

快速重传和快速恢复,是 TCP 面对丢包时最优雅的一套组合拳。说白了,就是不用傻等超时,而是靠重复 ACK 提前感知丢包,然后一边补发数据一边稳住发送速率。我刚开始调优时,总觉得这俩机制是黑盒,直到有一次线上服务半夜抖动,抓包一看——快速重传触发了,但恢复阶段把拥塞窗口砍得太狠,业务延迟直接飙升。嗯,从那以后我就把这套逻辑彻底啃透了。

4.1 快速重传的触发条件

快速重传不依赖 RTO 超时,而是靠接收端发来的重复 ACK。接收端收到乱序报文时,会重复确认最后一个连续收到的序号。发送端收到 3 个或更多重复 ACK 时,就认为该报文已丢失。

触发条件: 收到 3 个重复的 ACK(即第 4 次确认同一个序号)。

为什么是 3 个?我个人的理解是:1 个重复可能是乱序,2 个可能是轻微抖动,3 个基本可以断定是丢了。RFC 5681 里也明确写了这个阈值。我在项目中遇到过一种情况——网络偶尔出现短暂乱序,重复 ACK 到了 2 个就停了,快速重传没触发,但 RTO 也没超时,结果数据卡了 200ms 才恢复。后来我们调整了接收端的重排缓冲区,才把这种假丢包压下去。

4.2 拥塞窗口调整(CWR)

快速重传触发后,发送端会立即重传丢失的报文,同时调整拥塞窗口。这个调整分两步:

  1. 记录阈值: 将慢启动阈值(ssthresh)设为当前拥塞窗口的一半,但至少为 2 个 MSS。
  2. 重置窗口: 将拥塞窗口(cwnd)设为 ssthresh + 3 × MSS(加 3 是因为已收到的重复 ACK 表明有 3 个报文已离开网络)。

你想想看,这个设计其实很聪明——既承认网络拥塞,又保留了一部分正在传输的数据量。我见过不少调优新手直接按教科书把 cwnd 砍到 ssthresh,结果恢复阶段吞吐量上不去。实际上,加 3 这个细节是快速恢复能快速的关键。

参数 调整前 调整后
ssthresh 原值(如 64 KB) max(原 cwnd / 2, 2 × MSS)
cwnd 原值(如 64 KB) ssthresh + 3 × MSS
我的习惯: 在调优时,我会用 ss -i 或 netstat -s 观察 cwnd 和 ssthresh 的变化。如果快速重传后 cwnd 恢复太慢,可以检查是否因为 ssthresh 被砍得太低。

4.3 快速恢复算法(NewReno)

快速恢复阶段的目标是:在补发丢失数据的同时,维持一个合理的发送速率。NewReno 是当前最主流的实现,它改进了早期 Reno 的一个大问题——如果一次丢包丢了多个报文,Reno 只能一个一个恢复,效率很低。

NewReno 的核心改进是:部分 ACK 处理。当发送端收到一个部分 ACK(确认了新数据但未确认所有丢失数据)时,NewReno 不会退出快速恢复,而是继续重传下一个丢失的报文。这样就能在一次快速恢复周期内处理多个丢包。

我记得有一次线上环境,业务高峰期出现了连续丢包,用 Reno 内核的机器恢复时间比 NewReno 长了将近 3 倍。后来我们统一升级了内核,把拥塞控制算法切到 NewReno,问题才解决。

我曾经踩过的坑: 部分 ACK 的处理逻辑在不同内核版本上有细微差异。有些老内核在收到部分 ACK 后会把 cwnd 减到 ssthresh,导致后续发送能力不足。建议在生产环境使用 Linux 4.9+ 内核,NewReno 的实现更稳定。

4.4 PRR(比例速率降低)

PRR(Proportional Rate Reduction)是 NewReno 的进阶版,由 RFC 6937 定义。它的目标更精细:在丢包恢复期间,让发送速率平滑地降低到新的目标值,而不是像 NewReno 那样靠 ACK 时钟被动调整。

PRR 的核心逻辑是:每收到一个 ACK,就根据当前 cwnd 和目标 cwnd 的比例,决定发送多少新数据。这样能避免两个极端:

  • 发送太多 → 加剧拥塞
  • 发送太少 → 链路利用率低

我个人的经验是,PRR 在高速网络(如 10Gbps+)上的优势特别明显。NewReno 在丢包后恢复时间较长,而 PRR 能更快地回到目标速率。Linux 内核从 3.2 开始默认使用 PRR,你可以在 /proc/sys/net/ipv4/tcp_use_prr 里确认是否启用。

特性 NewReno PRR
丢包恢复方式 依赖 ACK 时钟 按比例主动调整
多丢包处理 部分 ACK 逐个恢复 按比例平滑降低
高速网络表现 恢复较慢 恢复更快
内核支持 所有内核 Linux 3.2+

4.5 快速重传与恢复的完整流程

下面这张图把整个流程串起来了。从收到重复 ACK 开始,到快速重传、窗口调整、快速恢复,再到 PRR 的平滑降速,最后退出恢复阶段。我建议你把它当作排查丢包问题的路线图。

快速重传与恢复流程图 快速重传与恢复完整流程 收到 3 个重复 ACK 快速重传丢失报文 ssthresh = cwnd/2, cwnd = ssthresh + 3 进入快速恢复阶段 PRR 按比例平滑降低速率 收到完整 ACK,退出恢复 关键判断点 重复 ACK 数量 ≥ 3 部分 ACK 不退出恢复 PRR 按比例调整发送量 常见问题 假丢包:乱序导致误触发 恢复慢:ssthresh 砍太低 多丢包:NewReno 逐个恢复

4.6 实战排查要点

如果你怀疑快速重传和恢复出了问题,我建议按这个顺序排查:

  • 抓包确认: 用 tcpdump 或 wireshark 看重复 ACK 的数量和时序。如果重复 ACK 超过 3 个但没触发快速重传,可能是内核参数问题。
  • 检查内核参数: 确认 tcp_use_prr、tcp_sack、tcp_fack 是否启用。SACK 和 FACK 能帮助快速重传更精准地定位丢包。
  • 观察 cwnd 变化: 用 ss -i 或 tcpprobe 跟踪 cwnd 和 ssthresh 的实时值。如果 cwnd 恢复太慢,考虑调整初始 ssthresh 或启用 PRR。
  • 测试不同拥塞控制算法: 在测试环境切换 cubic、bbr、newreno,对比快速恢复阶段的表现。我遇到过 BBR 在丢包时完全不触发快速重传的情况,因为它靠 pacing 而不是 cwnd 控制速率。
一个小技巧: 在 Linux 上可以用 tc 模拟丢包,配合 tcpdump 验证快速重传和恢复的行为。命令:tc qdisc add dev eth0 root netem loss 1% delay 20ms。这样就能在可控环境下测试了。

快速重传与恢复,说白了就是 TCP 的自我修复机制。理解它的触发条件和调整逻辑,你就能在丢包发生时快速定位问题,而不是盲目地调大缓冲区或降低发送速率。我个人的经验是,90% 的丢包问题都能通过优化快速恢复阶段来解决,剩下的 10% 才需要动 RTO 或重传次数这些更底层的参数。