TCP连接数过多怎么办?TIME_WAIT、CLOSE_WAIT状态分析

服务器监控突然报警:连接数暴涨到几万。你登上机器 ss -ant 一看,满屏的 TIME_WAIT,或者满屏的 CLOSE_WAIT。这两种状态看着像,处理方向却完全不同——一个是协议的正常产物、需要疏导,另一个几乎百分之百是应用代码的 bug。

这篇文章把 TIME_WAIT 和 CLOSE_WAIT 的成因讲透,给出定位命令,分别给出处理方案,并说明哪些内核参数值得调、哪些早该废弃。最后给一套"看到连接数高先做什么"的排查顺序,让你不慌、不瞎调参数。

一、先分清两个状态是谁产生的

回忆 TCP 四次挥手:主动关闭的一方最后会进入 TIME_WAIT,被动关闭的一方在收到 FIN 但应用还没调用 close 时处于 CLOSE_WAIT。

记住一句话就够了:

  • TIME_WAIT 出现在主动关闭方。谁先发起关闭,谁就有 TIME_WAIT。它是协议规定的正常状态,持续 2MSL(通常 60 秒)。
  • CLOSE_WAIT 出现在被动关闭方。它意味着"对方已经关了,我这边还没关"。持续多久,取决于应用什么时候真正释放套接字。

先搞清楚你的服务在这段连接里是主动方还是被动方,问题就解决了一半。比如你的服务作为客户端去调第三方接口,连接是你发起的,收起连接自然也是你主动,TIME_WAIT 就落在你身上。

二、TIME_WAIT 为什么存在,为什么是 2MSL

TIME_WAIT 不是 bug,它有两个必须存在的原因:

第一,保证最后那个 ACK 能到达。主动关闭方发完最后的 ACK 后,如果这个 ACK 丢了,被动方会重发 FIN。此时主动方还处于 TIME_WAIT,才能重新响应这个 FIN。如果立刻关闭,被动方会收到 RST,连接异常终止。

第二,让旧连接的数据包在网络中消散。同一组四元组(源 IP、源端口、目的 IP、目的端口)如果在短时间内被复用建立新连接,上一次连接延迟的旧数据包可能被误认为新连接的数据。等 2MSL(报文最大生存时间的两倍)后,旧包基本已消亡。

所以 TIME_WAIT 是"用一点资源换连接的干净和正确",本身是好事。问题只在于:短连接太多时,TIME_WAIT 会积累到占用大量本地端口,导致新连接建不出来。

三、TIME_WAIT 过多怎么处理

第一步:确认量级和端口占用

# 统计 TIME_WAIT 数量
ss -ant | grep -c TIME_WAIT

# 按状态整体统计
ss -ant | awk 'NR>1 {print $1}' | sort | uniq -c | sort -rn

# 查看本地端口范围(可用的临时端口数量)
cat /proc/sys/net/ipv4/ip_local_port_range

# 看 TIME_WAIT 是否集中在某些目标(找出高频短连接的对端)
ss -ant | awk '/TIME-WAIT/ {print $4}' | awk -F: '{print $1}' | sort | uniq -c | sort -rn | head

经验参考:ip_local_port_range 默认常见是 32768–60999,约 2.8 万个临时端口。如果 TIME_WAIT 数量接近这个量级且持续增长,就可能影响新连接的建立。

第二步:优先从架构上减少短连接

调内核参数是下策,真正的解法是少产生短连接:

  • 改用长连接 / 连接池。HTTP 客户端启用 keep-alive,数据库用连接池(如 HikariCP、各类连接池默认配置),不要每次请求都新建连接。
  • 反向代理/网关统一管理连接。让 Nginx、网关层维持到后端的连接池,应用侧不再频繁建连拆连。
  • 健康检查间隔调大。负载均衡频繁做短连接健康检查,也会制造大量 TIME_WAIT。

