☰
TCP滑动窗口与快速重传:从原理到实战排查全解析
2026/10/23 8:19:09 网站建设 项目流程

先讲个我实际排查过的场景:一个文件下载服务,跑起来头几秒速度很猛,然后突然掉到几百KB,后面怎么都回不去。抓包看了一圈,TCP窗口字段在跳,满屏的Dup ACK和Retransmission。那一刻我意识到,滑动窗口和快速重传如果只停留在“懂了大概”的层面,线上出问题就只能靠猜。

这篇文章想把这两个机制掰开揉碎讲清楚:TCP为什么需要滑动窗口,快速重传到底快在哪,它们怎么配合工作,以及在实际抓包、内核参数、性能调优里怎么落地。适合后端开发、网络运维,以及所有被接口变慢、文件传输带宽起不来折磨过的人。我会把原理、公式、抓包现象和实战经验串在一起讲,尽量让你读完能直接拿去用。

1. 先搞清楚TCP为什么需要滑动窗口和快速重传

1.1 停等协议的低效,是理解这两个机制的起点

很多人背过TCP是“可靠传输”协议,但可靠是怎么来的?最简单粗暴的模型是一次只发一个报文段,发完就停下来等ACK,收到确认再发下一个。这叫停等协议。它绝对可靠,但效率低到离谱。

我算一笔账你就懂了。假设链路带宽100Mbps,往返时延RTT是100ms,一个报文段MSS取典型值1460字节。发送这一个报文段需要的时间是:

1460 * 8 / 100,000,000 ≈ 0.117ms

但发完之后,你要等整整一个RTT(100ms)才能收到ACK。也就是说,100.117ms的时间里,真正在传输数据的时间只有0.117ms,链路利用率大约0.12%。剩下99.88%的时间,链路完全是空闲的。

这就像你去银行柜台办业务,柜员办完一笔要等总部系统确认,确认期间柜台就闲着,下一笔业务必须等反馈回来才能办。单笔业务本身很快,但等待确认的时间把整个吞吐拖垮了。

现实中的TCP不可能这么干,所以必须让链路里同时存在多个在途报文,这就是流水线思想。滑动窗口解决的核心问题,就是管理这条流水线上最多能有多少数据“飞在路上”。

1.2 滑动窗口解决的问题与设计取舍

滑动窗口本质上是在发送缓冲区上维护了一个逻辑区间,数据按序号排好,窗口左边是已经确认的部分,窗口右边还没轮到发送。随着ACK到达,窗口不断右移,所以叫“滑动”。

这个机制解决的第一个问题是吞吐:允许多个报文同时发送,不再等一个确认再发一个。第二个问题是流量控制:窗口大小限制了在途数据量,不会让接收方缓存爆掉。第三个问题是重传边界:通过确认号,发送方清楚哪些数据已经安全到达、哪些还需要重传。

设计上的取舍也很明显。窗口太小,在途数据少,带宽再大也跑不满;窗口太大,数据一股脑全塞到网络上,可能直接把瓶颈链路打爆,接收方也扛不住。所以TCP需要用两个维度的窗口互相约束,一个听接收方的,一个听网络拥塞状态的。这部分在第二章详细展开。

我个人的理解是,滑动窗口的本质就是“限流但不刹死”。它允许你一口气发一批数据,但不允许你无限制地发。这个“限量”由接收方和网络共同决定,这正好符合我们做服务限流的直觉。

1.3 丢包检测的双保险:超时重传与快速重传

可靠传输除了要靠确认机制,还得解决丢了怎么办。TCP有两条路:

第一条是超时重传。发送方给每个报文段启动一个重传计时器,如果在RTO(Retransmission Timeout)时间内没收到ACK,就认为丢了,重新发送。RTO通常比RTT大不少,因为要考虑网络抖动,不能设太紧。超时重传的问题就是慢,它往往要等几百毫秒甚至更久,而且一旦超时,TCP对拥塞的判断会非常激进,直接进入慢启动,吞吐断崖式下跌。

第二条是快速重传。接收方收到乱序报文时,会立刻回复一个重复ACK,告诉发送方“我最期望的序号还是这个,赶紧发我缺的”。当发送方连续收到3个重复ACK,就认为报文丢了,不等超时,立刻重传。因为触发时间通常在一个RTT内,比超时快得多,所以叫“快速”。

