TIME_WAIT 过多排查实战:从端口耗尽到连接复用优化

高并发服务偶发 “Cannot assign requested address”,或者新建连接莫名卡顿、超时?罪魁往往不是 CPU 也不是内存,而是 TCP 的 TIME_WAIT 状态悄悄堆积。当一台机器在短时间内主动关闭大量短连接,本地端口会被 TIME_WAIT 占满,导致新连接拿不到可用端口——这就是典型的 TIME_WAIT 过多引发的端口耗尽。本文从现象到根因,结合 ss/netstat 诊断与内核参数、应用层 连接复用 优化,给出一套可直接操作的治理方案。

一、现象:怎么发现 TIME_WAIT 过多

TIME_WAIT 是 TCP 主动关闭方在发送最后一个 ACK 后进入的状态,会停留 2MSL(通常 60 秒)。它本身是正常的,但当数量级达到几万甚至十几万时,问题就来了。最直接的症状是应用日志里出现地址分配失败,或压测时连接数上不去。

# 按状态统计所有 TCP 连接
ss -ant | awk '{print $1}' | sort | uniq -c

# 只看 TIME_WAIT 的数量
ss -ant state time-wait | wc -l

# 内核视角的 TCP 内存与 TIME_WAIT 计数
cat /proc/net/sockstat

如果 TIME-WAIT 那一行轻松过万,且还在持续增长,基本可以判定存在端口耗尽风险。更关键的是看 /proc/net/sockstat 里的 TW 字段与 TCP: inuse 占用,它能告诉你内核当前真实的连接压力。

指标查看方式危险信号
TIME_WAIT 总数ss -ant state time-wait | wc -l持续 > 3 万且增长
可用本地端口范围sysctl net.ipv4.ip_local_port_range范围过小(如默认 32768–60999)
TCP 内存占用cat /proc/net/sockstatTCP: inuse 接近系统上限
最大 TW 桶数sysctl net.ipv4.tcp_max_tw_buckets大量 TW 被强制回收丢包

二、原理:TIME_WAIT 为什么必须存在

很多人的第一反应是“能不能直接关掉 TIME_WAIT”。答案是不能,也不该。它被设计出来有两个核心目的:

  • 保证最后一个 ACK 可靠到达:如果被动关闭方没收到这个 ACK,会重发 FIN,主动方需要靠 TIME_WAIT 里的控制块来回应重传。
  • 让旧连接的残留报文在网络中消逝:2MSL 时长确保同一四元组(源 IP、源端口、目的 IP、目的端口)上的迟到报文不会串到新连接上。

所以 TIME_WAIT 不是 bug,而是 TCP 可靠性的代价。我们能做的不是消灭它,而是减少它产生的速度,以及让端口更快被复用。

三、根因:为什么会堆积如山

同一个现象,根因往往集中在以下几类:

  • 短连接风暴:HTTP 短连接、每次请求新建并关闭 socket,QPS 越高 TW 增长越快。
  • 本机是主动关闭方:谁先调用 close(),谁就进入 TIME_WAIT。客户端、或反向代理向后端转发时主动断开,都会让代理机自己背上 TW。
  • NAT / 负载均衡场景:出口 IP 固定、端口空间有限,TW 极易占满可用端口。
  • 端口范围与桶数配置偏小:默认值在高并发下根本不够用。

四、诊断清单:三步定位主动关闭方

动手优化前,先用下面三步确认“是不是我主动关的、是不是端口真的不够”:

# 1. 各状态数量一览
ss -ant | awk '{print $1}' | sort | uniq -c

# 2. 确认本地端口范围(可用端口 = 上限-下限)
sysctl net.ipv4.ip_local_port_range
# net.ipv4.ip_local_port_range = 32768    60999  => 仅约 2.8 万个

# 3. 查看 TW 桶上限与当前是否在被强行回收
sysctl net.ipv4.tcp_max_tw_buckets
cat /proc/net/sockstat

如果 ip_local_port_range 还是默认的小范围,且 TW 数量逼近 tcp_max_tw_buckets,那么新连接拿不到端口就是必然。此时应优先扩大端口范围与桶数,而不是盲目缩短 TIME_WAIT。

五、优化方案一:内核参数调优

在主动关闭方(如反向代理、压测客户端)上,合理的 sysctl 配置能显著缓解端口压力。把下面内容写入 /etc/sysctl.d/99-timewait.conf 后执行 sysctl --system:

# 缩短 FIN-WAIT-2 超时(秒)
net.ipv4.tcp_fin_timeout = 30

# 允许对【出向】连接复用 TIME_WAIT  socket(需开启时间戳)
net.ipv4.tcp_tw_reuse = 1

# 提高 TIME_WAIT 桶上限,避免被强制回收导致丢包
net.ipv4.tcp_max_tw_buckets = 262144

# 扩大本地端口范围
net.ipv4.ip_local_port_range = 1024 65535

关于 tcp_tw_reuse:它只对主动发起的新的出向连接生效,且依赖 tcp_timestamps(默认开启),相对安全,推荐开启。而老的 tcp_tw_recycle 在 Linux 4.12 之后已被彻底移除,早期资料让你开启它来“快速回收”,在 NAT 环境下会直接丢弃合法报文、造成大面积连接失败,切勿使用。

