3、高级恢复策略:选择性确认(SACK)、重复SACK(DSACK)、前向纠错(FEC)原理

好,咱们继续往下聊。上一章我们讲了基础的超时重传和快速重传,说白了就是「丢了就重发」。但实际网络环境远比这复杂——丢一个包就重传整个窗口?太浪费了。今天要聊的三个高级策略,就是来解决这个问题的。

我个人习惯把这三个技术分成两类:SACK/DSACK 属于「精确反馈」,告诉发送端到底丢了什么;FEC 属于「主动冗余」,提前塞点冗余数据,丢了也不用重传。两者思路完全不同,但目标一致——减少不必要的重传,提升吞吐。

核心观点:没有 SACK 的 TCP,就像蒙着眼睛开车——只知道撞了,不知道撞在哪。SACK 给了你一张「事故现场地图」。

3.1 选择性确认(SACK)—— 精准定位丢失数据

先回忆一下传统 TCP 的确认机制。接收端只能确认「连续收到的最大序号」,比如收到 1、2、3、5、6,它只能 ACK 3,告诉发送端「3 及之前都收到了」。发送端一看,4 没确认,那就重传 4 吧。但 5 和 6 其实也到了,却要等 4 到了才能被确认——这就是所谓的「头部阻塞」。

我在项目中遇到过这样一个场景:一条跨国链路上,RTT 高达 200ms,带宽 100Mbps。没有 SACK 时,只要丢一个包,整个窗口都要等重传,吞吐直接掉到 30Mbps。开了 SACK 之后,吞吐稳定在 85Mbps 以上。差距就这么大。

SACK 的工作原理

SACK 在 TCP 选项字段里塞入一组「非连续已收数据块」的边界。接收端收到乱序包后,除了发正常的 ACK,还会带上 SACK 选项,告诉发送端:

  • 我收到了 5-6,但中间缺了 4
  • 我收到了 8-10,但中间缺了 7

发送端一看就明白了——只重传缺失的 4 和 7,不用动其他数据。

特性 传统 ACK SACK
反馈粒度 只确认连续数据 可确认多个不连续块
重传范围 从丢失点开始全部重传 只重传真正丢失的段
适用场景 低丢包率、小窗口 高丢包率、大窗口、长肥网络
选项开销 无额外开销 每个 ACK 增加 8-40 字节

实战建议:现代 Linux 默认开启 SACK(net.ipv4.tcp_sack = 1)。但有些老旧嵌入式设备或专有协议栈可能不支持。我建议你在部署前用 tcpdump 抓包确认一下——看 SYN 包是否携带 SACK Permitted 选项。

3.2 重复 SACK(DSACK)—— 揪出「假丢包」

DSACK 是 SACK 的增强版。它解决了一个很实际的问题:如果重传的数据其实没丢,只是延迟到达了,怎么办?

举个例子。发送端发了包 1、2、3,接收端都收到了,但 ACK 在路上延迟了。发送端等超时后重传包 1,结果重传的包和原始包都到了接收端。接收端收到重复数据,如果不告诉发送端,发送端还以为自己重传对了——实际上这次重传是浪费的。

DSACK 的做法是:接收端在 SACK 选项里报告「我收到了重复的数据块」。发送端收到后就知道——哦,原来这次重传是多余的,网络其实没丢包,只是延迟了。

我曾经排查过一个诡异的问题:某台服务器的重传率高达 5%,但链路质量明明很好。开了 DSACK 抓包分析,发现 80% 的重传都是「假丢包」——数据其实到了,只是 ACK 被对端网卡合并延迟了。调整了中断合并参数后,重传率降到 0.5%。

注意:DSACK 不会触发任何新的重传行为,它只用于信息收集。发送端根据 DSACK 可以调整自己的 RTO 估算和拥塞窗口,但不会因为收到 DSACK 就立即改变发送策略。这个细节很多人搞混。

3.3 前向纠错(FEC)—— 把冗余塞进数据流

SACK 和 DSACK 都是「事后补救」——丢了再重传。FEC 的思路完全不同:发送数据时多塞一些冗余信息,接收端即使丢了一部分,也能自己算出来。

你想想看,这就像寄快递时多放一份说明书——就算包裹在路上被压坏了一角,收件人也能根据说明书自己修复。不需要让寄件人再发一次。

FEC 的两种常见实现

  1. XOR 奇偶校验(1+1 保护):每 N 个数据包生成 1 个冗余包。丢失任意 1 个包,都可以用其他 N 个包和冗余包 XOR 恢复。
  2. Reed-Solomon 编码(K+M 保护):K 个数据包生成 M 个冗余包。最多容忍丢失 M 个包,恢复能力更强,但计算开销也更大。
方案 冗余开销 恢复能力 计算复杂度 典型应用
XOR (1+1) 1/N 最多丢 1 个 极低 实时语音、视频
Reed-Solomon (K+M) M/K 最多丢 M 个 中等 卫星通信、存储系统
RaptorQ 可调 任意丢包模式 较高 流媒体、大规模分发

关键权衡:FEC 不是万能的。冗余多了,有效带宽就少了。丢包率 1% 时加 10% 冗余可能很划算;丢包率 0.1% 时加 10% 冗余就是浪费。我一般建议:丢包率 > 2% 时考虑 FEC,< 0.5% 时优先用 SACK。

3.4 三种策略的协同与选择

实际工程中,这三个策略不是互斥的,而是配合使用的。我画了一张图来说明它们的关系:

高级恢复策略协同关系图 发送端 接收端 数据包 + FEC 冗余 ACK + SACK/DSACK 选项 发送端决策逻辑 1. 正常发送数据 + FEC 冗余 2. 收到 SACK → 只重传丢失段 3. 收到 DSACK → 调整 RTO 估算 接收端处理流程 1. 尝试 FEC 恢复丢失数据 2. 生成 SACK 报告乱序块 3. 检测重复 → 生成 DSACK 协同效果 FEC 减少重传次数 SACK 提高重传效率 最终目标:最小化重传延迟

从图上可以看得很清楚:FEC 在发送端做「预防」,SACK/DSACK 在接收端做「精准反馈」。两者配合,才能达到最优效果。

实际选型建议

  • 实时音视频(WebRTC):丢包率 5-20%,延迟敏感。用 FEC(XOR 或 Reed-Solomon)+ NACK(类似 SACK 的反馈机制)。重传来不及,主要靠 FEC。
  • TCP 长肥网络(数据中心互联):丢包率 0.1-1%,带宽大。用 SACK + DSACK,FEC 基本不用。重传成本低,精确反馈更划算。
  • 卫星通信:丢包率 1-5%,RTT 高达 500ms+。必须用 FEC,重传一次要等 1 秒,谁也受不了。
  • 无线/移动网络:丢包率波动大(0.5-10%)。建议动态切换——低丢包时用 SACK,高丢包时开启 FEC。

我的经验:不要试图在所有场景都用同一套配置。我见过一个团队在丢包率 0.1% 的内网开了 20% 的 FEC 冗余,白白浪费了 20% 带宽。反过来,在卫星链路上只靠 SACK 重传,吞吐惨不忍睹。先测量,再配置——这是铁律。

3.5 小结

今天聊的三个策略,本质上是在回答同一个问题:数据丢了,怎么用最小的代价恢复?

  • SACK 告诉你「丢了哪几个」——精准重传,不浪费
  • DSACK 告诉你「哪些重传是多余的」——帮你优化参数
  • FEC 提前塞冗余——丢了也不用重传