两条路径不是谁替代谁,而是兜底关系。快速重传需要接收方有后续报文到达才能产生重复ACK,如果某个报文丢了之后,后续再也没有数据包到达,发送方等不到重复ACK,只能靠超时救命。这个细节后面推演部分会重点讲,实际排障时非常关键。

我把两个机制放在一起对比一下:

机制触发条件触发速度对拥塞窗口的影响
超时重传计时器到期仍未收到ACK通常为RTO,几百毫秒到秒级cwnd视为拥塞,降为1,重新慢启动
快速重传收到3个重复ACK一般一个RTT内cwnd减半,进入快速恢复

排障时看到大量超时重传,说明链路状态已经非常差;看到快速重传次数增加,说明开始有轻微丢包,需要警惕。

2. 滑动窗口的工作机制,搞懂窗口怎么动

2.1 发送窗口内部的三指针模型

滑动窗口不是一个抽象概念,它在TCP协议栈内部有明确的实现。发送方维护三个关键值:

  • SND.UNA:最早一个还没有被确认的序号。它对应窗口的左边界。
  • SND.NXT:下一个要发送的新数据的序号。
  • SND.WND:发送窗口大小,表示发送方当前允许发送的最大字节数。

这三个值把发送缓冲区划分成几个区域:SND.UNA左边的数据全部已确认,可以丢弃;SND.UNA到SND.NXT之间是已经发送但还没收到确认的数据;SND.NXT到SND.UNA+SND.WND之间是允许发送但还没发的新数据;再往右就是暂时不能发的数据。

你可以想象成地铁站台的排队上车场景:SND.UNA是检票口,SND.NXT是车厢门,窗口右边是队尾的隔离带。乘客依次上车,每往前挪一步就代表一个新的ACK确认。窗口滑动的本质就是检票口向右推进,队尾跟着放进来新的乘客。

有一个细节很多人会记混:窗口的单位是字节,不是报文段数。MSS是1460字节,如果接收窗口是8KB,那大概能容纳5个报文段,而不是8个。抓包和日志里看到的窗口值都是以字节为单位的。

累计确认机制也在这里体现:一个ACK号为N的确认包,代表序号N之前(不包含N)的所有字节都收到了。所以接收方不需要给每个报文段单独回ACK,发一个ACK就能确认一大片,协议开销小很多。

2.2 窗口大小谁说了算:rwnd和cwnd要取最小

TCP的窗口不是一个值,而是两个值共同决定的。接收窗口rwnd由接收方通过TCP头部的Window字段通告给发送方,代表“我还能接收多少字节”。这个值取决于接收方应用程序读取数据的速度和接收缓冲区剩余空间。如果应用读得慢,rwnd会越来越小,最后变成0,发送方就得停下来,这叫零窗口。

拥塞窗口cwnd由发送方自己维护,代表“根据网络拥塞状况,我认为现在可以发送多少数据”。慢启动阶段cwnd从初始值开始指数增长,达到ssthresh后进入拥塞避免,遇到丢包或超时再大幅收缩。

真正决定发送窗口大小的是:

发送窗口 = min(rwnd, cwnd)

两者都必须考虑。如果只看rwnd,网络可能早就堵了但发送方还在硬塞;如果只看cwnd,可能接收方缓存已经要爆了发送方还在发。所以在实际调优中,你既得看接收方通告窗口,也得看网络拥塞情况。

这里我提一个真实踩过的坑:某个下载服务吞吐上不去,抓包发现窗口值一直比较小,一开始以为是接收方缓存不够,后来仔细看TCP握手包,发现Window Scale factor是0,也就是窗口缩放没生效,rwnd最大只能到64KB。而对于当时那条链路来说,这正是限制吞吐的瓶颈。

2.3 窗口缩放和BDP:为什么大带宽长链路一调就灵

老TCP协议里窗口字段只有16位,最大值刚好吃掉一整段,也就是65535字节。放到20年前的网络环境是够用的,但现在动辄几百兆、上万兆带宽,64KB窗口在高时延链路上根本跑不出性能。

限制条件可以用带宽时延积(BDP)算出来:

需要的窗口大小 = 带宽 × 往返时延

举个例子,链路带宽200Mbps,RTT是20ms:

200,000,000 × 0.02 = 4,000,000 bit ≈ 500KB

如果你窗口只有64KB,那理论最大吞吐:

64KB / 20ms = 3.2768MB/s ≈ 26.2Mbps

