Skip to content

案例: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 -s
text
Total: 68412
TCP:   68234 (estab 128, closed 68000, orphaned 0, timewait 67904)
bash
ss -tan state time-wait | head -3
text
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:8080

Local 是随机高位端口,Peer 是固定的 8080 —— 本机是客户端,在主动关闭大量短连接。

bash
ss -tan state time-wait | awk '{print $5}' | cut -d: -f1 | sort | uniq -c | sort -rn | head
text
  67904 10.0.0.51

全指向同一个下游。

bash
ss -tan state time-wait | awk -F'[: ]+' '{print $5}' | sort | uniq -c | sort -rn | head -5
text
  41230 8080
  26104 3306

第二步:确认端口范围已经用尽 ​

bash
cat /proc/sys/net/ipv4/ip_local_port_range
text
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_buckets
text
net.ipv4.tcp_tw_reuse = 2
net.ipv4.tcp_max_tw_buckets = 262144

tcp_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: 3000
java
// 每个请求 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=30
bash
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

根治 ​

  1. 连接池 + 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();
  1. Spring Boot 3 用 RestClient/WebClient 或配置 httpclient 连接池,不要每次 new。

  2. 下游如果是 Nginx,确认 keepalive_timeout 足够:

ini
keepalive_timeout 65;
  1. 数据库连接也同理 —— 用 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-aliveNginx 默认 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。