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 是我在生产环境最头疼的状态,没有之一。
它的产生过程是这样的:
- 客户端发送 FIN,要求关闭连接
- 服务端内核收到 FIN,回复 ACK
- 服务端进入 CLOSE_WAIT 状态
- 服务端应用层需要调用
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 状态转换的核心路径。我建议你把它保存下来,遇到问题对着看。
3.7 实战:如何快速定位端口状态问题
最后,分享一套我自己的排查流程。遇到端口相关的问题,按这个顺序查:
- 先看总量:
ss -ant | wc -l,确认连接总数是否异常 - 再看分布:
ss -ant | awk '{print $1}' | sort | uniq -c,看哪个状态最多 - 定位进程:如果 CLOSE_WAIT 多,用
lsof -i找到对应进程 PID - 检查代码:找到进程后,看代码中 socket 的关闭逻辑
- 临时缓解:如果 TIME_WAIT 太多,可以调整内核参数;如果 CLOSE_WAIT 太多,只能重启进程
一句话总结:
- LISTENING 没了 → 服务挂了
- ESTABLISHED 太多 → 连接泄漏或攻击
- TIME_WAIT 太多 → 短连接场景,调内核参数
- CLOSE_WAIT 太多 → 代码有 bug,赶紧修
端口状态分析,说白了就是看 TCP 连接「卡」在哪一步。你只要记住每个状态的含义和常见原因,排查问题就能快很多。下次线上出问题,别慌,先跑一遍 ss -ant,看看状态分布——答案往往就在那里。