4. 快速重传与恢复:从丢包到自愈的完整路径
快速重传和快速恢复,是 TCP 面对丢包时最优雅的一套组合拳。说白了,就是不用傻等超时,而是靠重复 ACK 提前感知丢包,然后一边补发数据一边稳住发送速率。我刚开始调优时,总觉得这俩机制是黑盒,直到有一次线上服务半夜抖动,抓包一看——快速重传触发了,但恢复阶段把拥塞窗口砍得太狠,业务延迟直接飙升。嗯,从那以后我就把这套逻辑彻底啃透了。
4.1 快速重传的触发条件
快速重传不依赖 RTO 超时,而是靠接收端发来的重复 ACK。接收端收到乱序报文时,会重复确认最后一个连续收到的序号。发送端收到 3 个或更多重复 ACK 时,就认为该报文已丢失。
为什么是 3 个?我个人的理解是:1 个重复可能是乱序,2 个可能是轻微抖动,3 个基本可以断定是丢了。RFC 5681 里也明确写了这个阈值。我在项目中遇到过一种情况——网络偶尔出现短暂乱序,重复 ACK 到了 2 个就停了,快速重传没触发,但 RTO 也没超时,结果数据卡了 200ms 才恢复。后来我们调整了接收端的重排缓冲区,才把这种假丢包压下去。
4.2 拥塞窗口调整(CWR)
快速重传触发后,发送端会立即重传丢失的报文,同时调整拥塞窗口。这个调整分两步:
- 记录阈值: 将慢启动阈值(ssthresh)设为当前拥塞窗口的一半,但至少为 2 个 MSS。
- 重置窗口: 将拥塞窗口(cwnd)设为 ssthresh + 3 × MSS(加 3 是因为已收到的重复 ACK 表明有 3 个报文已离开网络)。
你想想看,这个设计其实很聪明——既承认网络拥塞,又保留了一部分正在传输的数据量。我见过不少调优新手直接按教科书把 cwnd 砍到 ssthresh,结果恢复阶段吞吐量上不去。实际上,加 3 这个细节是快速恢复能快速的关键。
| 参数 | 调整前 | 调整后 |
|---|---|---|
| ssthresh | 原值(如 64 KB) | max(原 cwnd / 2, 2 × MSS) |
| cwnd | 原值(如 64 KB) | ssthresh + 3 × MSS |
4.3 快速恢复算法(NewReno)
快速恢复阶段的目标是:在补发丢失数据的同时,维持一个合理的发送速率。NewReno 是当前最主流的实现,它改进了早期 Reno 的一个大问题——如果一次丢包丢了多个报文,Reno 只能一个一个恢复,效率很低。
NewReno 的核心改进是:部分 ACK 处理。当发送端收到一个部分 ACK(确认了新数据但未确认所有丢失数据)时,NewReno 不会退出快速恢复,而是继续重传下一个丢失的报文。这样就能在一次快速恢复周期内处理多个丢包。
我记得有一次线上环境,业务高峰期出现了连续丢包,用 Reno 内核的机器恢复时间比 NewReno 长了将近 3 倍。后来我们统一升级了内核,把拥塞控制算法切到 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 的平滑降速,最后退出恢复阶段。我建议你把它当作排查丢包问题的路线图。
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 控制速率。
快速重传与恢复,说白了就是 TCP 的自我修复机制。理解它的触发条件和调整逻辑,你就能在丢包发生时快速定位问题,而不是盲目地调大缓冲区或降低发送速率。我个人的经验是,90% 的丢包问题都能通过优化快速恢复阶段来解决,剩下的 10% 才需要动 RTO 或重传次数这些更底层的参数。