一个没配连接池的应用,QPS 高一点就能产生每秒几千个短连接,TIME_WAIT 必然爆炸。把连接池配上,往往直接解决问题。

第三步:必要时调整内核参数

如果架构一时改不了,可以配合内核参数缓解。编辑 /etc/sysctl.d/99-tcp.conf(或 /etc/sysctl.conf)加入:

# 允许复用处于 TIME_WAIT 的连接(仅对主动发起连接的一方有效,且有安全前提)
net.ipv4.tcp_tw_reuse = 1

# 扩大本地临时端口范围,增加可用端口数量
net.ipv4.ip_local_port_range = 10000 65000

# 让 TIME_WAIT 套接字可被快速回收用于新的出站连接
net.ipv4.tcp_max_tw_buckets = 20000

# 加快 FIN_WAIT_2 超时回收,避免长时间挂住
net.ipv4.tcp_fin_timeout = 15
# 应用配置
sudo sysctl -p /etc/sysctl.d/99-tcp.conf
# 或直接生效
sudo sysctl --system

风险提示:tcp_tw_reuse 有适用前提(主要用于客户端主动连接、且需要时间戳支持),对服务端被动连接意义有限。绝对不要开启已废弃的 net.ipv4.tcp_tw_recycle,它在 NAT 环境下会导致大量连接异常,新内核已移除。tcp_max_tw_buckets 设太小会让内核直接丢弃 TIME_WAIT 并打日志,反而增加风险。改参数前记录原值,出问题可 sysctl -w 改回。

关于 tcp_tw_reuse 与 tcp_tw_recycle 的区别,务必分清:

参数 作用 是否推荐
tcp_tw_reuse 允许复用 TIME_WAIT 连接(主要客户端场景) 可用(谨慎)
tcp_tw_recycle 快速回收 TIME_WAIT 强烈不推荐,NAT 下会出故障,新内核已删除
ip_local_port_range 扩大可用端口数 推荐
tcp_fin_timeout 缩短 FIN_WAIT_2 超时 可适度调小

四、CLOSE_WAIT 过多:基本是应用 bug

CLOSE_WAIT 的语义是"我收到了对方的 FIN,回了 ACK,但我的应用还没调用 close()"。它停留的时间完全由应用控制——如果应用永远不 close,它就一直挂着。

所以大量 CLOSE_WAIT 的含义非常明确:应用没有正确释放连接。常见原因:

  • 代码里拿到连接/套接字后,在异常分支、超时分支里没有 close()。
  • 连接池配置不当,或自己实现的连接管理有泄漏。
  • 读到了 EOF 却没有关闭,误以为对方会再发数据。
  • 长时间阻塞在某个操作上,迟迟没走到关闭逻辑。

CLOSE_WAIT 会累积,不像 TIME_WAIT 会自动消失。它耗的是文件描述符和内存,最终可能把进程的 fd 耗尽,报 Too many open files。

定位是哪个进程

# 列出所有 CLOSE_WAIT 连接及对应进程(需要 root)
sudo ss -antp | grep CLOSE_WAIT

# 统计每个进程的 CLOSE_WAIT 数量,快速找出元凶
sudo ss -antp | grep CLOSE_WAIT | awk '{print $NF}' | sort | uniq -c | sort -rn | head

# 或者按 PID 聚合
sudo ss -antp | awk '/CLOSE-WAIT/ {for(i=1;i<=NF;i++) if($i ~ /pid=/) print $i}' | sort | uniq -c | sort -rn

# 看该进程打开的文件描述符数量
sudo ls /proc//fd | wc -l
sudo lsof -p  | grep -c 'CLOSE_WAIT\|sock'

定位到进程后,看它打开了多少 fd、连接的对端是谁,就能对上代码或组件。如果是第三方组件(比如某个没配好的客户端库),升级或改配置;如果是自己写的代码,去代码里找没有 close 的分支。

