在物联网(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 状态。这印证了从客户端视角观察到的现象:

  1. 收到 SYN,发出 SYN+ACK:服务端成功接收到客户端发来的初始 SYN 报文(报文 1)。
  2. 进入 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 报文。

其后果是:

  1. 该连接无法从半连接队列转移到全连接队列,因此连接状态持续停留在 SYN_RECV
  2. 由于服务端认为握手未完成,它会继续重传 SYN+ACK 报文,试图完成握手。
  3. 客户端后续发送的数据或 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 半连接队列与全连接队列

解决方案与调优建议

针对全连接队列溢出导致的 TCP 幽灵连接问题,主要可以通过以下几个方面进行优化:

  1. 增大全连接队列容量,使其能够容纳更多连接等待被 accpet()
  2. 提升应用程序 accpet() 的速率,优化业务逻辑处理每个连接所需的时间
  3. 调整 tcp_abort_on_overflow 参数,置 1 可以向客户端发送 RST 包,终止该连接

需要注意的是,在物联网场景中,服务端设备资源往往受限。在这种情况下,单纯依靠服务端调优可能效果有限或难以实现。因此,也需要结合客户端策略,例如实施连接频率控制、采用指数退避的重连机制等,以主动适应服务端的处理能力,从源头减轻其连接建立的压力。