在物联网(IoT)场景中,设备数量庞大且网络环境复杂多变,对网络连接的稳定性和可靠性提出了极高的要求。然而,实践中我们常常会遭遇一些棘手的网络异常现象。“TCP 幽灵连接”便是其中一种令人困惑的问题,它指的是客户端认为连接已经成功建立,但是服务端却在反复重传 SYN+ACK 报文,认为连接仍处于 SYN_RECV 阶段。这种状态不一致可能导致客户端发送的数据丢失、连接建立失败或资源浪费,给 IoT 业务的稳定运行带来挑战。本文将深入探讨 TCP 幽灵连接产生的原因及一些业务建议。
客户端视角的问题现象与分析
此现象最初表现为数据通信层面的异常。运维排查发现,大量客户端与对端 20.30.3.101:502
建立的 TCP 连接长时间处于 FIN_WAIT1
状态。
进一步的网络抓包分析(报文 1 ~ 8)揭示了详细的交互过程:客户端在完成三次握手(SYN -> SYN+ACK -> ACK)后,随即发送了应用层数据。但这些数据报文似乎未被服务端成功接收或确认,引发了客户端的多次数据重传。在若干次重传尝试后,客户端最终选择发送 FIN 报文来终止该连接。
报文 9 揭示了一个关键信息:服务端(20.30.3.101
)在此时重新发送了 SYN+ACK 报文(与报文 2 内容相同)。这表明服务端并未成功接收到客户端发送的第三次握手 ACK 报文(报文 3)。因此,从服务端的视角来看,TCP 连接并未成功建立,仍处于 SYN_RECV 状态。
这种客户端与服务端状态的不一致(客户端认为连接已建立并开始发送数据,而服务端仍在等待握手完成)正是“TCP 幽灵连接”的核心特征。由于服务端未收到 ACK,它会忽略客户端后续发送的数据报文(报文 4 ~ 7)和 FIN 报文(报文 8),并持续重传 SYN+ACK。
客户端在收到重复的 SYN+ACK(报文 9)后,根据其当前状态(FIN_WAIT1
)会回应一个 ACK(报文 10),但服务端依然无法正确处理这个 ACK,因为它期望的是对初始 SYN+ACK 的确认。随后,客户端继续重传其数据和 FIN(如报文 11、12 等),而服务端则继续重传 SYN+ACK(如报文 13、16 等),双方陷入无效的通信循环,直到一方超时放弃。
1 172.20.42.84.59032 > 20.30.3.101.502: Flags [S], seq 4013763009, win 64240, length 0
2 20.30.3.101.502 > 172.20.42.84.59032: Flags [S.], seq 1013872532, ack 4013763010, win 14480, length 0
3 172.20.42.84.59032 > 20.30.3.101.502: Flags [.], ack 1, win 502, length 0
4 172.20.42.84.59032 > 20.30.3.101.502: Flags [P.], seq 1:13, ack 1, win 502, length 12
5 172.20.42.84.59032 > 20.30.3.101.502: Flags [P.], seq 1:13, ack 1, win 502, length 12
6 172.20.42.84.59032 > 20.30.3.101.502: Flags [P.], seq 1:13, ack 1, win 502, length 12
7 172.20.42.84.59032 > 20.30.3.101.502: Flags [P.], seq 1:13, ack 1, win 502, length 12
8 172.20.42.84.59032 > 20.30.3.101.502: Flags [F.], seq 13, ack 1, win 502, length 0
9 20.30.3.101.502 > 172.20.42.84.59032: Flags [S.], seq 1013872532, ack 4013763010, win 14480, length 0
10 172.20.42.84.59032 > 20.30.3.101.502: Flags [.], ack 1, win 502, length 0
11 172.20.42.84.59032 > 20.30.3.101.502: Flags [FP.], seq 1:13, ack 1, win 502, length 12
12 172.20.42.84.59032 > 20.30.3.101.502: Flags [FP.], seq 1:13, ack 1, win 502, length 12
13 20.30.3.101.502 > 172.20.42.84.59032: Flags [S.], seq 1013872532, ack 4013763010, win 14480, length 0
14 172.20.42.84.59032 > 20.30.3.101.502: Flags [.], ack 1, win 502, length 0
15 172.20.42.84.59032 > 20.30.3.101.502: Flags [FP.], seq 1:13, ack 1, win 502, length 12
16 20.30.3.101.502 > 172.20.42.84.59032: Flags [S.], seq 1013872532, ack 4013763010, win 14480, length 0
17 172.20.42.84.59032 > 20.30.3.101.502: Flags [.], ack 1, win 502, length 0
18 172.20.42.84.59032 > 20.30.3.101.502: Flags [FP.], seq 1:13, ack 1, win 502, length 12
19 20.30.3.101.502 > 172.20.42.84.59032: Flags [S.], seq 1013872532, ack 4013763010, win 14480, length 0
20 172.20.42.84.59032 > 20.30.3.101.502: Flags [.], ack 1, win 502, length 0
21 172.20.42.84.59032 > 20.30.3.101.502: Flags [FP.], seq 1:13, ack 1, win 502, length 12
22 172.20.42.84.59032 > 20.30.3.101.502: Flags [FP.], seq 1:13, ack 1, win 502, length 12
服务端视角的问题现象与分析
服务端执行 netstat -nat
命令,可以看到大量连接停留在 SYN_RECV
状态。这印证了从客户端视角观察到的现象:
- 收到 SYN,发出 SYN+ACK:服务端成功接收到客户端发来的初始 SYN 报文(报文 1)。
- 进入 SYN_RECV 状态:服务端回复 SYN+ACK 报文(报文 2)后,进入
SYN_RECV
状态,等待客户端的最终 ACK 报文(报文 3)以完成握手。
[root@server ~]$ netstat -nat
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 6 0 0.0.0.0:502 0.0.0.0:* LISTEN
tcp 0 0 20.30.3.101:502 20.30.10.5:1456 SYN_RECV
tcp 0 0 20.30.3.101:502 20.30.10.5:48129 SYN_RECV
tcp 0 0 20.30.3.101:502 20.30.10.5:23670 SYN_RECV
tcp 0 0 20.30.3.101:502 20.30.10.5:54627 SYN_RECV
tcp 0 0 20.30.3.101:502 20.30.10.5:54803 SYN_RECV
tcp 0 0 20.30.3.101:502 20.30.10.5:54483 SYN_RECV
tcp 0 0 20.30.3.101:502 20.30.10.5:29119 SYN_RECV
tcp 0 0 20.30.3.101:502 20.30.10.5:23102 SYN_RECV
tcp 13 0 20.30.3.101:502 20.30.10.5:64107 CLOSE_WAIT
tcp 13 0 20.30.3.101:502 20.30.10.5:12805 CLOSE_WAIT
tcp 13 0 20.30.3.101:502 20.30.10.5:1932 CLOSE_WAIT
tcp 13 0 20.30.3.101:502 20.30.10.5:43151 CLOSE_WAIT
tcp 13 0 20.30.3.101:502 20.30.10.5:8611 CLOSE_WAIT
tcp 13 0 20.30.3.101:502 20.30.10.5:42900 CLOSE_WAIT
表面上看,SYN_RECV
状态意味着服务端未收到客户端针对 SYN+ACK 回复的 ACK 报文(即三次握手中的第三个报文)。然而,结合网络抓包和服务端内核网络统计(/proc/net/netstat
)进行深入分析,揭示了更深层次的原因:
TCP 服务端维护两个关键队列:
- 半连接队列 (SYN Queue):存储已收到 SYN 并发出 SYN+ACK 的连接,这些连接处于
SYN_RECV
状态,等待最终的 ACK。 - 全连接队列 (Accept Queue):存储已完成三次握手的连接,这些连接处于
ESTABLISHED
状态,等待应用程序通过accept()
系统调用来取走。
在本案例中,抓包确认客户端的 ACK 报文(报文 3)实际上已经到达了服务端。但此时,服务端的全连接队列已满。根据 TCP 协议栈的处理逻辑(具体行为可能因 tcp_abort_on_overflow
参数配置而异),当收到 ACK 且全连接队列满时,服务端会丢弃这个 ACK 报文。
其后果是:
- 该连接无法从半连接队列转移到全连接队列,因此连接状态持续停留在
SYN_RECV
。 - 由于服务端认为握手未完成,它会继续重传 SYN+ACK 报文,试图完成握手。
- 客户端后续发送的数据或 FIN 报文,对于处于
SYN_RECV
状态的服务端来说是无效的,会被忽略。
服务端 /proc/net/netstat
的统计数据为此提供了直接证据:
[root@server ~]$ cat /proc/net/netstat
TcpExt: ListenOverflows ListenDrops
TcpExt: 657727 661488
ListenOverflows
计数器显著增加:这直接表明了当客户端的最终 ACK 到达时,服务端的全连接队列 (Accept Queue) 已满,导致内核无法将完成握手的连接放入此队列中等待应用程序accept()
。这是造成 ACK 被丢弃和连接停留在SYN_RECV
状态的根本原因。ListenDrops
计数器也显著增加:此计数器在两种主要情况下会增加:1) 半连接队列 (SYN Queue) 满导致 SYN 包被丢弃(若未开启syncookies
);2) 全连接队列 (Accept Queue) 满导致最终 ACK 包被丢弃。在本案例中,鉴于ListenOverflows
同时急剧增长,可以推断出ListenDrops
的增加主要也是由全连接队列溢出引起的,尽管不能完全排除半连接队列也存在压力的可能性。
解决方案与调优建议
针对全连接队列溢出导致的 TCP 幽灵连接问题,主要可以通过以下几个方面进行优化:
- 增大全连接队列容量,使其能够容纳更多连接等待被
accpet()
- 提升应用程序
accpet()
的速率,优化业务逻辑处理每个连接所需的时间 - 调整
tcp_abort_on_overflow
参数,置 1 可以向客户端发送 RST 包,终止该连接
需要注意的是,在物联网场景中,服务端设备资源往往受限。在这种情况下,单纯依靠服务端调优可能效果有限或难以实现。因此,也需要结合客户端策略,例如实施连接频率控制、采用指数退避的重连机制等,以主动适应服务端的处理能力,从源头减轻其连接建立的压力。