|
|
马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?用户注册
×
数据库连接池治理实战:从连接耗尽到泄漏排查
一、具体的问题
周三晚上八点,订单服务高峰期开始报警:日志里刷 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:确认现状基线
- -- MySQL 侧:连接总量、历史峰值、超时参数
- SHOW GLOBAL STATUS LIKE 'Threads_connected';
- SHOW GLOBAL STATUS LIKE 'Max_used_connections';
- SHOW VARIABLES LIKE 'max_connections';
- SHOW VARIABLES LIKE 'wait_timeout';
- SHOW VARIABLES LIKE 'interactive_timeout';
复制代码- -- 按 user/host 聚合看连接构成,确认是哪个应用在占
- SELECT user, LEFT(host, 20) AS host, COUNT(*) AS cnt,
- SUM(command = 'Sleep') AS sleeping,
- MAX(time) AS max_sleep_sec
- FROM information_schema.processlist
- GROUP BY user, LEFT(host, 20)
- 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 加泄漏检测(观测用,定位后关掉):
- # 单位毫秒:借出超过 60 秒未归还就打印持有者的调用堆栈
- leakDetectionThreshold=60000
复制代码
数据库侧找长睡连接并保留现场:
- SELECT id, user, host, db, command, time, state, info
- FROM information_schema.processlist
- WHERE command = 'Sleep' AND time > 300
- ORDER BY time DESC;
复制代码
把结果按 host 里的 IP 聚合,就能定位到是哪几台应用机器在漏连接。再结合应用日志里 leakDetectionThreshold 打出的堆栈,直接指到具体代码行——那次定位到的是订单导出功能里一个手工 try 块,异常分支没走 finally close。
步骤 2:修泄漏点
- // 修复前:异常时连接不还
- Connection conn = dataSource.getConnection();
- try {
- // 业务逻辑,可能抛异常
- export(conn);
- } catch (Exception e) {
- log.error("export failed", e); // 连接悬空
- }
- // 修复后:try-with-resources,无论如何都归还
- try (Connection conn = dataSource.getConnection()) {
- export(conn);
- }
复制代码
团队顺便加了一条 Checkstyle 规则禁止直接 DriverManager.getConnection,所有连接必须从池里借。
步骤 3:收敛池参数
按 HikariCP 官方给出的经验公式,池大小从 40 收到 20(CPU 核数 × 2 + 磁盘数,DB 服务器 8 核 SSD 算下来 20 上下最合理;池再大只会增加上下文切换,吞吐反而降):
- maximumPoolSize=20
- minimumIdle=20 # 固定池,避免弹性伸缩抖动
- connectionTimeout=3000 # 借不到连接 3 秒快速失败,别让线程干等 30 秒
- maxLifetime=1800000 # 30 分钟,必须小于 wait_timeout
- idleTimeout=0 # 固定池时该参数不生效
复制代码
关键校验:maxLifetime(30min) < wait_timeout(8h),中间任何 NAT/防火墙的空闲断开时间也要覆盖到——如果网络设备 5 分钟掐空闲连接,就把 maxLifetime 压到 4 分钟以内。
步骤 4:数据库侧兜底
- -- 定时清理超过 1 小时的空闲连接(保留复制和管理账号)
- SELECT CONCAT('KILL ', id, ';')
- FROM information_schema.processlist
- WHERE command = 'Sleep' AND time > 3600
- 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 小时连接 | 655 | 0(脚本兜底未触发) | | 连接耗尽告警 | 5~8 次/日 | 0(连续 30 天) | | 借连接 P99 | 3200ms | 4ms | | 订单接口 P99(高峰) | 4.1s | 96ms |
这套治理没有动一行数据库内核,改的全是"连接怎么借、怎么还、怎么死"的三件小事。连接池的病,十有八九病根在应用侧,数据库侧只是替它背锅。 |
|