在快递公司当调度员的老唐,每天要给客户阿诚发几百件包裹。每发出几件,阿诚都会回一张“签收回执”;可要是某件包裹半路丢了,阿诚那边就会一直缺货。老唐怎么知道包裹丢了?等多久才能补发?补发之后发货节奏怎么调整?这套“包裹救援”的打法,跟 TCP 协议在网络上保障数据可靠到达的逻辑一模一样。本文要讲的 TCP 重传与快速恢复,就是老唐面对丢包时的整套应急预案:等多久算丢(超时重传)、如何提前发现丢了(快速重传)、补发后怎样调整发货节奏(快速恢复)、以及如何用一张“缺货清单”精准补救(SACK)。把这一套吃透,你就掌握了 TCP 可靠性传输的核心心法。
理解 TCP 的重传与恢复机制,是排查网络性能问题、优化传输吞吐量的基本功——面试必考、调优必备。
可视化演示
交互式展示TCP超时重传与快速重传/恢复两条可靠性路径:超时模式下RTO定时器超触发重传并cwnd归1,快速模式下3个冗余ACK触发快重传并cwnd减半,切换模式对比窗口调整策略。
本节配有交互动效(进入视口后自动加载,约 3KB)。
超时重传机制 发送方为每个报文段启动RTO定时器 超时未收到ACK则重传,cwnd降为1进入慢启动 RTO = SRTT + 4\u00d7RTTVAR RTO由平滑RTT动态估计
超时重传机制
老唐给阿诚发包裹后,会默默在心里掐一个秒表:正常情况下一来一回大概需要多久?如果秒表都走完一圈了还没收到回执,老唐就会断定“这单肯定丢了”,于是重新发一份。这个“秒表走一圈”的等待上限,在 TCP 里就是重传超时时间(RTO,Retransmission Timeout),而那只“秒表”则是重传计时器。它是 TCP 可靠性的最后一道保险——不管网络状况多复杂,只要超时了,就一定重传。
RTO 的值不能拍脑袋定:定太短,网络稍微慢一点就误判为丢包,造成大量重复发送;定太长,真的丢了包要干等很久,吞吐量直线下降。所以 TCP 用一套平滑算法动态估计 RTT(报文往返时间),再叠加一个安全余量,得出 RTO。
超时之后,TCP 还会采取指数退避策略:第一次超时重传后,如果再次超时,RTO 直接翻倍(2 倍、4 倍、8 倍……)。这就像老唐发现补发的包裹又丢了,会怀疑“是不是路况全面恶化了”,于是下次再多等一会儿才下结论,避免在瘫痪的网络上疯狂重发。与此同时,超时重传还意味着网络可能严重拥塞,拥塞窗口会直接降为 1 个 MSS,相当于老唐决定“先只发一件试试水”。
- SRTT(平滑往返时间)对历史 RTT 样本做加权平均得到的稳定估值,相当于老唐统计过去一百趟快递的平均耗时。
- RTTVAR(往返时间偏差)衡量 RTT 的波动大小,相当于“这一趟比平均快还是慢、平均差多少”。
- 4 倍偏差做余量把波动幅度放大 4 倍加在均值上,留足安全余量,避免网络抖动引发误判。
快速重传机制
超时重传虽然可靠,但代价是“干等”。老唐很快发现了一个更聪明的办法:如果阿诚收到包裹 1、2、4、5,发现 3 丢了,他回执时不会干等 3,而是继续确认“最后收到的连续包裹是 2”,然后每次再收到新包裹都重复发一张“我还在等 3 号”的回执。连续三张相同的“等 3 号”回执飘回老唐手上时,老唐几乎可以断定 3 号真的丢了——于是不等秒表走完,立刻补发 3 号。
这就是冗余 ACK(Duplicate ACK) 的原理:接收方每收到一个乱序到达的报文段,就重发一次对“期望收到的下一个序号”的确认。当发送方收到 3 个相同的冗余 ACK 时,触发快速重传(Fast Retransmit)——它最大的价值是提前感知丢包,不用等 RTO 计时器到期。
快速重传的精髓在于:用对方的“催促声”代替自己的“秒表”。Timer 还没响,但阿诚已经喊了三遍缺货,老唐还等什么?马上补发!这条路径比超时重传快得多,因此成为 TCP 应对单包丢失的首选救援方案。
- 冗余 ACK 的产生接收方收到比期望序号更大的报文段时,不能确认这个乱序段,只能重复确认“最后一段连续数据”。
- 三次重复的意义收到 1 个冗余 ACK 可能只是包乱序(后发的先到),但连续 3 个说明后续报文都到了、唯独缺这一个,丢包概率极高。
- 立即重传触发条件满足后发送方马上重发丢失段,不再等待 RTO,显著缩短了丢包恢复时间。
快速恢复机制
补发包裹之后,老唐面临一个抉择:发货节奏要不要调整?如果调整为“一件一件发”(回到慢启动),虽然稳妥但效率太低;如果完全不停,又怕网络真的堵了。快速重传给出的答案是:将发货上限砍一半,但保持连续发货,绝不停摆——这就是快速恢复(Fast Recovery)。
快速恢复的核心逻辑:3 个冗余 ACK 不仅说明丢了 1 个包,同时还说明有 3 个报文段已经成功到达对端。既然网络还能“消化”这些数据,说明拥塞程度并没有严重到需要从头再来。于是 TCP 将拥塞窗口从“减半后的阈值”再往上加 3 个 MSS,相当于承认那 3 个已成功离网的报文段占用的容量。
对比一下:超时重传后 cwnd 归 1,等于老唐“只骑一辆三轮车送货”;而快速恢复后 cwnd 只降到原来一半附近,等于老唐“把卡车换成小货车,但照样跑”。这就是“快恢复”里“快”字的含义——恢复速度快,绝不推倒重来。
- 更新 ssthresh先把慢启动阈值设为当前窗口的一半,并保证至少 2 个 MSS。
- 新 cwnd 等于阈值加 3在减半后的基础上加回 3 个 MSS,因为这 3 个段已被确认离开网络。
- 进入拥塞避免此后每收到一个冗余 ACK,cwnd 临时再增加 1 个 MSS,直到重传的包被确认,再回落到 ssthresh 继续线性增长——整个过程不会回到慢启动。
拥塞窗口调控
老唐的仓库门口挂着一块白板,上面写着“当前最多同时在路上的包裹数”,这就是 TCP 的拥塞窗口(cwnd,Congestion Window)。它决定了发送方一次能往网络里“塞”多少数据而不等确认。窗口太小浪费带宽,窗口太大会把网络堵死。TCP 用一套分阶段的调控策略,让窗口大小既激进又稳妥。
调控分两个阶段,分水岭是慢启动阈值 ssthresh:
慢启动的“快”和拥塞避免的“稳”,配合 ssthresh 的自动降档,构成了 TCP 拥塞控制的骨架。老唐就是靠着白板上这个动态数字,在网络这个“路况未知”的环境里找到了“既快又不堵”的发货节奏。
- 慢启动阶段cwnd 从 1 个 MSS 开始,每收到一个 ACK 就翻倍增长,对应老唐“这趟跑通了,下趟发两倍的货”。
- 到达阈值后转入拥塞避免当 cwnd ≥ ssthresh,增速降为每轮 +1,避免线性拥堵,对应老唐“量大了,每次只多加一辆车”。
- 丢包触发阈值更新无论超时还是快重传,ssthresh 都会被降到当前 cwnd 的一半(有下限 2 MSS),作为下一轮“激进转谨慎”的提前刹车点。
重传事件公式
前面几节分别讲了不同场景的窗口调整规则,这一节把它们浓缩成一张“公式速查表”。当丢包事件发生时,TCP 会先统一更新 ssthresh,再根据事件类型决定 cwnd 的取值。这就像一个标准化的应急预案:不管遇到什么事故,老唐先做同一件事(把发货上限砍一半),然后再按事故等级决定是“只发一件”还是“降到半速继续跑”。
这套公式的巧妙之处在于用同一个动作(减半阈值)衔接两种不同程度的响应。超时是“红色警报”——全面收手重新试探;快重传是“黄色预警”——降速但不停工。老唐根据警报颜色的不同,决定是骑三轮车还是开小货车,但不管哪种情况,都会把“白板上的目标上限”先减半再说。
- 统一更新阈值无论哪种重传,先把 ssthresh 降到旧窗口的一半,并设下限 2 个 MSS,防止阈值小到没法用。
- 超时分支超时意味着网络可能严重拥塞甚至断路,cwnd 直接置为 1 个 MSS,从头开始慢启动试探。
- 快恢复分支冗余 ACK 证明链路仍能通行,cwnd 只需降到减半后的阈值再加 3,然后继续拥塞避免。
RTO平滑估计
老唐的手账本里记录着每一趟快递的往返耗时。但他不会拿最新一趟的数据直接当“标准耗时”,因为单次数据可能被红绿灯、堵车等偶然因素带偏。他用的方法是:新估算 = 大部分旧记忆 + 小部分新样本。这个思路落到 TCP 里就是 SRTT(平滑往返时间) 的递推更新。
由于网络波动是常态,TCP 还维护了一个偏差估计 RTTVAR,记录“实际往返时间和平均值的偏离程度”。两者结合,就是我们在第一节见过的 RTO 公式:
α 取 0.125、β 取 0.25 是 RFC 6298 的标准参数。它们决定了 RTO 对网络变化的反应速度:α 越小,SRTT 越平滑但反应越迟钝;α 越大,反应越快但越容易被单次抖动带偏。老唐选择“每次只信新样本一点点”,正是为了在“快速适应”和“稳定不飘”之间取得平衡。
- SRTT 递推更新新 SRTT 由 87.5% 的旧值和 12.5% 的新样本加权合成,α 取 0.125 表示“每次只相信新样本一点点”。
- RTTVAR 递推更新偏差统计同样采用指数加权,β 取 0.25,衡量“这次偏离平均值的程度”,每次吸收 25% 的新偏差信息。
- 合成 RTO为了对抗突发抖动,RTO 至少等于 SRTT 加 4 倍偏差;同时 RFC 规定 RTO 下限不低于 1 秒(早年实现),避免频繁伪重传。
SACK选择性确认
当网络一次丢了多个包裹时,老唐的麻烦来了:快速重传只能“猜”出第一个丢的包,剩下的包还要靠后续的冗余 ACK 一个个去猜、一个个去补。更糟的是,如果只靠累积 ACK,接收方甚至无法告诉发送方“我其实已经收到了 5 号和 7 号,就差 6 号”。这时 TCP 祭出SACK(Selective Acknowledgment,选择性确认):让接收方在 ACK 里附上一张“缺货清单”,精确列出哪些包裹已到、哪些还缺。
SACK 之所以重要,是因为它把“快速重传”从“猜一个补一个”升级为“看一眼全补”。配合快速重传使用,SACK 能显著提升多包丢失场景下的恢复效率:老唐不再需要等阿诚喊三遍才知道第一件丢的是 3 号,而是看着缺货清单一次性把 3 号、6 号、9 号全部补发完——这在无线网络、高丢包链路上效果尤其明显。
- 确认块结构每个块是一个左闭右开的序号区间,标记一段已连续收到的数据范围。
- 多包丢失识别接收方把所有已收段全部罗列出来,发送方对比自己的发送记录,即可算出所有丢失段的位置。
- 一次重传多个缺失段得到完整丢失列表后,发送方不再受“每次只重传一个段”的限制,可在一个 RTT 内补发所有缺口。
AI 应用场景
直播推流链路优化
阶段:网络传输优化
解决的问题:视频直播推流依赖 TCP 传输,主播网络抖动导致丢包时,传统 RTO 设置过长会造成画面卡顿数秒。通过调小 RTO 下限、启用 SACK 和快速重传,能让丢包后 1-2 个 RTT 内恢复传输,显著降低卡顿率。
具体案例:某直播平台的推流 SDK 在弱网测试中发现,启用 SACK 后多包丢失场景的重传效率提升约 40%,配合快重传参数调优,主播上行丢包 5% 时观众端卡顿时长从 4.8 秒降至 1.2 秒。
游戏服务器指令同步
阶段:实时通信优化
解决的问题:多人竞技游戏中,玩家操作指令经 TCP 传输时若发生丢包,超时重传的长等待会导致“瞬移”和操作回滚。网络层开启快速重传与快速恢复,让服务器在 3 个冗余 ACK 触发下即时补发指令包,保障操作流畅。
具体案例:某 MOBA 游戏开发团队在跨区对战场景中发现,默认 Linux TCP 栈未开启 SACK,跨洋链路 1% 丢包率下重传延迟约 200ms;开启 SACK 并将 RTO 最小下限调至 200ms 后,指令恢复时间缩短到 60ms 以内。
数据中心大流量吞吐调优
阶段:网络运维与容量规划
解决的问题:数据中心内部 AI 训练任务涉及海量参数同步,TCP 丢包会导致训练任务停滞。通过观察 cwnd 曲线和重传统计,运维人员可定位是超时重传过多(说明拥塞严重)还是快速恢复频繁(说明偶发丢包),从而针对性调整网络设备缓冲和流控参数。
具体案例:某云厂商在训练集群扩容时发现,分布式同步的 all-reduce 流量因尾部丢包触发大量超时重传,吞吐量下降 30%;调整交换机 ECN 阈值并结合快重传参数后,有效吞吐恢复至理论值的 92%。
Python 演示
"""TCP 超时重传与快速恢复模拟"""import random MSS = 1SRTT = 100.0RTTVAR = 50.0ALPHA = 0.125BETA = 0.25 def update_rto(rtt_sample): global SRTT, RTTVAR SRTT = (1 - ALPHA) * SRTT + ALPHA * rtt_sample RTTVAR = (1 - BETA) * RTTVAR + BETA * abs(SRTT - rtt_sample) return SRTT + 4 * RTTVAR def after_loss(cwnd, kind): ssthresh = max(cwnd // 2, 2 * MSS) if kind == "timeout": return MSS, ssthresh return ssthresh + 3 * MSS, ssthresh def run(): cwnd, ssthresh = MSS, 16 events = (["normal"] * 4 + ["timeout"] + ["normal"] * 3 + ["fast"] + ["normal"] * 3 + ["fast"] + ["normal"] * 3) for i, ev in enumerate(events, 1): rtt = random.uniform(80, 120) rto = update_rto(rtt) if ev == "normal": if cwnd < ssthresh: cwnd *= 2 else: cwnd += 1 else: cwnd, ssthresh = after_loss(cwnd, ev) print("第%2d轮 %-4s cwnd=%-3d ssthresh=%-3d RTO=%.1fms" % ( i, ev, cwnd, ssthresh, rto)) run()
def update_rto(rtt_sample):实现 RTO 平滑估计,每来一个新 RTT 样本就更新 SRTT 与 RTTVAR,再合成 RTO。SRTT = (1 - ALPHA) * SRTT + ALPHA * rtt_sample对应正文的 SRTT 递推公式,α=0.125 表示只吸收 12.5% 的新样本。RTTVAR = (1 - BETA) * RTTVAR + BETA * abs(SRTT - rtt_sample)偏差统计用 β=0.25,量化往返时间的波动程度。def after_loss(cwnd, kind):丢包后的统一处理入口,先算 ssthresh 再按事件类型分支。ssthresh = max(cwnd // 2, 2 * MSS)阈值减半且有 2 个 MSS 的下限,对应正文公式。return ssthresh + 3 * MSS, ssthresh快速恢复分支:新 cwnd 等于“减半阈值加 3”,不回到慢启动。if cwnd < ssthresh: cwnd *= 2 else: cwnd += 1慢启动翻倍、拥塞避免加一的窗口调控规则。events = ...预设事件序列,在"normal"中插入一次 timeout 和两次 fast,模拟两种丢包场景对比。
常见误区
误区我以为是网络丢包了,赶紧重传。但有时候其实是网络路径变了,导致包乱序到达,接收方发冗余 ACK 只是因为顺序不对,并不是真的丢了。
正解3 个冗余 ACK 是“大概率丢包”的强信号,但不是绝对证据。包乱序、重复投递都可能产生冗余 ACK。所以 TCP 校验了重复 ACK 携带的序号是否在已发送但未确认的窗口内,若序号不合法则忽略。业界还有“DSACK“(带 SACK 的重复确认)机制,专门告诉发送方”你重传的包可能白传了”。
为何常见:资料里常简化说“三次冗余 ACK 触发重传”,初学者容易忽略“为什么是三次”背后的概率考量。
误区反正都是把丢了的包重新发一遍,区别不大,性能应该差不多吧。
正解两者差别巨大。超时重传要等 RTO 到期,等待期可能长达数百毫秒甚至数秒,且触发后 cwnd 归 1 重新慢启动;快速重传在收到 3 个冗余 ACK 的瞬间就重传,等待时间几乎为零,且配合快速恢复 cwnd 只降一半。一个是“最后保险”,一个是“主动救援”,恢复代价完全不同。
为何常见:从用户视角看两者都表现为“数据最终到了”,容易忽视底层窗口调整的巨大差异。
误区RTO 越小,丢包发现得越快,重传越及时,用户体验应该更好。
正解RTO 太小会导致“伪重传”:网络只是稍微慢了一点,发送方就误判丢包并重传,白白浪费带宽、加重拥塞。RFC 6298 规定 RTO 下限为 1 秒(后续实现中可调小但需谨慎),正是为了防止这种“过敏反应”。就像老唐不能在快递刚晚点两分钟就断定包裹丢了——那样只会让路上的包裹更多、路更堵。
为何常见:直觉上“反应快 = 好”,但忽略了网络环境的动态性和误判的代价。
误区丢包之后 TCP 都要从头慢启动,快速恢复只是换了个名字。
正解恰恰相反,快速恢复的设计初衷就是避免回到慢启动。它只把 cwnd 降到 ssthresh(减半后的阈值)再补 3 个 MSS,然后继续在拥塞避免阶段线性增长;只有超时重传才回慢启动。两者是截然不同的“降速策略”——一个是切换档位继续跑,一个是靠边停车重新起步。
为何常见:把“快速重传”和“快速恢复”打包记成“丢包了就重来”,忽略了快恢复的“半降窗”独特价值。
误区有了 SACK 能精确知道哪些包丢了,那冗余 ACK 和快速重传还有啥用?删掉算了。
正解SACK 和快速重传是协作关系,不是替代关系。快重传是“触发机制”(谁先喊缺货),SACK 是“信息增强”(把缺货清单列全)。没有快重传,SACK 接收方仍可发冗余 ACK,但发送方缺乏明确的触发条件及时行动;没有 SACK,快重传在多包丢失时只能一个个猜。两者配合,才能实现“快速定位 + 完整补发”。
为何常见:人们容易认为“更先进的机制会淘汰旧机制”,但网络协议中很多机制是叠加演进而非互相取代。
面试题
Q1. TCP 快速重传的触发条件是发送方收到几个相同的重复 ACK?
- A.1 个
- B.2 个
- C.3 个← 正确答案
- D.4 个
答案:C。收到 3 个冗余 ACK 才触发快速重传,这是 RFC 的标准阈值。1 个或 2 个冗余 ACK 可能只是包乱序导致的,不足以判定丢包。就像阿诚喊一遍“缺货”可能是路况问题,喊三遍才是真的丢了。
Q2. TCP 发生超时重传后,拥塞窗口 cwnd 的值被设置为多少?
- A.1 个 MSS← 正确答案
- B.当前 ssthresh
- C.ssthresh + 3 个 MSS
- D.当前 cwnd 的一半
答案:A。超时重传意味着网络可能严重拥塞,TCP 将 cwnd 直接降为 1 个 MSS,从头开始慢启动试探。这好比老唐把卡车换成三轮车,先探探路况再说。
Q3. 快速恢复之后,新的拥塞窗口 cwnd 值为多少?
- A.1 个 MSS
- B.当前 ssthresh
- C.ssthresh + 3 个 MSS← 正确答案
- D.当前 cwnd 的一半
答案:C。快恢复中 cwnd = ssthresh + 3×MSS。ssthresh 先降为旧 cwnd 的一半,再加回 3 个 MSS——这 3 个 MSS 对应已成功到达对端的 3 个报文段。注意 A 选项是超时重传的处置,不要混淆。
Q4. 快速恢复机制中,ssthresh 的更新公式是什么?
- A.ssthresh = cwnd
- B.ssthresh = max(cwnd / 2, 2 × MSS)← 正确答案
- C.ssthresh = cwnd / 4
- D.ssthresh = MSS
答案:B。无论超时还是快恢复,ssthresh 统一更新为 max(cwnd/2, 2×MSS)。下限 2 个 MSS 是为了防止阈值过小导致窗口永远卡在慢启动阶段。
Q5. 以下哪些情况会触发 TCP 重传?
- A.RTO 超时← 正确答案
- B.收到 3 个冗余 ACK← 正确答案
- C.SACK 选项指示有报文段丢失← 正确答案
- D.收到接收窗口为 0 的报文
答案:ABC。RTO 超时触发超时重传,3 个冗余 ACK 触发快速重传,SACK 告知缺失段后发送方也会针对性重传。D 选项的零窗口只是暂停发送,并不触发重传——那是流量控制而非丢包恢复。
Q6. 与超时重传相比,快速重传+快速恢复的优势有哪些?
- A.无需等待计时器到期,恢复更及时← 正确答案
- B.cwnd 只降一半而非归 1,吞吐量损失小← 正确答案
- C.能识别出所有类型的丢包
- D.不需要接收方发送冗余 ACK
答案:AB。快重传用冗余 ACK 提前感知丢包,不必等 RTO;快恢复将 cwnd 降为 ssthresh+3 而非归 1。但快重传只针对单包丢失高效,多包丢失需配合 SACK;且它依赖接收方发冗余 ACK,D 说反了。
Q7. 关于 SACK 机制,以下说法正确的有哪些?
- A.接收方用 SACK 块精确告知发送方哪些数据段已收到← 正确答案
- B.SACK 能显著提升多包丢失场景的重传效率← 正确答案
- C.SACK 可以完全取代快速重传
- D.SACK 选项携带在 TCP 报文头部← 正确答案
答案:ABD。SACK 的确认块 [L,R] 精确标记已收区间,多包丢失时一次就能补发全部缺口。但它是快速重传的增强而非替代——快重传负责触发、SACK 负责信息,两者配合才高效。SACK 信息确实携带在 TCP 选项字段中。
Q8. RTO(重传超时时间)的标准估计公式是?
- A.RTO = SRTT
- B.RTO = SRTT + 4 × RTTVAR← 正确答案
- C.RTO = RTTVAR + 4 × SRTT
- D.RTO = SRTT + 2 × RTTVAR
答案:B。RFC 6298 规定 RTO = SRTT + 4×RTTVAR。SRTT 是平滑的往返时间均值,RTTVAR 是往返时间的偏差估计,4 倍偏差为安全余量。切忌把系数张冠李戴——均值占主导、偏差做缓冲。
Q9. SRTT 平滑更新公式中,平滑系数 α 的标准取值是?
- A.0.125← 正确答案
- B.0.25
- C.0.5
- D.1.0
答案:A。α=0.125 意味着新 SRTT 保留 87.5% 的旧值、吸收 12.5% 的新样本;β=0.25 用于 RTTVAR 更新。α 取 0.125 是为了让估计值足够平滑,不被单次抖动带偏。
Q10. 快速恢复算法中,为何新 cwnd 要在 ssthresh 基础上加上 3 个 MSS?
- A.为了加速慢启动的增速
- B.因为 3 个冗余 ACK 证明已有 3 个报文段成功到达对端并离开网络← 正确答案
- C.因为 ssthresh 计算时少算了 3 个 MSS
- D.为了填满接收方的接收窗口
答案:B。3 个冗余 ACK 意味着有 3 个比丢失段序号更大的报文段已到达接收方,这些段占用的网络容量已被释放。所以 cwnd 在减半的 ssthresh 基础上加回 3 个 MSS,相当于“承认这 3 段已走完的路程”,既不低估网络容量也不过度激进。