凌晨告警响起,接口大面积超时,日志里却找不到慢 SQL。这种“查无慢查询、却连不上库”的诡异现象,多半是连接池耗尽或端口耗尽在作祟。两者表面都是“连不上”,根因却一个在应用层、一个在系统层。一旦这一类故障发生,典型后果是接口 P99 延迟从几十毫秒飙到数秒、应用线程池被打满、进而引发上游调用的级联雪崩。本文用一版可复用的排错脚本,带你从 TIME_WAIT 暴涨一路定位到连接泄漏根因,并给出 HikariCP、Redis 与内核参数的根治方案。
一、连接池耗尽 vs 端口耗尽,先分清对手
曾遇到过这样一个场景:大促零点一过,订单服务报错量直线上升,但数据库 CPU 并不高、慢查询日志也空空如也。最后发现是应用侧连接池被借光、请求全卡在“等连接”的队列里。这种“数据库很闲、应用却连不上”的反直觉现象,正是两类耗尽的典型特征。
连接池耗尽:应用向连接池“借”连接,但连接没被归还(泄漏)或池太小,所有连接都在忙,新请求只能排队或报错——典型异常是 Timeout: Pool exhausted 或 HikariPool-1 - Connection is not available。
端口耗尽:客户端(或本机)频繁建短连接,操作系统临时端口(ephemeral port,默认约 2.8 万个)被占满,大量连接卡在 TIME_WAIT,新连接直接 Cannot assign requested address。两者常被混为一谈,但修复手段完全不同。
| 维度 | 连接池耗尽 | 端口耗尽 |
|---|---|---|
| 故障层级 | 应用层(连接池) | 系统层(内核 TCP) |
| 典型报错 | Pool exhausted / Connection not available | Cannot assign requested address |
| 瓶颈资源 | 连接池活跃连接数 | 临时端口 + TIME_WAIT 数量 |
| 首选修复 | 修泄漏 / 调大池 / 杀慢事务 | tcp_tw_reuse / 扩端口 / 长连接 |
二、第一步:用命令看清症状
别急着改配置,先看现场。下面这组命令能在一分钟内区分“到底是哪种耗尽”,避免凭感觉乱调参数。
# 统计各状态的 TCP 连接数,重点关注 TIME_WAIT 是否暴涨
ss -tan | awk '{print $1}' | sort | uniq -c
# 查看某端口(如 3306)的连接分布
ss -tanp | grep ':3306' | awk '{print $1}' | sort | uniq -c
# 进程级:谁在疯狂建连(替换 <pid>)
lsof -p <pid> -i | grep -c ESTABLISHED
# 本机可用临时端口范围
cat /proc/sys/net/ipv4/ip_local_port_range
若 TIME_WAIT 数万且多为短连接来源,优先怀疑端口耗尽;若业务线程卡在“等待连接池”,则是连接池耗尽。两者也可能同时出现:短连接太多既耗端口又压垮连接池。
三、连接池耗尽:根因通常是“借了不还”
三种最常见根因:①连接泄漏——代码拿到连接后异常路径没 close;②池容量过小——maxPoolSize 没按并发量估算;③慢事务/慢查询长期占用连接,连接周转不过来(这类问题常与MySQL 死锁排查相伴,慢 SQL 把连接占死,池子很快见底)。
spring:
datasource:
hikari:
maximum-pool-size: 20 # 按 (核心数*2 + 磁盘数) 估算
minimum-idle: 5
connection-timeout: 3000 # 借不到连接 3s 就报错,别无限等
idle-timeout: 600000
max-lifetime: 1800000 # 小于数据库 wait_timeout,防失效连接
leak-detection-threshold: 5000 # >5s 未归还即告警,抓泄漏神器
Redis 同理,务必给客户端配上限,否则单实例也会被本地连接数拖垮(延伸阅读Redis 连接管理)。注意 Lettuce 默认是共享连接、按需借用,若要真正的连接池需在配置里显式开启 pool,否则高并发下仍可能把后端连接打满。
spring:
redis:
lettuce:
pool:
max-active: 16
max-idle: 8
min-idle: 2
max-wait: 2000ms
四、端口耗尽:根因是“短连接太多”
每次新建 TCP 连接都要消耗一个临时端口,连接关闭后进入 TIME_WAIT(默认 60s)。高 QPS 短连接场景下,端口根本回收不过来。修复思路:开启 TIME_WAIT 快速复用、扩大端口范围、用长连接代替短连接。注意 tcp_tw_reuse 仅对客户端(主动关闭方)安全,切勿在生产误开 tcp_tw_recycle(早已废弃且会丢包)。
# 临时生效
sysctl -w net.ipv4.tcp_tw_reuse=1
sysctl -w net.ipv4.ip_local_port_range="1024 65535"
sysctl -w net.ipv4.tcp_keepalive_time=600
# 持久化
cat >> /etc/sysctl.conf <<'EOF'
net.ipv4.tcp_tw_reuse = 1
net.ipv4.ip_local_port_range = 1024 65535
net.ipv4.tcp_keepalive_time = 600
EOF
sysctl -p
应用侧把短连接改成连接池复用的长连接(呼应上面的 HikariCP/Redis 池);反向代理层也可开启 keepalive,减少回源建连——做法见Nginx 反向代理 的 keepalive 配置。
五、真实案例:一段“借了不还”的 Java 代码
// ❌ 异常时 conn 未被关闭,连接永久泄漏
public User load(Long id) {
Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(SQL);
ps.setLong(1, id);
return map(ps.executeQuery()); // 这里抛异常,conn 永远不归还
}
正确写法用 try-with-resources,任何路径都会自动关闭并归还连接池:
// ✅ try-with-resources 保证任何路径都关闭
public User load(Long id) {
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(SQL)) {
ps.setLong(1, id);
return map(ps.executeQuery());
} // conn 与 ps 自动关闭并归还连接池
}
配合 HikariCP 的 leak-detection-threshold,超时未归还时会在日志打印完整调用栈,直接定位到那一行“借了不还”的代码。容器化部署下这类泄漏还会表现为 Pod 连接数随时间只增不减,排查思路与Docker 网络排查一脉相承。
六、上线前的预防清单
| 检查项 | 建议值 | 说明 |
|---|---|---|
| maxPoolSize 估算 | 核心数×2+磁盘数 | 防池过小 |
| leak-detection-threshold | 5000ms | 抓连接泄漏 |
| max-lifetime < wait_timeout | 留 30s 余量 | 防失效连接 |
| tcp_tw_reuse + 端口范围 | 开启 / 1024-65535 | 防端口耗尽 |
| 反代/客户端 keepalive | 开启 | 减少短连接 |
| 连接数/等待数监控 | 接入K8s 监控 | 早于告警发现 |
七、压测复现:把故障提前逼出来
修复上线前,最好用压测把问题复现一遍,确认根因判断没错,也避免“调完参数却不知道有没有用”。以 API 服务为例,用 wrk 打一波短连接,同时观察本机连接状态:
# 200 并发、持续 30s 的短连接压测
wrk -t4 -c200 -d30s --latency http://127.0.0.1:8080/api/ping
# 压测期间另开窗口观察 TIME_WAIT 增长
watch -n1 'ss -tan | awk "{print $1}" | sort | uniq -c'
如果肉眼看到 TIME_WAIT 随压测线性飙升、接口延迟尾部暴涨,说明端口/连接瓶颈就在这层;若延迟高但连接数平稳,则更可能是连接池之外的慢依赖。把复现脚本固化进回归测试,下次改连接参数就能自动验证效果,而不是靠运气。
八、监控与告警:在用户之前发现问题
等告警炸了才处理,损失已经造成。把下面三类指标接进监控(容器环境可结合K8s 监控的采集体系),做到“还没超时就被预警”:
| 指标 | 采集点 | 告警建议 |
|---|---|---|
| 连接池活跃数 / 等待队列长度 | HikariCP / Lettuce 暴露的 Micrometer 指标 | 活跃数持续 > 80% max 即告警 |
| TIME_WAIT 数 / 临时端口使用率 | node_exporter 的 TCP 状态采集 | TIME_WAIT > 1万 或 端口率 > 70% 告警 |
| 建连失败率 / 获取连接超时数 | 应用日志 + APM | 环比突增即告警 |
告警之外,定期用本文的命令做一次“连接体检”,比事后救火成本低得多。对于微服务集群,建议把连接池与端口指标纳入统一大盘,跨服务对比更容易发现是单体问题还是链路共因。
九、小结
连接池耗尽与端口耗尽,本质都源于“连接没被好好管理”。把池配对、把连接还回去、把短连接变长连接,两类故障都能从源头消失。值得强调的是,二者常常结伴出现:端口耗尽往往是连接池没复用、每次短连的表象;修好连接池,端口问题往往一并缓解。相关延伸阅读:Redis 连接管理、Nginx 反向代理、Docker 网络排查。