参数作用风险 / 注意
tcp_fin_timeout缩短 FIN-WAIT-2 停留过小可能导致正常 FIN 未完成
tcp_tw_reuse出向连接复用 TW socket仅出向、需 timestamps;安全
tcp_tw_recycle(旧)快速回收已移除;NAT 下致命,禁用
tcp_max_tw_buckets提高 TW 上限设过大略增内存,通常无碍
ip_local_port_range扩大可用端口优先调大,性价比最高

六、优化方案二:应用层连接复用

内核调优是“治标”,真正治本的是减少连接的新建与关闭次数。最有效的手段是长连接 + 连接池。

6.1 服务端开启 SO_REUSEADDR

重启服务时若端口仍被 TIME_WAIT 占用,绑定会失败。SO_REUSEADDR 允许在时间窗口内复用处于 TIME_WAIT 的本地地址,避免“Address already in use”:

import socket

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(("0.0.0.0", 8080))
s.listen(128)

多进程/多线程监听同一端口时,进一步使用 SO_REUSEPORT 让内核做负载均衡,既避免端口争用,又提升吞吐。

6.2 反向代理开启 keep-alive

如果 Nginx 反向代理到后端每次都新建短连接,代理机自身就会积累海量 TIME_WAIT。开启到后端的 keep-alive 复用,能把 TW 数量降一到两个数量级:

upstream api_backend {
    server 10.0.0.10:8080;
    keepalive 32;          # 代理机到后端的空闲长连接池
}

server {
    location / {
        proxy_pass http://api_backend;
        proxy_http_version 1.1;        # 必须 1.1 才能复用
        proxy_set_header Connection ""; # 清空客户端的 Connection 头
    }
}

应用代码侧同理:数据库连接、Redis、HTTP Client 都应复用连接池,而不是每次请求现建现关。容器化部署时,把这一配置固化进镜像与编排文件更稳妥,参见我们的 Docker 编排实践 与 Nginx 反向代理完整配置。

七、优化方案三:架构与角色调整

  • 让对端来主动关闭:如果无法避免短连接,尽量让客户端(而不是你的服务)做主动关闭方,把 TIME_WAIT 转移到客户端机器上。
  • 前置 LB 卸载连接:在 L4/L7 负载均衡处终结客户端连接,内网用长连接转发,避免每台后端都直面海量 TW。
  • 使用长连接协议:gRPC、WebSocket、HTTP/2 天然多路复用,从根本上消灭大量短连接。

八、实战案例:压测把端口打满

用 wrk 以短连接高并发压测一个只支持短连接的接口,很快就能复现端口耗尽:

# 1000 并发、30 秒、默认短连接
wrk -t4 -c1000 -d30s http://127.0.0.1:8080/

# 观察本机 TIME_WAIT 是否飙升
watch -n1 'ss -ant state time-wait | wc -l'

复现后,先 sysctl 调大端口范围与 tcp_tw_reuse,再在应用侧开启连接池 / keep-alive,重新压测,TW 数量通常会回落一个数量级,且 “Cannot assign requested address” 消失。排查“连接数满”类问题时,思路与 MySQL 连接数满排查、文件描述符耗尽排查 一脉相承:先量化、再定位主动方、最后分层治理。

九、落地清单与避坑

步骤动作预期效果
1ss -ant state time-wait | wc -l 量化确认 TW 量级与增长趋势
2确认本机是主动关闭方明确优化对象(客户端/代理)
3扩大 ip_local_port_range立竿见影,性价比最高
4开启 tcp_tw_reuse=1出向连接复用 TW
5应用层接入连接池 / keep-alive从根上减少新建连接
6监控告警 TW 与端口使用率提前发现,避免线上雪崩

避坑提醒:不要开启已废弃的 tcp_tw_recycle;不要把 tcp_fin_timeout 调得过小导致正常连接被掐断;长连接也要配合理的空闲超时,否则另一头又会堆积空闲连接。把端口范围、TW 复用、连接池三件事一起做,效果最佳。

十、小结

TIME_WAIT 过多本质是“主动关闭方 + 短连接 + 有限端口”三者的叠加。排查时先用 ss 量化、确认主动关闭方;优化时分层推进——扩大端口范围与 tcp_tw_reuse 是快速止血,连接池与长连接才是治本。把它和 tcp_fin_timeout、SO_REUSEADDR 组合使用,配合监控告警,就能让高并发服务彻底摆脱端口耗尽的梦魇。

上一篇 Java 动态代理实战:JDK 与 CGLIB 对比
下一篇 Restic 实战:用命令行打造加密增量备份
最新评论
Docker 网络模式详解:bridge/host 生产选型 - FSData
Docker 网络模式详解:bridge/host 生产选型 - FSData
8月20日
[…] 注意最后那条:把端口绑定到 127.0.0.1,再由宿主机上的 Nginx 反代进来,比直接 -p 80:80 暴露安全得多,也方便统一做
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
8月20日
[…] Pinia 的工程化用法,可以参考我们之前的实战文章 《Vue3 组合式 API + Pinia:状态管理从入门到实战》,那里覆盖了 Sto
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
8月19日
[…] 状态,点一下就能起停容器,配合我们讲过的 Docker Compose 编排 […]
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
8月19日
[…] 缓存三大坑的本质都是”缓存失效导致请求穿透到数据库”。记住口诀:穿透用布隆、击穿用锁、雪崩用错峰。配合合理的过期策略
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
8月19日
[…] Docker 镜像部署。结合我们之前写的 Docker Compose 一键编排实战,可以在 CI […]