修复方向

  • 保证所有取到连接的路径最终都会释放(用 try-finally / defer / with 等机制)。
  • 配置连接池,由池统一管理生命周期和最大连接数。
  • 设置合理的读超时和空闲回收,避免连接长时间挂着。
  • 对端关闭(读到 EOF)时立即关闭本端套接字。

CLOSE_WAIT 基本不需要调内核参数,调了也治标不治本——参数治不了代码泄漏。

五、其他高连接状态速查

除了这两个,还有几个状态堆积也值得关注:

状态 出现方 含义 堆积说明
SYN_RECV 服务端 已回 SYN+ACK 等 ACK 可能是 SYN Flood,或队列满
FIN_WAIT_2 主动关闭方 已发 FIN 收到 ACK,等对方 FIN 对方迟迟不关,或应用未处理
LAST_ACK 被动关闭方 已发 FIN 等最后 ACK 少见,通常是短暂状态
ESTABLISHED 双方 连接已建立 正常连接数,看水位而非堆积

快速看全貌:

ss -ant | awk 'NR>1 {print $1}' | sort | uniq -c | sort -rn
# 看内核层面统计(是否因队列满而丢弃)
nstat -az | grep -E 'TcpExtListenOverflows|TcpExtListenDrops|TcpExtTCPBacklogDrop'

六、排查顺序建议

看到"连接数过多"报警,按这个顺序走:

  1. 先分类:ss -ant | awk '{print $1}' | sort | uniq -c,看是 TIME_WAIT、CLOSE_WAIT 还是别的。
  2. TIME_WAIT 多 → 确认是不是短连接业务;优先上连接池/长连接;再考虑调端口范围和 tcp_tw_reuse。
  3. CLOSE_WAIT 多 → 立刻定位进程,这是泄漏,改代码或改连接池配置。
  4. SYN_RECV 多 → 查是否被扫描/攻击,检查 SYN 队列和 tcp_max_syn_backlog。
  5. ESTABLISHED 过高 → 是业务量真上来了,考虑扩容或限流,而不是当作异常。

常见问题

Q:TIME_WAIT 多到底要不要处理?
A:如果没影响新连接建立、端口没耗尽、业务正常,可以不管。只有占用端口明显、导致 Cannot assign requested address 这类错误时才需要处理。

Q:调 tcp_tw_reuse 有风险吗?
A:它只影响主动发起连接的场景,且需时间戳支持,一般风险可控。但仍建议先确认业务是对外主动连接的一方。tcp_tw_recycle 则绝对不要用。

Q:CLOSE_WAIT 会自己消失吗?
A:不会,除非应用调用 close 或进程退出。它属于泄漏,必须从应用侧修。

Q:连接数到多少算危险?
A:没有绝对值,取决于内存、fd 限制和业务。看 ulimit -n、fs.file-max、可用内存。经验上,单个进程 fd 用掉上限的七八成就该关注了。

Q:ss 和 netstat 该用哪个?
A:优先用 ss。连接数很大时 netstat 慢得多,ss 直接读内核,快很多。老系统上 netstat 属于 net-tools,新系统可能默认不装。

总结

  • TIME_WAIT 出现在主动关闭方,持续 2MSL,是协议正常行为;过多会占端口,解法优先是长连接/连接池,其次调 ip_local_port_range、谨慎开 tcp_tw_reuse,绝不用 tcp_tw_recycle。
  • CLOSE_WAIT 出现在被动关闭方,意味着应用没 close,几乎一定是代码或连接池问题,会累积,只能靠改代码/配置解决。
  • 定位用 ss -antp | grep CLOSE_WAIT 并聚合到进程,再用 /proc//fd 看 fd 数量。
  • 排查顺序:先分类状态 → TIME_WAIT 疏导 → CLOSE_WAIT 修代码 → SYN_RECV 查攻击 → ESTABLISHED 看业务水位。
  • 改内核参数或防火墙前先备份原值/规则,在低峰期操作,避免把性能问题变成可用性事故。