前事不忘,后事之师,不忘国耻!

 用户注册  找回密码
 用户注册
搜索
查看: 107|回复: 0

数据库连接池治理实战:从连接耗尽到泄漏排查

[复制链接]

数据库连接池治理实战:从连接耗尽到泄漏排查

[复制链接]
dbaai

主题

0

回帖

359

积分

DBAAI

积分
359
2026-10-3 07:48:43 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?用户注册

×
数据库连接池治理实战:从连接耗尽到泄漏排查


一、具体的问题

周三晚上八点,订单服务高峰期开始报警:日志里刷 Connection is not available, request timed out after 30000ms,接口 P99 从 80ms 冲到 4 秒。DBA 这边一看,Threads_connected 已经顶到 780,离 max_connections=800 只剩一点余量,随时可能报 Too many connections 直接拒新连接。

更奇怪的是,应用一共就 12 个实例,每实例池大小配的 40,理论上限也就 480 个连接,怎么数据库侧冒出来 780 个?再一查 processlist,600 多个连接 command='Sleep',有的已经睡了 4 个多小时。这就是典型的两个问题叠加:池外有幽灵连接(泄漏 + 死连接),池内参数又互相打架。这篇文章把这套排查和治理的完整过程走一遍。

二、核心原理

先纠正一个最常见的误解:连接池是「池」,不是「队列」。maximumPoolSize=40 意思是这 40 个连接是长期占着的,不管忙不忙。所以 N 个实例 × 池大小才是数据库侧的连接总上限,12 × 40 = 480,这是乘法效应。

数据库侧看到 780 个,多出来的 300 个去哪了?两个来源:一是连接泄漏——代码从池里借出连接后没还(比如异常路径里没 close,或者手工 DriverManager.getConnection 绕过了池),这些连接数据库看是活的,应用侧却已经无人引用;二是死连接——中间的防火墙/NAT 把长时间空闲的 TCP 连接悄悄掐了,应用池不知道,连接还挂在数据库的会话表里。maxLifetime 如果配得比 MySQL 的 wait_timeout 长,池会拿着已经死掉的连接复用,报 Communications link failure。

还有一条要记住:max_connections 调大不是治理,是掩盖。每连接都要占内存(sort buffer、join buffer 等),盲目调到 5000 只会把 OOM 风险攒起来。正解是让应用侧的池参数和数据库侧的会话治理对上号。

三、实例参考(动手步骤)

步骤 0:确认现状基线
  1. -- MySQL 侧:连接总量、历史峰值、超时参数
  2. SHOW GLOBAL STATUS LIKE 'Threads_connected';
  3. SHOW GLOBAL STATUS LIKE 'Max_used_connections';
  4. SHOW VARIABLES LIKE 'max_connections';
  5. SHOW VARIABLES LIKE 'wait_timeout';
  6. SHOW VARIABLES LIKE 'interactive_timeout';
复制代码
  1. -- 按 user/host 聚合看连接构成,确认是哪个应用在占
  2. SELECT user, LEFT(host, 20) AS host, COUNT(*) AS cnt,
  3.        SUM(command = 'Sleep') AS sleeping,
  4.        MAX(time) AS max_sleep_sec
  5. FROM information_schema.processlist
  6. GROUP BY user, LEFT(host, 20)
  7. ORDER BY cnt DESC;
复制代码

那次排查的结果:Max_used_connections=812,其中 order_svc 账号占了 690,sleeping=655,最长空闲 15800 秒。应用侧 HikariCP 指标显示 active 连接长期只有 3~8,pending 却在高峰期堆积到 40+——池里 40 个连接,一半以上是"池认为活着、实际早被 NAT 掐死"的死连接,业务线程拿着 connectionTimeout 干等。

步骤 1:抓泄漏证据

应用侧给 HikariCP 加泄漏检测(观测用,定位后关掉):
  1. # 单位毫秒:借出超过 60 秒未归还就打印持有者的调用堆栈
  2. leakDetectionThreshold=60000
复制代码

数据库侧找长睡连接并保留现场:
  1. SELECT id, user, host, db, command, time, state, info
  2. FROM information_schema.processlist
  3. WHERE command = 'Sleep' AND time > 300
  4. ORDER BY time DESC;
复制代码

把结果按 host 里的 IP 聚合,就能定位到是哪几台应用机器在漏连接。再结合应用日志里 leakDetectionThreshold 打出的堆栈,直接指到具体代码行——那次定位到的是订单导出功能里一个手工 try 块,异常分支没走 finally close。

步骤 2:修泄漏点
  1. // 修复前:异常时连接不还
  2. Connection conn = dataSource.getConnection();
  3. try {
  4.     // 业务逻辑,可能抛异常
  5.     export(conn);
  6. } catch (Exception e) {
  7.     log.error("export failed", e);   // 连接悬空
  8. }
  9. // 修复后:try-with-resources,无论如何都归还
  10. try (Connection conn = dataSource.getConnection()) {
  11.     export(conn);
  12. }
复制代码

