代理IP 链路上一条长连接突然卡住十几秒,抓包一看既没有丢包重传也没有连接断开,只有客户端每隔一会儿发一个 1 字节的小包。这个小包就是窗口探测,发它的计时器叫持续计时器。链路不是坏了,是接收方窗口曾经归零,而恢复通知在路途中丢了——持续计时器在兜底。
零窗口之后,发送方为什么不能自己继续发
TCP 的流量控制依赖接收窗口:接收方在确认报文里告诉发送方”我还能收多少字节”,发送方只在窗口范围内发数据。上一篇文章讲过零窗口——接收方窗口归零后发送方停止发送,等接收方腾出空间后发一个窗口更新 ACK 通知恢复。问题在于这个窗口更新 ACK 也会丢:它本身不带数据,接收方不会因为”发送方没回应”而重发,因为 TCP 只有收到数据才确认,收不到数据就永远不再发第二次窗口更新。
于是两边都进入等待:发送方等窗口更新,接收方以为窗口更新早就到了。如果没有兜底机制,这条连接会一直卡到天荒地老,任何一方都察觉不到异常。
TCP 的解法是持续计时器:发送方发现窗口归零后,启动一个 persist timer,到点就主动发一个 1 字节的窗口探测包(window probe)。接收方收到探测包后必须回复一个 ACK,即使窗口还是零也要回——这个 ACK 里就带着最新的窗口值。窗口恢复了,发送方立刻接着发;窗口没恢复,就按退避节奏继续探测。
| 对比项 | 持续计时器 Persist | 重传超时 RTO | Keepalive 探活 |
|---|---|---|---|
| 要解决什么 | 窗口恢复通知丢了 | 数据确认丢了 | 对端静默消失 |
| 发送什么 | 1 字节窗口探测 | 重发原数据段 | 空探针包 |
| 探测频率 | 退避增长 | 指数退避 | 固定间隔 |
| 代理链路上的表现 | 周期性 1 字节小包 | 重复数据段 | 空闲期探针 |
代理IP 链路为什么更容易被持续计时器”困住”
代理IP 链路把一条连接拆成两段,每段各有自己的发送窗口和持续计时器,但窗口更新 ACK 的传递路径变长了:本机到代理、代理到目标站两段连接在代理处背靠背转发,中间任何一端的接收缓冲被占满,窗口就会归零,而窗口更新 ACK 要穿过代理的转发队列才能到达对端。转发队列一积压,窗口更新 ACK 就被延迟;延迟超过发送方的探测节奏,持续计时器就会把 1 字节探测包发出来。
所以走代理IP 后,抓包看到成串的 1 字节小包并不罕见。判断是不是它在作怪,看三点:探测包的间隔是否在退避增长(5 秒、10 秒、20 秒翻上去);两次探测之间是否有大量已确认数据积压;窗口更新 ACK 到达后连接是否立刻恢复传输。三者都对上,基本就是窗口更新在代理转发队列里被拖住了。
处置方向:优先看代理转发缓冲与出口拥塞,而不是怀疑连接断了;代理IP 链路上的大流量长连接业务(同步、备份、推送)对窗口恢复最敏感,把这类流量安排到稳定出口、错开高峰,能明显减少”卡住十几秒又自己好”的现象。
零窗口不是终点,窗口更新丢失才是真正的坑——持续计时器就是 TCP 为这个坑兜的底,代理IP 链路的两段连接各有各的计时器,任何一段被拖住都会表现为莫名其妙的停顿。
