4. TCP流量控制:滑动窗口协议、窗口大小与通告、零窗口与持续计时器
流量控制这个话题,说白了就是解决一个问题:发送方别把接收方撑死。
接收方的缓冲区是有限的。你发得再快,人家来不及处理,数据就得丢。TCP 的设计者很聪明,搞了一套滑动窗口机制,让双方能协商一个合适的发送节奏。我刚开始调网络性能的时候,总觉得这玩意儿太保守,后来在线上环境吃过亏,才明白它的精妙之处。
4.1 滑动窗口协议:核心思想
滑动窗口,你可以把它想象成一个「发送许可证」的队列。
发送方不需要每发一个包就等一次确认。只要窗口没满,就可以连续发。窗口大小决定了你最多能同时发多少个字节而不等 ACK。
嗯,这里要注意:窗口是字节单位,不是包个数。
核心公式:
可用窗口 = 接收方通告窗口 - (已发送未确认的数据量)
我画了一张图,帮你理清滑动窗口的四个区域:
窗口滑动的过程是这样的:
- 发送方收到 ACK,左边界右移。
- 右边界根据接收方通告的窗口大小重新计算。
- 新的可用窗口 = 通告窗口 - (已发送未确认)。
- 发送方继续发送新数据,直到填满窗口。
我个人习惯把滑动窗口比作「流水线」。流水线越长,生产效率越高。但流水线太长,万一中间某个环节出问题,返工成本也大。TCP 就是在效率和可靠性之间找平衡。
4.2 窗口大小与通告
窗口大小不是发送方自己定的,而是接收方说了算。
接收方在每次回复 ACK 时,都会在 TCP 头部携带一个字段叫 Window Size(窗口大小)。这个值告诉发送方:我当前还有多少缓冲区空间,你最多能再发多少字节。
这个过程叫「窗口通告」。
小技巧:
TCP 头部的窗口字段只有 16 位,最大只能表示 65535 字节。但现代网络动不动就几 MB 的窗口,怎么办?
答案是 Window Scaling 选项(RFC 1323)。通过握手时的选项协商,可以把窗口放大到 1GB 级别。我在调优高带宽链路时,这个选项几乎是必开的。
窗口大小的变化,直接影响发送速率。举个例子:
| 场景 | 接收方缓冲区剩余 | 通告窗口 | 发送方行为 |
|---|---|---|---|
| 空闲状态 | 64 KB | 65535 | 正常发送 |
| 应用层读取慢 | 32 KB | 32768 | 降低发送速率 |
| 缓冲区快满 | 4 KB | 4096 | 几乎停止发送 |
| 缓冲区满 | 0 | 0 | 停止发送(零窗口) |
你想想看,如果接收方通告了一个很小的窗口,发送方还拼命发,那数据到了也只能被丢弃。TCP 不会干这种傻事。
4.3 零窗口与持续计时器
当接收方缓冲区完全满了,它会通告窗口大小为 0。这就是零窗口状态。
发送方收到零窗口通告后,必须立即停止发送。但问题来了:接收方什么时候才能恢复?
接收方应用层读取了数据,缓冲区腾出空间后,会发送一个非零窗口的 ACK。但如果这个 ACK 丢了,发送方就会一直等下去——这就是经典的死锁场景。
我曾经踩过的坑:
有一次排查一个 TCP 连接卡死的问题,抓包发现接收方发了窗口更新(非零窗口),但发送方没收到。双方就这么干等着,连接挂了整整 5 分钟。后来才发现,是中间某个路由器把那个窗口更新包给丢了。
TCP 的设计者早就想到了这个问题,解决方案就是 持续计时器(Persist Timer)。
持续计时器的工作原理:
- 发送方收到零窗口通告后,启动持续计时器(默认 5 秒)。
- 计时器超时后,发送方主动发送一个 窗口探测包(Window Probe),通常只有 1 字节数据。
- 接收方回复 ACK,通告当前窗口大小。
- 如果窗口还是 0,发送方重置计时器,等待下一次探测。
- 如果窗口非零,发送方恢复数据传输。
探测间隔会指数退避:5 秒、10 秒、20 秒……最大到 60 秒。这样做既避免了频繁探测浪费带宽,又能保证最终能恢复。
关键点总结:
- 零窗口是接收方告诉发送方「我吃不下了」。
- 持续计时器是发送方主动「试探」接收方是否恢复。
- 窗口探测包只有 1 字节,避免造成新的缓冲区压力。
下面这张图展示了零窗口到恢复的完整流程:
持续计时器的超时时间,不同操作系统实现略有差异。Linux 下初始是 5 秒,Windows 下也是 5 秒。但探测间隔的退避策略基本一致。
调优建议:
如果你在抓包时发现频繁出现零窗口,说明接收方应用层处理能力不足。这时候调大接收缓冲区(SO_RCVBUF)只能缓解症状,根本办法是优化应用层的读取逻辑。
我遇到过一台 Redis 服务器,因为慢查询导致 TCP 接收缓冲区频繁满,客户端不断触发零窗口。最后优化了查询,问题才彻底解决。
4.4 滑动窗口与流量控制的实战意义
理解了滑动窗口和零窗口机制,你在排查网络问题时就有了方向:
- 吞吐量低:先看窗口大小是否被限制。如果通告窗口很小,说明接收方是瓶颈。
- 连接卡死:抓包看是否有零窗口通告,以及持续计时器是否正常工作。
- 延迟抖动:窗口频繁变化,说明双方处理能力不匹配。
我个人习惯在排查性能问题时,先用 tcpdump 抓一段流量,然后用 Wireshark 的「TCP Stream Graph」里的「Window Scaling」图,一眼就能看出窗口变化趋势。如果窗口曲线像锯齿一样剧烈波动,那肯定有问题。
流量控制不是 TCP 的独角戏,它和后面的拥塞控制是搭档。窗口大小由接收方决定,但发送方实际能发的数据量,还要受拥塞窗口的限制。这部分我们后面会详细讲。