200Mbps的链路,实际只能跑出26Mbps,这就是窗口太小造成的。

要解决这个问题,需要TCP窗口缩放选项(TCP Window Scaling)。窗口字段加上左移的位数后,最大可以到约1GB。两端必须在握手时协商该选项,如果有一边不支持或没有开启,窗口就会回落到64KB。

我在调优高性能链路时,第一件事就是抓TCP握手包,确认Window Scale factor是否正常协商。如果为0或没有该选项,后面的参数都不用看,先把这个问题解决再说。Linux下可以通过net.ipv4.tcp_window_scaling控制,默认是开启的,某些嵌入式设备或老版本系统可能会关掉,这时候长肥链路性能劣化基本逃不掉。

3. 快速重传为什么“快”,以及和快速恢复的关系

3.1 重复ACK的产生与“三个”阈值

快速重传的触发信号是重复ACK,要理解它,得先明白乱序时接收方会怎么反应。

接收方维护一个期望收到的下一个序号RCV.NXT。假设发送方依次发了报文段1、2、3、4,其中段2丢了,段3先到。接收方发现序段跳过了2,它不能确认2,因为2还没收到,只能把已经连续收到的最高序号——也就是段1的边界——回给发送方。这个ACK的确认号还是“1”,和之前收到段1时回的ACK一模一样,所以叫重复ACK。

等段4也到了,情况还是一样的,接收方继续回确认号“1”的ACK。于是发送方短时间内连续收到多个相同的ACK号。这有点像你在群里看到三个人同时说“少了一个文件”,说明大概率真少了一个,而不是一个人看错了。

为什么阈值是3?因为轻微乱序也会产生重复ACK。网络里常见的乱序一般只涉及一两个报文,如果重传阈值设成1或2,一次正常的乱序就可能触发重传,反而浪费带宽。RFC建议把快速重传阈值设为3,也就是连续收到3个重复ACK再判定丢包。这时候因为单个报文乱序导致连续3个重复ACK的概率已经很低,误判风险可控。

注意,重复ACK的确认号必须和之前一样,不能携带新的累计确认信息。如果ACK号往前推进了,那说明有新的数据被确认,这是正常的ACK,不参与重复计数。

3.2 快速重传与快速恢复的联动

快速重传触发后,发送方做的第一件事是立即重传丢失的报文段,同时进入快速恢复状态。RFC 5681里描述的标准行为是:收到3个重复ACK时,把ssthresh设为当前cwnd的一半,cwnd也降为一半,然后进入拥塞避免阶段;每再收到一个重复ACK,cwnd临时加一个MSS,表示链路还有数据在流动;直到收到确认新数据的ACK,才退出快速恢复。

这个机制的精妙之处在于,它把丢包当成一次“需要减速”的信号,而不是“需要停车”的信号。超时重传会把cwnd直接重置为1,从慢启动重新爬坡,对吞吐伤害巨大,这也是为什么超时重传频繁时,应用会明显感觉到“卡一下”。快速重传只砍一半,后续还能慢慢恢复,整个流程平滑很多。

用开车类比:超时重传是前方突发事故,一脚急刹停到路边,再起步时必须慢慢加速;快速重传更像看到前车尾灯亮起,先减到半速,确认路况后逐渐回到正常车速。两者对整体通行效率的影响完全不是一个量级。

3.3 SACK选择性确认,把快速重传效率再推一层

早期的TCP没有SACK,发送方只能靠累积确认猜哪些报文丢了。快速重传虽然能快速补发,但补发范围往往过大,容易重传一些其实已经到达的报文。

SACK(Selective Acknowledgment)解决了这个问题。接收方可以在ACK里额外携带SACK块,告诉发送方“这些区间我都收到了”,发送方就知道具体缺哪些段,只补发真正丢失的部分。抓包里会看到TCP SACK选项,里面有一组序号区间,一看就知道哪些数据已经被接收方缓存。

在实际环境中,SACK对丢包恢复效率的提升非常明显。我处理过一个跨机房传输项目,开启SACK之后,重传包的数量下降了大概一半,吞吐恢复时间也明显缩短。Linux默认是开启tcp_sack的,但中间有网络设备或对端系统不支持时,协商会退化。排查时如果发现大量不必要的重传,记得看一眼握手包里的SACK选项。

4. 实际网络里怎么看窗口、抓重传、调参数

4.1 用抓包工具让窗口“可视化”

