适用场景
这类问题常见于网关、爬虫、批处理任务、消息消费者、API 聚合服务等高并发出站访问场景。应用本身没有明显 CPU 或内存瓶颈,但访问下游服务、Redis、MySQL 代理、HTTP API 时偶发超时,重启应用后短时间恢复,过一会儿又开始失败。
临时端口耗尽的核心原因是:本机主动发起大量 TCP 连接,连接关闭后短时间内保留在 TIME_WAIT 等状态,占用了本机可用于出站连接的端口范围。当可用端口不足时,新连接可能出现 Cannot assign requested address、连接超时或请求排队。
现象描述
线上通常会看到下面几类表现:
- 应用日志里出现
connect timeout、Connection timed out、Cannot assign requested address。 - 同一台机器上只有高并发出站服务异常,其他服务基本正常。
ss看到大量TIME-WAIT、ESTAB或SYN-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
可能原因
临时端口耗尽不一定只由单个原因导致,常见组合如下:
- 应用没有复用连接,每次请求都新建 TCP 连接。
- HTTP 客户端连接池过小、空闲连接过期太快,导致频繁建连。
- 批量任务并发过高,短时间打满本机出站连接。
ip_local_port_range范围太窄,可用临时端口数量不足。TIME_WAIT数量长期很高,端口回收跟不上连接创建速度。- 下游服务响应慢,连接停留在
ESTAB或SYN-SENT的时间变长。 - 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.Client 或 Transport。
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 参数当作第一修复手段。更稳妥的顺序是:
- 修复连接池复用。
- 控制并发和重试。
- 扩大临时端口范围。
- 再评估 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 使用内网专线、代理池或更合理的请求聚合策略。
预防措施
- 为出站请求设置连接池、连接超时和读取超时。
- 对批量任务设置最大并发,避免无上限 goroutine、线程或协程。
- 监控
TIME_WAIT、ESTAB、SYN-SENT、CLOSE_WAIT数量。 - 监控应用请求下游的错误码、超时率和重试次数。
- 在压测中加入真实下游访问或连接池模拟,不只压业务函数。
- 容器环境同时监控 Pod 和 Node 的连接状态。
- 变更
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_range 和 ss 状态分布,再定位连接最多的目标地址和进程。修复时不要只依赖扩大端口范围,真正有效的方案通常是复用连接池、限制并发、设置合理超时,并在容器或 Kubernetes 环境中同时关注节点级连接压力。
Discussion
评论