3. 端口状态分析:从 LISTENING 到 CLOSE_WAIT 的完整旅程

端口状态,说白了就是 TCP 连接的生命周期快照。我刚开始做网络开发时,总觉得这些状态就是背背概念。直到有一次线上服务大面积超时,我盯着 netstat 输出看了两个小时——嗯,从那以后我再也不敢小看这些状态了。

这一节,我们就把 TCP 的六个核心状态掰开揉碎。我会结合自己踩过的坑,帮你建立一套「看状态就能定位问题」的本事。

3.1 状态全景图:一次完整连接的一生

先看一张总图。一个 TCP 连接从创建到销毁,会经历这些状态:

核心状态速览(共 11 个状态,生产环境最常见 6 个)

状态 角色 含义 常见时长
LISTENING 服务端 等待客户端连接 持续
SYN_SENT 客户端 已发 SYN,等待 ACK 毫秒级
SYN_RCVD 服务端 收到 SYN,等待 ACK 毫秒级
ESTABLISHED 双方 连接已建立,可传输数据 业务时长
FIN_WAIT_1 主动关闭方 已发 FIN,等待 ACK 毫秒级
FIN_WAIT_2 主动关闭方 收到 ACK,等待对方 FIN 取决于对方
CLOSE_WAIT 被动关闭方 收到 FIN,等待应用关闭 问题高发区
CLOSING 双方 同时关闭,等待 ACK 罕见
LAST_ACK 被动关闭方 已发 FIN,等待最后 ACK 毫秒级
TIME_WAIT 主动关闭方 等待 2MSL,确保旧包消失 1-4 分钟
CLOSED 双方 连接完全关闭 -

你想想看,这 11 个状态里,真正让运维和开发头疼的,其实就四个:LISTENING、ESTABLISHED、TIME_WAIT、CLOSE_WAIT。下面我一个一个讲。

3.2 LISTENING:服务在等谁?

LISTENING 状态表示一个端口已经打开,正在等待客户端连接。服务端进程调用 listen() 后就会进入这个状态。

我的习惯:每次部署新服务,第一件事就是 ss -tlnp | grep :8080,确认 LISTENING 状态存在。如果看不到,说明服务根本没启动成功。

这里有个坑:LISTENING 只出现在服务端。客户端不会 LISTEN,它只会主动发起连接。我曾经见过有人用 netstat -an 看到大量 LISTENING 以为是攻击——其实那是正常的服务端口。

注意:如果 LISTENING 状态的端口突然消失,大概率是进程挂了。如果端口被占用(显示另一个进程的 LISTENING),那就是端口冲突。用 lsof -i :端口号 可以快速定位。

3.3 ESTABLISHED:连接已就绪,但别大意

ESTABLISHED 是唯一一个「正常工作中」的状态。三次握手完成后,双方都进入这个状态,可以收发数据。

我个人习惯用这个命令快速查看连接数:

# 统计各状态连接数
ss -ant | awk '{print $1}' | sort | uniq -c

# 只看 ESTABLISHED 的 IP 分布
ss -ant state established | awk '{print $5}' | cut -d: -f1 | sort | uniq -c | sort -rn

ESTABLISHED 数量异常时,通常意味着:

  • 太多:连接池泄漏,或者遭受了慢速攻击
  • 太少:服务负载低,或者连接被意外关闭

避坑指南:我曾经遇到一个 Java 应用,ESTABLISHED 连接数从 200 慢慢涨到 5000,最后 OOM。查了半天,是 HTTP 连接池没有设置超时时间,每次请求都新建连接但不释放。嗯,从那以后我写代码必配 connectionTimeout 和 maxTotal。

3.4 TIME_WAIT:最让人纠结的状态

TIME_WAIT 是主动关闭连接的一方(通常是客户端)在发送最后一个 ACK 后进入的状态。它会持续 2MSL(Maximum Segment Lifetime),Linux 默认 60 秒,实际等待 60-120 秒。

为什么会这样?因为要确保最后一个 ACK 能被对方收到。如果 ACK 丢了,对方会重发 FIN,这时 TIME_WAIT 状态还能响应。说白了,就是给网络留一个「缓冲期」。

但问题来了:高并发短连接场景下,TIME_WAIT 会堆积。我见过一台机器上有 3 万个 TIME_WAIT 连接,端口号都快用光了。