纸上谈兵说再多,不如实际抓一次包。Wireshark打开一个TCP流,你最需要关注这几列和字段:

  • TCP Window:这是接收方通告的窗口值,如果上面开了窗口缩放,抓包工具通常会显示缩放前的值和实际值。
  • Window scale factor:缩放因子,标记在TCP选项里。
  • Sequence number 和 Acknowledgment number:用来判断累计确认推进了多少。
  • TCP Dup ACK / TCP Retransmission / TCP Fast Retransmission:Wireshark的分析标记,能直接看出哪里的重传被触发。

我排查下载慢问题时,一般先过滤出目标TCP流的Expert Info,看里面有没有大量Retransmission和Dup ACK。如果是快速重传,你会发现重传报文和3个重复ACK在时间上靠得很近,间隔通常在1个RTT以内;如果是超时重传,间隔就松松垮垮,几百毫秒甚至更久才出现一次。

这里有个容易误判的点:Wireshark标记的TCP Retransmission,有快重传也有超时重传,还有可能是接收端重复确认引发的伪重传标签。不能只盯着数量,要结合时间和序号来判断。如果同一序号的重传出现在三个重复ACK之后,基本是快速重传;如果前面没有任何重复ACK,就是超时兜底。

4.2 性能问题定位:现象与排查路径

我把平时排查网络性能问题时最长遇到的几类现象整理成一个表格,方便你按图索骥:

现象大概率原因排查路径
吞吐跑不满,窗口值一直上不去窗口小于BDP,或窗口缩放未开启计算BDP,确认握手包里的Window Scale factor
大量Dup ACK和快速重传链路丢包或出口拥塞看网卡错误计数、长时间ping丢包率、运营商链路质量
接收窗口长期为0,发送方停等接收方应用读取太慢,缓冲堆积检查接收端进程CPU、内存、Socket读缓存积压情况
超时重传多,但没有重复ACK突发大流量丢包或路由黑洞抓包分析重传间隔,确认对端是否完全不回ACK
重传效率低,一次重传一大片SACK未开启或对端不支持看握手包SACK选项,评估中间设备兼容性

这几种现象里,接收窗口长期为0是我见过最隐蔽的问题。表面上看是网络问题,实际上可能是接收方应用不做流量控制,数据积压在Socket缓冲里,通告窗口一步步被压到0,整个连接进入僵死状态。抓包时看到窗口稳步减小到0,基本可以断定问题在接收端应用,而不是网络链路。

另一个常见误区是看到重复ACK就怀疑丢包。实际上偶尔几个重复ACK可能只是乱序,等连续3个以上再去定位丢包,会准确很多。

4.3 Linux内核参数该改哪些

在Linux上,和滑动窗口、快速重传相关的主要内核参数有这些。下面是我实际用过的调整命令:

# 开启窗口缩放,大带宽长链路必备 sysctl -w net.ipv4.tcp_window_scaling=1 # 开启选择性确认,提升重传效率 sysctl -w net.ipv4.tcp_sack=1 # 查看TCP收发缓冲的当前范围(最小值、默认值、最大值) sysctl net.ipv4.tcp_rmem net.ipv4.tcp_wmem

默认情况下,多数Linux发行版已经把tcp_sack和tcp_window_scaling打开了。真正需要动的是tcp_rmem和tcp_wmem,这两个值分别控制接收和发送缓冲区的大小范围。简单说,缓冲区太小会让rwnd提前收紧,窗口打不满;缓冲区太大又会在高并发场景下吃掉大量内存。

我记得有次给一个高并发API服务调优,一次性把tcp_rmem最大值调到了16MB,结果几十万个连接直接把内存耗光,反而引发了新的问题。后来按照“先抓包确认瓶颈,再小步调整”的原则,每次只动默认值和最大值,配合监控观察两天,才把参数稳定下来。

调优顺序我的建议是:先抓包判断窗口和丢包情况,再决定是调窗口缩放、缓冲区还是拥塞控制算法。盲目改内核参数,大概率是白调,甚至帮倒忙。如果业务场景是长距离大流量传输,拥塞控制算法也值得关注,像cubic、bbr这类算法在不同链路上的表现差异非常大,但换算法前一定要有对比测试数据。

5. 一个模拟推演:滑动窗口与快速重传的完整配合

5.1 推演场景设定

