高并发服务偶发 “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/sockstat | TCP: 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 连接数满排查、文件描述符耗尽排查 一脉相承:先量化、再定位主动方、最后分层治理。
九、落地清单与避坑
| 步骤 | 动作 | 预期效果 |
|---|---|---|
| 1 | ss -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 组合使用,配合监控告警,就能让高并发服务彻底摆脱端口耗尽的梦魇。




