连接池耗尽与端口耗尽排查:从 TIME_WAIT 到连接泄漏根治

凌晨告警响起,接口大面积超时,日志里却找不到慢 SQL。这种“查无慢查询、却连不上库”的诡异现象,多半是连接池耗尽端口耗尽在作祟。两者表面都是“连不上”,根因却一个在应用层、一个在系统层。一旦这一类故障发生,典型后果是接口 P99 延迟从几十毫秒飙到数秒、应用线程池被打满、进而引发上游调用的级联雪崩。本文用一版可复用的排错脚本,带你从 TIME_WAIT 暴涨一路定位到连接泄漏根因,并给出 HikariCP、Redis 与内核参数的根治方案。

一、连接池耗尽 vs 端口耗尽,先分清对手

曾遇到过这样一个场景:大促零点一过,订单服务报错量直线上升,但数据库 CPU 并不高、慢查询日志也空空如也。最后发现是应用侧连接池被借光、请求全卡在“等连接”的队列里。这种“数据库很闲、应用却连不上”的反直觉现象,正是两类耗尽的典型特征。

连接池耗尽:应用向连接池“借”连接,但连接没被归还(泄漏)或池太小,所有连接都在忙,新请求只能排队或报错——典型异常是 Timeout: Pool exhaustedHikariPool-1 - Connection is not available

端口耗尽:客户端(或本机)频繁建短连接,操作系统临时端口(ephemeral port,默认约 2.8 万个)被占满,大量连接卡在 TIME_WAIT,新连接直接 Cannot assign requested address。两者常被混为一谈,但修复手段完全不同。

维度连接池耗尽端口耗尽
故障层级应用层(连接池)系统层(内核 TCP)
典型报错Pool exhausted / Connection not availableCannot 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-threshold5000ms抓连接泄漏
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 网络排查

上一篇 K8s Ingress 实战:NGINX 暴露服务与 TLS
下一篇 Elasticsearch 实战:从全文检索到聚合分析