适用场景

这类问题常见于网关、爬虫、批处理任务、消息消费者、API 聚合服务等高并发出站访问场景。应用本身没有明显 CPU 或内存瓶颈,但访问下游服务、Redis、MySQL 代理、HTTP API 时偶发超时,重启应用后短时间恢复,过一会儿又开始失败。

临时端口耗尽的核心原因是:本机主动发起大量 TCP 连接,连接关闭后短时间内保留在 TIME_WAIT 等状态,占用了本机可用于出站连接的端口范围。当可用端口不足时,新连接可能出现 Cannot assign requested address、连接超时或请求排队。

现象描述

线上通常会看到下面几类表现:

  • 应用日志里出现 connect timeoutConnection timed outCannot assign requested address
  • 同一台机器上只有高并发出站服务异常,其他服务基本正常。
  • ss 看到大量 TIME-WAITESTABSYN-SENT 连接。
  • 短时间重启服务后恢复,但流量上来后再次复现。
  • 下游服务监控没有明显异常,本机网络、端口或连接池配置更可疑。

典型日志示例:

java.net.BindException: Cannot assign requested address
requests.exceptions.ConnectTimeout: HTTPSConnectionPool(host='api.example.com', port=443): Max retries exceeded
dial tcp 10.10.1.20:443: connect: cannot assign requested address

可能原因

临时端口耗尽不一定只由单个原因导致,常见组合如下:

  1. 应用没有复用连接,每次请求都新建 TCP 连接。
  2. HTTP 客户端连接池过小、空闲连接过期太快,导致频繁建连。
  3. 批量任务并发过高,短时间打满本机出站连接。
  4. ip_local_port_range 范围太窄,可用临时端口数量不足。
  5. TIME_WAIT 数量长期很高,端口回收跟不上连接创建速度。
  6. 下游服务响应慢,连接停留在 ESTABSYN-SENT 的时间变长。
  7. NAT、四层代理或容器节点上多业务共享同一出口,端口被共同消耗。

排查思路

1. 先确认临时端口范围

cat /proc/sys/net/ipv4/ip_local_port_range

常见输出:

32768 60999

这个范围表示本机主动发起连接时可使用的临时端口区间。可用端口数量约为 60999 - 32768 + 1 = 28232。如果单机短时间并发出站连接很高,这个数量可能不够。

2. 统计 TCP 状态分布

ss -ant | awk 'NR>1 {state[$1]++} END {for (s in state) print s, state[s]}' | sort -k2 -nr

重点关注:

  • TIME-WAIT:主动关闭连接后等待释放,数量过高说明短连接太多。
  • ESTAB:已建立连接,数量过高说明并发连接多或下游响应慢。
  • SYN-SENT:连接请求已发出但未完成,数量高时要排查下游、路由、防火墙或丢包。
  • CLOSE-WAIT:对端已关闭但本地应用没有关闭 socket,数量持续升高通常是应用未正确释放连接。

3. 找出连接最多的目标地址

ss -ant state time-wait | awk 'NR>1 {print $5}' | sed 's/^\[//;s/\]//' | awk -F: '{print $(NF-1)":"$NF}' | sort | uniq -c | sort -nr | head -20

这个命令用于观察 TIME_WAIT 主要集中在哪些远端地址和端口。如果大量连接都指向同一个 API、代理或数据库入口,就要回到应用层检查连接池和请求并发。

如果要看所有已建立连接的目标分布:

ss -ant state established | awk 'NR>1 {print $5}' | sed 's/^\[//;s/\]//' | awk -F: '{print $(NF-1)":"$NF}' | sort | uniq -c | sort -nr | head -20

4. 找出占用连接的进程

ss -antp | awk 'NR>1 && $1 ~ /ESTAB|TIME-WAIT|SYN-SENT|CLOSE-WAIT/ {print $1, $5, $6}' | head -50

如果需要按进程粗略统计:

ss -antp | grep -o 'pid=[0-9]*' | sort | uniq -c | sort -nr | head

拿到 PID 后查看进程:

ps -fp <PID>

容器环境中还要进一步映射到容器:

docker ps --no-trunc | grep <容器关键字>
docker inspect --format '{{.State.Pid}} {{.Name}}' $(docker ps -q)

定位示例

某台 API 聚合服务报错:

connect: cannot assign requested address

现场执行:

cat /proc/sys/net/ipv4/ip_local_port_range
ss -ant | awk 'NR>1 {state[$1]++} END {for (s in state) print s, state[s]}' | sort -k2 -nr

输出类似:

32768 60999
TIME-WAIT 26000
ESTAB 1800
SYN-SENT 120
LISTEN 38

临时端口总量约 28232,而 TIME_WAIT 已接近端口范围上限。继续查看目标分布:

ss -ant state time-wait | awk 'NR>1 {print $5}' | awk -F: '{print $(NF-1)":"$NF}' | sort | uniq -c | sort -nr | head

结果显示大部分连接指向同一个下游接口:

21580 10.20.30.15:443
 2100 10.20.30.16:443
  980 10.20.30.17:443

结合应用代码发现 HTTP 客户端在每个请求函数里临时创建,用完立即关闭,没有全局连接池复用。高峰期每秒数千次请求会持续创建短连接,最终耗尽临时端口。