场景 TIME_WAIT 数量 影响 建议
正常 Web 服务 几百到几千 基本无影响 保持默认
高并发短连接 数万 端口耗尽,新连接失败 开启 tcp_tw_reuse
压测工具 数十万 系统资源紧张 使用长连接或调整 MSL

我的优化经验:如果 TIME_WAIT 太多,可以这样调:

# 允许将 TIME_WAIT 连接用于新连接(客户端有效)
echo 1 > /proc/sys/net/ipv4/tcp_tw_reuse

# 加快 TIME_WAIT 回收(不推荐,可能导致问题)
# echo 1 > /proc/sys/net/ipv4/tcp_tw_recycle  # 已废弃

但注意:tcp_tw_reuse 只对客户端有效。服务端想减少 TIME_WAIT,最好的办法是让客户端主动关闭连接,或者改用长连接。

3.5 CLOSE_WAIT:应用层的「僵尸」状态

CLOSE_WAIT 是我在生产环境最头疼的状态,没有之一。

它的产生过程是这样的:

  1. 客户端发送 FIN,要求关闭连接
  2. 服务端内核收到 FIN,回复 ACK
  3. 服务端进入 CLOSE_WAIT 状态
  4. 服务端应用层需要调用 close() 来真正关闭连接

问题就出在第 4 步。如果应用层不调用 close(),CLOSE_WAIT 就会一直挂着。我见过一个服务 CLOSE_WAIT 超过 1 万个,文件描述符全部耗尽,新连接全部失败。

我曾经踩过的坑:一个 Node.js 服务,用了第三方 HTTP 库,没有监听 close 事件。客户端断开后,服务端不知道要关闭 socket,CLOSE_WAIT 越积越多。最后排查了整整一天,才发现是少写了一行 socket.destroy()。

排查 CLOSE_WAIT 的方法:

# 查看 CLOSE_WAIT 数量
ss -ant state close-wait | wc -l

# 查看具体连接
ss -ant state close-wait

# 结合 lsof 看是哪个进程
lsof -i :端口号 | grep CLOSE_WAIT

修复思路:找到持有 socket 的进程,检查代码中是否有未关闭的 socket 或文件流。常见原因包括:

  • 未捕获的异常导致 close() 没执行
  • 连接池中的连接被客户端关闭后,服务端没有及时清理
  • 反向代理(如 Nginx)与后端之间的连接泄漏

3.6 状态转换图:一张图看懂所有路径

下面这张 SVG 图,画出了 TCP 状态转换的核心路径。我建议你把它保存下来,遇到问题对着看。

TCP 状态转换图(核心路径) TCP 状态转换图(生产环境核心路径) CLOSED LISTENING SYN_RCVD ESTABLISHED CLOSE_WAIT LAST_ACK FIN_WAIT_1 FIN_WAIT_2 TIME_WAIT listen() 收到 SYN 收到 ACK 收到 FIN(被动) close() 收到最后 ACK 主动发 FIN 收到 ACK 收到 FIN 2MSL 超时 说明: 红色节点 = 关闭过程中的关键状态 虚线 = 最终回到 CLOSED 的路径

3.7 实战:如何快速定位端口状态问题

最后,分享一套我自己的排查流程。遇到端口相关的问题,按这个顺序查:

  1. 先看总量:ss -ant | wc -l,确认连接总数是否异常
  2. 再看分布:ss -ant | awk '{print $1}' | sort | uniq -c,看哪个状态最多
  3. 定位进程:如果 CLOSE_WAIT 多,用 lsof -i 找到对应进程 PID
  4. 检查代码:找到进程后,看代码中 socket 的关闭逻辑
  5. 临时缓解:如果 TIME_WAIT 太多,可以调整内核参数;如果 CLOSE_WAIT 太多,只能重启进程

一句话总结:

  • LISTENING 没了 → 服务挂了
  • ESTABLISHED 太多 → 连接泄漏或攻击
  • TIME_WAIT 太多 → 短连接场景,调内核参数
  • CLOSE_WAIT 太多 → 代码有 bug,赶紧修

端口状态分析,说白了就是看 TCP 连接「卡」在哪一步。你只要记住每个状态的含义和常见原因,排查问题就能快很多。下次线上出问题,别慌,先跑一遍 ss -ant,看看状态分布——答案往往就在那里。