bigywater.tech
网络协议 · 47

快速重传:三个重复 ACK 的触发

为什么大文件传输里丢一两个包几乎没感觉,而小请求丢一个包就卡顿?这篇用抓包比较两种丢失:窗口里有后续数据时靠重复 ACK 快速重传,只有一个包时只能等定时器,时间差 4~7 倍。

42 ms
大传输里丢包后快速重传距首次发送的间隔(RTT 40 ms),3 次重传全是快速重传
189 ms
只发一个小包丢了时的首次重传间隔中位数(最小 181、最大 275 ms)
3 个重复 ACK
触发快速重传所需的重复 ACK 个数(RFC 5681)

重复 ACK

实测 · 本机执行输出
  传输 2 MiB 用了 0.38 秒;tcp_info(发送方套接字):发出 135 个包,重传 3 个(48996 字节)
  抓包里被重传过的段 3 个:第一次重传距首次发送的间隔小于 150 ms 的 3 个(快速重传的特征:约一个 RTT 40 ms 再加上收到 3 个重复 ACK 所需的时间),≥ 150 ms 的 0 个(超时重传的特征:RTO)
  快速重传的间隔:最小 42 / 中位 42 / 最大 42 ms

接收方遇到中间有洞的段,会立刻重发“我还在等第 X 个字节”的 ACK。数到 3 个就判定丢包,不等 RTO。

42 ms 对 189 ms

实测 · 本机执行输出
  段(相对序号 1045248):首次发送 +0 ms;之后到达的、确认号停在它的序号上的 ACK 共 5 个抓到记录(去掉近乎同时的重复约 1 个),时刻:[21] ms;重传发生在 +42 ms
  段(相对序号 1472064):首次发送 +0 ms;之后到达的、确认号停在它的序号上的 ACK 共 16 个抓到记录(去掉近乎同时的重复约 1 个),时刻:[21] ms;重传发生在 +42 ms
  段(相对序号 1521060):首次发送 +0 ms;之后到达的、确认号停在它的序号上的 ACK 共 0 个抓到记录(去掉近乎同时的重复约 0 个),时刻:[] ms;重传发生在 +42 ms
  说明:接收方每收到一个序号更大、但中间有洞的段,就立刻再发一次“我还在等第 X 个字节”的 ACK(不能等延迟 ACK)。发送方数到 3 个这样的重复 ACK 就重传;为什么是 3 个:网络里偶尔乱序 1~2 个包很常见,3 个意味着很可能是真的丢了(RFC 5681)

有后续数据时重传在约一个 RTT 之后发生;没有后续数据时要等定时器,慢 4~7 倍。

对设计的启示

实测 · 本机执行输出
  60 次单包收发里,重传了 17 个包,tcp_info 显示重传 24 个;首次重传的间隔最小 181 / 中位 189 / 最大 275 ms——都在 RTO 量级(≥ 200 ms),远大于一个 RTT(40 ms),因为没有后续数据产生重复 ACK
  所以快速重传只对“窗口里有后续数据”的丢失有效;请求 / 应答式的小消息丢了只能靠 RTO。

请求 / 应答式的小消息对丢包最敏感,因为没有后续数据产生重复 ACK;所以小消息的延迟尾巴主要来自定时器。

分步演示

快速重传
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64(Apple 芯片)/ Python 3.14.8 / tcpdump(libpcap)/ scapy 2.8 / dpkt / dnspython / h2 / aioquic / OpenSSL / curl 8.7.1;抓包在 lo0(环回)上做,少数实验用真实网卡 en0(这台机器上 en0 是 Wi-Fi,tcpdump 看到的是系统转成以太网格式的帧,所以文中的“以太网头”都指这个 14 字节的头)或访问公网(会在文章里注明);需要丢包 / 延迟 / 限速时,用一个只作用于 lo0 端口 9400-9499 的 dummynet 辅助脚本(netlab);所有服务端都是本机自己起的进程;时间戳取自 tcpdump 的 -tt,数字每次运行会有小幅抖动。全部在 lo0 上,丢包与延迟由 netlab 注入(单向 20 ms,RTT 约 40 ms);抓包里 dummynet 会让同一个包被抓到多份,按相隔 30 ms 去重分簇;第二节的“ACK 记录条数”包含这类重复,只用来说明确认号停在丢失段上,不是精确的重复 ACK 个数;重传次数很少(3 个),统计意义有限
    • 没有验证:SACK 如何把这类恢复做得更精确(见下一篇);尾部丢失探测(TLP)和 RACK 的具体触发条件;en0 真实网络上快速重传的比例;乱序造成的误快速重传(DSACK)
    京ICP备2026067502号-1京公网安备11010502063487号