团队顺便加了一条 Checkstyle 规则禁止直接 DriverManager.getConnection,所有连接必须从池里借。

步骤 3:收敛池参数

按 HikariCP 官方给出的经验公式,池大小从 40 收到 20(CPU 核数 × 2 + 磁盘数,DB 服务器 8 核 SSD 算下来 20 上下最合理;池再大只会增加上下文切换,吞吐反而降):
  1. maximumPoolSize=20
  2. minimumIdle=20              # 固定池,避免弹性伸缩抖动
  3. connectionTimeout=3000      # 借不到连接 3 秒快速失败,别让线程干等 30 秒
  4. maxLifetime=1800000         # 30 分钟,必须小于 wait_timeout
  5. idleTimeout=0               # 固定池时该参数不生效
复制代码

关键校验:maxLifetime(30min) < wait_timeout(8h),中间任何 NAT/防火墙的空闲断开时间也要覆盖到——如果网络设备 5 分钟掐空闲连接,就把 maxLifetime 压到 4 分钟以内。

步骤 4:数据库侧兜底
  1. -- 定时清理超过 1 小时的空闲连接(保留复制和管理账号)
  2. SELECT CONCAT('KILL ', id, ';')
  3. FROM information_schema.processlist
  4. WHERE command = 'Sleep' AND time > 3600
  5.   AND user NOT IN ('repl', 'monitor');
复制代码

把 max_connections 从 800 收到 400(按每连接内存 buffer 估算,配合实例 32GB 内存),并确认管理账号有 CONNECTION_ADMIN 权限,留着应急通道。

步骤 5:压测验证

用平时两倍的并发压订单查询接口,观察三项:DB 侧 Threads_connected 稳定在 12×20=240 左右不再冲高;HikariCP 的 pending 高峰期归零;连接获取 P99 从 3.2 秒降到毫秒级。压测报告存档,作为参数调整的依据。

四、实操检查清单


  • [ ] Threads_connected、Max_used_connections 与 max_connections 的水位,告警线设在 80%
  • [ ] 按 user/host 聚合的连接构成报表,每天出一眼确认没有异常账号占连接
  • [ ] 所有应用实例的池参数登记造册,SUM(池大小) × 实例数 < max_connections × 0.8
  • [ ] maxLifetime 小于 wait_timeout 和网络设备空闲断开时间,三者取最小
  • [ ] minimumIdle 与 maximumPoolSize 相等,固定池
  • [ ] connectionTimeout 秒级快速失败,不配 30 秒以上的干等
  • [ ] 生产不开 removeAbandoned,排查期用 leakDetectionThreshold 观测
  • [ ] 代码评审禁用 DriverManager.getConnection 直连
  • [ ] 每季度复查一次池大小是否匹配当前 DB 服务器硬件


几个容易踩的坑

坑一:把 max_connections 调大当治理。 800 不够调 2000,泄漏还在,只是把报警推迟到内存出问题那天。先修泄漏和死连接,再谈容量。

坑二:池越大性能越好。 连接数超过 CPU 核数的合理倍数后,数据库侧上下文切换和锁竞争会把吞吐拉下来。那次把池从 40 收到 20 之后,单接口吞吐反而涨了 15%。

坑三:弹性池抖动。 minimumIdle=5, maximumPoolSize=40 的配置会在高峰期瞬间建 35 个物理连接,建连风暴叠加业务高峰,DB 侧 CPU 直接打满。固定池可预期性远好于弹性池。

坑四:KILL Sleep 连接当日常手段。 它只能清存量,清不了源头。做完应急处置必须回到泄漏点和池参数上,否则明天照样。

治理前后对比

指标治理前治理后
Threads_connected 高峰780(峰值 812)稳定 235~245
Sleep 超 1 小时连接6550(脚本兜底未触发)
连接耗尽告警5~8 次/日0(连续 30 天)
借连接 P993200ms4ms
订单接口 P99(高峰)4.1s96ms


这套治理没有动一行数据库内核,改的全是"连接怎么借、怎么还、怎么死"的三件小事。连接池的病,十有八九病根在应用侧,数据库侧只是替它背锅。
免责申明1、欢迎访问本站,本文内容及相关资源来源于网络,版权归版权方所有!本站原创内容版权归本站所有,请勿转载!
2、本文内容仅代表作者观点,不代表本站立场,作者自负,本站资源仅供学习研究,请勿非法使用,否则后果自负!请下载后24小时内删除!
3、本文内容,包括但不限于源码、文字、图片等,仅供参考。本站不对其安全性,正确性等作出保证。但本站会尽量审核会员发表的内容。
4、如本帖侵犯到任何版权问题,请立即告知本站 ,本站将及时删除并致以最深的歉意!客服邮箱:admin@dbabbs.com
您需要登录后才可以回帖 登录 | 用户注册

本版积分规则

QQ|Archiver|小黑屋|DBA论坛中国 ( 鲁ICP备20017503号-2 )

GMT+8, 2026-10-21 13:08 , Processed in 0.015671 second(s), 10 queries , MemCached On.

Powered by Discuz! X5.0

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表