修复方案

1. 优先修复应用连接复用

如果是 Python requests,不要每次调用都直接 requests.get(),应复用 Session

import requests
from requests.adapters import HTTPAdapter

session = requests.Session()
adapter = HTTPAdapter(pool_connections=100, pool_maxsize=100, max_retries=1)
session.mount("http://", adapter)
session.mount("https://", adapter)

def fetch(url: str) -> str:
    resp = session.get(url, timeout=(2, 5))
    resp.raise_for_status()
    return resp.text

关键点:

  • pool_connections 控制连接池缓存的目标主机数量。
  • pool_maxsize 控制单个连接池最大连接数。
  • timeout=(2, 5) 分别是连接超时和读取超时,避免请求无限等待。
  • 全局复用 Session,不要在每次请求里创建再销毁。

如果是 Go,优先复用全局 http.Client,并配置连接池:

transport := &http.Transport{
    MaxIdleConns:        200,
    MaxIdleConnsPerHost: 100,
    IdleConnTimeout:     90 * time.Second,
}

client := &http.Client{
    Transport: transport,
    Timeout:   5 * time.Second,
}

不要在每次请求中创建新的 http.ClientTransport

2. 控制并发和重试

批量任务要限制并发,失败重试要带退避,避免瞬间放大连接数:

# GNU parallel 示例:最多 50 个并发
cat urls.txt | parallel -j 50 'curl -m 5 -sS {} >/dev/null'

服务内建议加入:

  • 最大并发数限制。
  • 熔断和限流。
  • 指数退避重试。
  • 对同一个下游设置独立连接池和超时。

3. 适当扩大临时端口范围

确认应用连接复用已经修复后,可以适当扩大端口范围:

sysctl -w net.ipv4.ip_local_port_range="10000 65000"

持久化配置:

cat >/etc/sysctl.d/99-local-port-range.conf <<'EOF'
net.ipv4.ip_local_port_range = 10000 65000
EOF

sysctl --system

注意不要让临时端口范围覆盖本机监听端口规划。如果机器上有固定服务端口,建议在变更前梳理端口使用情况:

ss -lntup

4. 谨慎调整 TCP 参数

可以查看当前参数:

sysctl net.ipv4.tcp_fin_timeout
sysctl net.ipv4.tcp_tw_reuse

一般不建议把 TCP 参数当作第一修复手段。更稳妥的顺序是:

  1. 修复连接池复用。
  2. 控制并发和重试。
  3. 扩大临时端口范围。
  4. 再评估 TCP 参数是否需要调整。

特别是涉及 NAT、负载均衡、容器网络的环境,错误调整可能引入更隐蔽的连接问题。

容器和 Kubernetes 环境注意点

在 Kubernetes 中,Pod 的出站连接最终可能经过节点 NAT。多个 Pod 共享同一节点出口时,端口压力会集中到节点上。排查时不能只看容器内,还要看节点:

# 在 Pod 内看应用自身连接
ss -ant | awk 'NR>1 {state[$1]++} END {for (s in state) print s, state[s]}'

# 在 Node 上看整体连接压力
ss -ant | awk 'NR>1 {state[$1]++} END {for (s in state) print s, state[s]}' | sort -k2 -nr

如果只有某个节点异常,可以继续看该节点上有哪些 Pod:

kubectl get pod -A -o wide --field-selector spec.nodeName=<node-name>

常见优化方向:

  • 给高出站流量服务单独部署节点池。
  • 降低单 Pod 并发,水平扩容分散连接压力。
  • 检查 Service Mesh、代理 Sidecar、NAT 网关的连接池配置。
  • 对外部 API 使用内网专线、代理池或更合理的请求聚合策略。

预防措施

  1. 为出站请求设置连接池、连接超时和读取超时。
  2. 对批量任务设置最大并发,避免无上限 goroutine、线程或协程。
  3. 监控 TIME_WAITESTABSYN-SENTCLOSE_WAIT 数量。
  4. 监控应用请求下游的错误码、超时率和重试次数。
  5. 在压测中加入真实下游访问或连接池模拟,不只压业务函数。
  6. 容器环境同时监控 Pod 和 Node 的连接状态。
  7. 变更 sysctl 前记录旧值,保留回滚命令。

可加入巡检脚本:

#!/usr/bin/env bash
set -euo pipefail

echo "ip_local_port_range: $(cat /proc/sys/net/ipv4/ip_local_port_range)"
ss -ant | awk '
NR > 1 { state[$1]++ }
END {
  for (s in state) {
    printf "%-12s %d\n", s, state[s]
  }
}' | sort -k2 -nr

TIME_WAIT 长期接近临时端口总数的 70% 以上,或者 SYN-SENT 持续升高,就应该主动排查应用连接复用和下游响应情况。

总结

Linux 临时端口耗尽的表象经常像是下游超时,但根因可能在本机出站连接管理。排查时先看 ip_local_port_rangess 状态分布,再定位连接最多的目标地址和进程。修复时不要只依赖扩大端口范围,真正有效的方案通常是复用连接池、限制并发、设置合理超时,并在容器或 Kubernetes 环境中同时关注节点级连接压力。