理论讲完,我们做个推演。假设一个TCP连接已经建立,MSS是1460字节,接收窗口rwnd是8个报文段,发送窗口主要受cwnd限制。为了演示方便,我直接假设发送窗口固定为8个报文段,编号为1到8,初始一次性全部发出去。

这里简化了慢启动和拥塞避免的细节,真实场景中窗口是逐渐扩大的,但固定窗口更容易看清滑动和重传的逻辑,推演结论不会变。

链路RTT假设50ms,RTO假设200ms。这个RTO差距后面对比时会非常直观。

5.2 正常情况下的窗口滑动

先看没有丢包的情况。发送方发出报文段1到8后,SND.NXT停在9,窗口左边界SND.UNA还在1。

接收方依次收到1、2、3、4。由于累计确认,它收到4后只需要回一个ACK号为5的确认包(表示1到4全部收到),不需要为前几个段分别回ACK。发送方收到ACK=5后,SND.UNA移动到5,窗口左边界右移,它现在可以继续发送9到12这四个新段。

这个过程很关键:8个段在途,但确认只需回4次左右,而且每次确认都能释放四个新段的发送额度。滑动窗口在这种“批量确认、批量放行”的模式下,既能保持高吞吐,又不需要每发一个包就等一次确认。

如果所有报文都正常到达,最终发送方会在收到ACK=9、ACK=13后,依次把窗口滑到整个数据流末尾。整个过程丝滑,没有任何重传。

5.3 丢包时的快速重传推演

现在模拟一个坏场景。假设发送方一次发出1到8一共8个段,其中段2在链路上丢了,段1、3、4、5、6、7、8正常到达接收方。

接收方先收到段1,回ACK=2。然后收到段3,发现段2缺失,只能继续回ACK=2。段4、5、6、7、8到达后,每收到一个都会触发一个重复ACK=2。这样一来,发送方在很短的时间内连续收到了7个重复ACK。

当第3个重复ACK到达时,快速重传条件已经满足。发送方立即重传段2,而不是傻等RTO超时。由于此时接收方已经缓存了3到8,收到重传的段2后,它就能把连续序号拼到9,直接回一个ACK=9。发送方收到ACK=9后,窗口一次性向前推进一大截,可以继续发送后面的新数据。

整个过程从发现段2丢失到重传完成,大约只需要1个RTT(50ms),而如果走超时重传,等待时间最少是RTO(200ms),而且超时后cwnd会直接降为1,后面要重新慢启动。在有大量数据要传的场景里,这种时间差会放大几十倍。

我再用表格简化一下这个推演过程:

时间点事件发送方动作
T0发送1-8SND.UNA=1
T0+RTT/2段2丢失,其余到达收到重复ACK=2
T0+RTT已收到3个重复ACK快速重传段2
T0+1.5×RTT段2重传到达收到ACK=9,窗口滑动到9-16

注意这里有个重要的现实细节:如果发送窗口只有4个段,段2丢了,段3、4到达只会产生2个重复ACK,达不到3次触发条件。发送方等不到快速重传的触发信号,最终只能由RTO超时兜底。所以“快速重传一定能触发”是个错觉,它也有覆盖不到的边界。

5.4 推演结果里的工程经验

这个推演告诉了我几个特别实用的结论。

第一,快速重传的本质是“用重复ACK代替计时器来加速丢包发现”。它能生效的前提是:在丢包之后,接收方还能陆续收到其他后续报文,从而生成足够的重复ACK。如果丢失的是窗口尾部的报文段,后续没有数据可来,就只能靠超时。

第二,窗口较大、链路上有持续数据流时,快速重传更容易被触发,恢复也更快。这也是为什么大文件传输、视频流这类持续发送场景,对丢包没有那么致命;反而是交互式小数据传输,赶上丢包往往要等超时,用户感知延迟明显。

第三,抓包判断重传类型也有技巧。快速重传的时间点和三次重复ACK高度集中,基本在一个RTT内完成;超时重传则总是隔着一个明显的RTO间隔。排障时先看时间线,基本一眼就能分辨。

排查这类问题,我现在的习惯是永远先抓包再谈调参。滑动窗口和快速重传这两套机制看着理论,但每次都实打实地出现在线上问题里。看懂窗口怎么滑、重复ACK怎么数、重传是快是慢,许多网络疑难怪症就不再是玄学,而是有清晰线索的工程问题。希望这篇经验和推演对你也有用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询