有一次线上告警炸了:数据库连接数逼近 max_connections=200,应用开始大量超时。DBA 在群里@我:你们是不是泄漏连接了?
我一脸无辜:我们就用了连接池啊,HikariCP,默认配置,应该很稳的吧?
DBA 回了一句:你们连接池 max_pool_size 配的多少?
我说:50。
DBA 说:你们 pod 副本数是 8 个。
我算了一下:50 × 8 = 400。等等,PostgreSQL 的 max_connections 是 200?
DBA 发了一个微笑表情。
这就是连接池最讽刺的地方——它本该是你的守护神,结果因为一个小学数学错误,把你送进了生产事故的修罗场。
连接池不是什么"连接放一起"
很多人以为连接池就是:建立一个连接,用完了放回去,下次接着用。很朴素,很直观,也很危险——因为这种理解漏掉了整个链路上的所有细节。
当你调用 dataSource.getConnection() 时,你以为拿到的是一个数据库连接。但实际上,这中间发生了:
应用进程 → [TCP三次握手] → 负载均衡器 → [TCP三次握手] → 数据库代理/服务 → [TCP三次握手] → PostgreSQL进程
每一个箭头都是一个完整的 TCP 连接建立过程,都有 RTT 延迟,都消耗操作系统的 fd(文件描述符),都会被 net.ipv4.tcp_keepalive_* 参数所影响。
连接池做了两件事:提前建立连接(避免每次请求都握手)和复用已归还的连接。但"复用"这个词背后,藏着一堆你可能从没仔细想过的细节。
连接复用的真相:你以为的"复用"不是真正的复用
大多数连接池(HikariCP、c3p0、pgbouncer)复用的是物理TCP连接,而不是逻辑数据库会话。
这句话什么意思?
PostgreSQL 的会话不是严格绑定在一个 TCP 连接上的。一个 PostgreSQL 连接(Session)包含了:
- 后端进程(backend process)
- 会话状态(session state)
- 事务状态(transaction state)
- Prepared statements
- SET参数
- 通知/订阅状态
当你把连接归还到连接池时,池会尝试重置这个连接:回滚未提交事务、清空会话变量、重置 search_path……然后把这个"干净"的连接给下一个请求。
但这个重置过程是有代价的,也是有风险的。
来看一个真实案例。我们系统里有一个定时任务,每小时跑一次,批量处理数据。代码结构大概是:
@Scheduled(cron = "0 0 * * * *")
public void hourlyJob() {
List<Record> records = fetchRecords();
for (Record r : records) {
processWithNewTransaction(r); // 每个记录开一个新事务
}
}
这个代码本身没什么问题。但它跑着跑着,连接池开始报警:Connection is not available, request timed out after 30000ms。
查了一圈,发现问题出在 processWithNewTransaction 里有一个嵌套调用,嵌套调用里又开了新事务,而最外层事务因为某种原因一直未提交。结果就是:这一个请求持有连接的时间,从毫秒级变成了整整30秒。
30秒 × 1000条记录 = 这个定时任务把整个池都堵死了。
这不是连接泄漏,这是慢查询持有连接时间过长导致的池耗尽。区别很大,但结果一样惨烈。
HikariCP 的那些"聪明"配置,坑了多少人
HikariCP 号称"最快"的连接池,配置项也确实比 c3p0 之流复杂得多。但很多团队的配置逻辑就是:去博客抄一份。
最常见的一份"最佳配置"是这样的:
spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
这套配置在单机部署时问题不大。但一旦上 K8s,副本数 × maximum-pool-size = 实际最大连接数。这个公式搞不清楚的人,比你想象的多得多。
还有一个更隐蔽的问题:minimum-idle。
很多人以为设了 minimum-idle=5,池里就会永远保持5个空闲连接。这在 HikariCP 里是真的——前提是连接能正常维持。
如果数据库有连接限制(云数据库常见),或者网络偶尔抖动,HikariCP 维持 minimum-idle 的努力会变成一场灾难:它会不断尝试创建新连接、失败、再尝试,最终把数据库连接数打满。
HikariCP 有一个 pool-name 配置,这个名字会出现在 pg_stat_activity 的 application_name 字段里。很多团队没配这个,导致出问题查连接来源时,只能看着一堆 ip 干瞪眼。
教训:pool-name 一定要设,设成有意义的字符串,比如 ${spring.application.name}-${POD_NAME:default}。
连接池与负载均衡:一个经常被忽视的死亡组合
我们的系统用过 AWS RDS + RDS Proxy。RDS Proxy 的作用是把大量短连接汇聚成少量长连接,减轻 PostgreSQL 的连接压力。听起来很美,但遇到了一个诡异的问题:
应用层连接池配置是 maximum-pool-size=50,RDS Proxy 的 max_connections 也够用,但生产环境里还是频繁出现连接超时。
查了很久,最后发现是 负载均衡器 health check 与连接池行为的交互。
我们的 K8s service 配置了 readinessProbe,会定期发 HTTP 请求到后端。后端服务有一个逻辑:每次请求都会从连接池拿一个连接来做健康检查查询(SELECT 1)。
这本身没问题。但当 pod 数量多、健康检查频率高时,大量并发的 SELECT 1 会同时从连接池借出连接,而这些连接的生命周期很短(健康检查嘛,用完就还)。这导致连接池的行为从"少量长连接复用"变成了"大量短连接轮转"。
连接池在高频短连接场景下,idle-timeout 形同虚设——连接根本到不了空闲超时就被还回去了。而真正需要连接的正常业务请求,反而要排队等。
解决方式:把健康检查的数据库查询改成了连接池外的 ping(直接 TCP),或者把健康检查频率降下来。
真正的连接池配置方法论
说了这么多坑,该给点正面的了。经过真实生产环境的踩坑,总结出一套相对可靠的连接池配置思路。
第一步:搞清楚上限。
PostgreSQL: max_connections = 1000(共享内存允许范围内)
云数据库:看实例规格,rds.mysql.small 实例可能 max_connections = 400
RDS Proxy:一般比数据库的 max_connections 低,要查文档
第二步:计算每个 pod 的配额。
单pod最大连接数 = floor(数据库max_connections × 0.7 / pod副本数)
乘 0.7 是留 30% 给:DBA 直连、监控agent、其他偶尔连进来的服务。不要贪心,留余量是美德。
第三步:设置 maximum-pool-size。
这个值应该等于计算出来的单pod配额。但更重要的是:确保你的并发线程数不会超过这个值。如果你的服务配置了 200 个工作线程,但连接池只有 50 个 max pool size,那最多只有 50 个线程能同时持有连接,其余 150 个会等待。这不一定是问题(线程池本身就会排队),但你要知道这个数学关系。
第四步:minimum-idle 设不设?
如果你的流量比较平稳,可以设,有助于避免冷启动延迟。如果流量波动大,不设也行,让 HikariCP 按需创建——它在这方面其实挺智能的。
第五步:connection-timeout、idle-timeout、max-lifetime 三兄弟。
connection-timeout: 5000(获取连接超时,5秒不给就报错的阈值)
idle-timeout: 300000(5分钟空闲后释放,不要太长)
max-lifetime: 1800000(30分钟强制销毁,不管用没用过)
max-lifetime 是必须的,因为数据库侧有时候会强制断开长时间空闲的连接(比如 RDS 有 wait_timeout),如果你不主动控制这个时间,对端断连时你的池会收到一个诡异的 IOException,然后整个池要重建。
监控:比配置更重要的事
最后说一个很多人忽略的点:连接池的监控比配置重要 100 倍。
HikariCP 自带了一套指标,通过 JMX 暴露:
HikariPool-1 Pool.Size:当前活跃 + 空闲连接数HikariPool-1 Active.Count:当前正在使用的连接数HikariPool-1 Pending.Count:等待获取连接的线程数HikariPool-1 ConnectionTimeoutRate:连接获取超时率
建议把这些指标全部接入 Prometheus/Grafana,设置报警:
- Active.Count > maximum-pool-size × 80%
- Pending.Count > 0(出现等待就该查)
- ConnectionTimeoutRate > 0.01(1%以上的超时率)
最后,回到开头那个故事。最终我们的解决方案是:把 maximum-pool-size 从 50 改成了 20,副本数从 8 缩容到 4,数学终于对上了,事故解除。
DBA 跟我说:下次上线前,先算清楚数。
我觉得他说得对,但那个微笑表情,我到现在还耿耿于怀。
连接池这玩意儿,说简单也简单,说复杂也复杂。大多数人倒在的不是"不会配置",而是"以为会了"。真正理解连接生命周期、池行为和上游限制之间的关系,才能真正用好它。
希望这篇文章,能让你下次被DBA@的时候,少一点微笑表情包。