外观
案例:TIME_WAIT 堆积与端口耗尽
现象
订单服务调用下游时偶发失败:
text
java.net.BindException: Address already in use
java.net.NoRouteToHostException
Caused by: java.net.SocketException: Can't assign requested address同时 ss -s 显示 TIME_WAIT 六万多。
定位过程
第一步:确认方向
bash
ss -stext
Total: 68412
TCP: 68234 (estab 128, closed 68000, orphaned 0, timewait 67904)bash
ss -tan state time-wait | head -3text
State Recv-Q Send-Q Local Address:Port Peer Address:Port
TIME-WAIT 0 0 10.0.0.41:51244 10.0.0.51:8080
TIME-WAIT 0 0 10.0.0.41:51246 10.0.0.51:8080
TIME-WAIT 0 0 10.0.0.41:51248 10.0.0.51:8080Local 是随机高位端口,Peer 是固定的 8080 —— 本机是客户端,在主动关闭大量短连接。
bash
ss -tan state time-wait | awk '{print $5}' | cut -d: -f1 | sort | uniq -c | sort -rn | headtext
67904 10.0.0.51全指向同一个下游。
bash
ss -tan state time-wait | awk -F'[: ]+' '{print $5}' | sort | uniq -c | sort -rn | head -5text
41230 8080
26104 3306第二步:确认端口范围已经用尽
bash
cat /proc/sys/net/ipv4/ip_local_port_rangetext
32768 60999可用端口约 2.8 万个。TIME_WAIT 6.7 万个,其中 4.1 万是 8080——同一目标 + 同一源 IP 的可用端口组合已经耗尽。
TCP 四元组是 (源IP, 源端口, 目的IP, 目的端口)。目的 IP/端口固定时,只能靠源端口区分,最多 2.8 万个并发(TIME_WAIT 还要占 60 秒)。
bash
sysctl net.ipv4.tcp_tw_reuse net.ipv4.tcp_max_tw_bucketstext
net.ipv4.tcp_tw_reuse = 2
net.ipv4.tcp_max_tw_buckets = 262144tcp_tw_reuse=2(允许 loopback 复用)对远程连接无效。
第三步:为什么全是短连接
bash
grep -c 'HttpClient' /var/log/order-api/app.log翻应用日志和配置:
yaml
# 修复前
order.client:
url: http://10.0.0.51:8080
connect-timeout: 1000
read-timeout: 3000java
// 每个请求 new 一个 client
CloseableHttpClient client = HttpClients.createDefault();
try (CloseableHttpResponse resp = client.execute(new HttpGet(url))) {
...
}每次调用都新建连接、用完关闭 —— 主动关闭方自然堆积 TIME_WAIT。
处置
立即
bash
# 1. 扩大端口范围(治标)
sysctl -w net.ipv4.ip_local_port_range="10000 65000"
# 2. 允许复用 TIME_WAIT(只对客户端有效)
sysctl -w net.ipv4.tcp_tw_reuse=1
sysctl -w net.ipv4.tcp_fin_timeout=30bash
cat > /etc/sysctl.d/99-net.conf <<'EOF'
net.ipv4.ip_local_port_range = 10000 65000
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 30
net.ipv4.tcp_max_tw_buckets = 262144
EOF
sysctl -p /etc/sysctl.d/99-net.conf根治
- 连接池 + keep-alive:
java
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
cm.setMaxTotal(200);
cm.setDefaultMaxPerRoute(50);
CloseableHttpClient client = HttpClients.custom()
.setConnectionManager(cm)
.setKeepAliveStrategy((response, context) -> 30_000)
.build();Spring Boot 3 用
RestClient/WebClient或配置httpclient连接池,不要每次 new。下游如果是 Nginx,确认
keepalive_timeout足够:
ini
keepalive_timeout 65;- 数据库连接也同理 —— 用 HikariCP,不要每次
DriverManager.getConnection()。
验证
bash
watch -n2 'ss -s | grep TCP'text
TCP: 1284 (estab 342, closed 900, orphaned 0, timewait 412)TIME_WAIT 从 6.7 万降到 400 左右。
复盘
| 问题 | 根因 | 改进 |
|---|---|---|
| 端口耗尽 | 短连接 + 主动关闭 | 连接池 + keep-alive |
| 只监控了连接数 | 没看 TIME_WAIT 趋势 | 加 ss -s 指标 |
| 扩端口掩盖了问题 | 治标 | 同时推连接池改造 |
| 下游未开 keep-alive | Nginx 默认 75s,但客户端每次都新建 | 客户端复用 |
bash
# TIME_WAIT 告警
ss -tan state time-wait | wc -l超过 20000 就告警,别等到 Can't assign requested address。
关于 tcp_tw_recycle
老文章常推荐 net.ipv4.tcp_tw_recycle=1。这个参数在 NAT 环境下会导致同一 NAT 后不同客户端连接失败,且已在 Linux 4.12 被移除。
不要用。正确做法是连接池 + tcp_tw_reuse(仅客户端)。
判断谁是主动关闭方
ss -tan state time-wait 里:
Local是随机高位端口 → 本机是客户端,主动关闭。Local是服务端口(80/8080) → 本机是服务端,被客户端先关了。
两者的优化方向完全不同:客户端用连接池,服务端调 keepalive_timeout。