MySQL连接池爆满问题排查与解决:从症状定位到参数调优 MySQL 数据库连接池爆满问题排查与解决1. 先判断这是不是“连接池爆满”1.1 症状识别从报错和调用链入手连接池爆满是后端系统里最典型的“夜间急诊”类问题。很多团队第一次遇到时最直接的感受是“服务突然开始抖了”接口要么超时要么直接报错紧接着监控页面上数据库连接数拉出一条直线冲到顶。你首先要做的不是猜而是确认。通常来说连接池爆满时你会看到这么几类明显信号应用日志里疯狂出现类似Cannot get connection from pool、Connection is not available, request timed out的报错数据库监控上Threads_connected或者应用侧的active_connection指标持续走高甚至撞到max_connections上限接口平均响应时间从几十毫秒涨到几秒部分接口开始出现 500数据库 CPU 不一定飙高但连接数和运行中线程数会出现明显异常。这里有一个很容易犯的误区你以为是数据库慢查询把连接占满了结果一看数据库 CPU 才 20%SQL 也看不出什么问题。实际上连接池爆满的根因并不总是慢查询很多时候是“连接拿得到但还不了”或者“拿连接的速度远远超过释放速度”。所以不要一上来就奔着慢查询去先把现场数据收集齐再判断病根在哪。那这份工作适合谁来参考后端开发、DBA、运维的同学都适用。如果你正在值班遇到服务大面积超时第一步不是重启数据库而是按本文的排查顺序把现场保存下来。1.2 量化指标看哪些数据才算“有依据”判断“是不是真的连接池爆满”不能靠猜我通常会同时看三层数据第一层应用层连接池指标。假如你用 HikariCP就关注active、idle、pending三个数字。active接近池上限说明业务线程正占用着所有连接pending大于 0说明已经有人在排队等连接了这是连接池被“榨干”的直接证据。第二层数据库层连接指标。用下面这条 SQL 看 MySQL 端连接分布SHOW GLOBAL STATUS LIKE Threads_connected; SHOW GLOBAL STATUS LIKE Threads_running; SHOW VARIABLES LIKE max_connections; SHOW VARIABLES LIKE wait_timeout; SHOW VARIABLES LIKE interactive_timeout;Threads_connected表示当前所有客户端连到 MySQL 的连接数Threads_running表示正在执行 SQL 的线程数。两者差距大通常意味着大量连接是“占着位置但不干活”的。第三层网络与中间件层。如果前端有代理比如 MyCat、ProxySQL或者云数据库前面有 SLB也要看这些组件的连接数和后端连接池配置。我遇到过一个案例数据库本身max_connections很大但中间件默认max_connections只有 100业务一扩容就先打爆中间件。三层数据对比完你基本能判断问题到底出在“应用拿不到连接”还是“数据库连不上”还是“中间件排队”。2. 排查路径顺着调用链路找根因2.1 第一步拿到现场数据避免“事后拍脑袋”问题发生时最怕的一件事是还没把现场保存下来就有人急着重启应用或者重启数据库。重启之后连接数清了问题也“消失”了但下一次流量过来事故还会复现。所以我的习惯是先固化现场# 每 5 秒采集一次数据库连接状态持续 1 分钟 mysqladmin -uroot -p status --sleep5 --count12 # 采集当前所有连接的详细快照 mysql -uroot -p -e SELECT * FROM information_schema.processlist ORDER BY time DESC;拿到processlist后重点看三列TIME、STATE、COMMAND。COMMAND为Sleep的连接说明它已经执行完 SQL但连接没有还给池子TIME非常大的Query说明 SQL 卡住了STATE为Waiting for table metadata lock、Waiting for lock这类说明在等锁。同时把应用连接池的监控截图保存下来特别是active和pending的变化曲线。有了这些现场数据后面复盘定位才有依据。2.2 第二步区分连接是“被用着”还是“在睡觉”这是整个排查过程中最关键的一步。如果Threads_connected高而且information_schema.processlist里大部分是Sleep连接那问题大概率出在“连接泄漏”或者“连接池参数配置不合理”上。MySQL 的wait_timeout默认是 8 小时意味着一个连接睡着后最多要 8 小时才会被服务端断开。如果应用侧连接池又没有主动回收这些 Sleep 连接就会把数据库连接数慢慢顶高直到新的业务连接挤不进来。如果Threads_running很高同时processlist里能看到大量真实的Query还在执行那才是 SQL 本身慢的问题。比如一张千万级的大表没有合适索引一次查询走全表扫描扫了 30 秒还没结束后面的请求全部排队。用一条命令区分这两种情况SELECT command, COUNT(*) FROM information_schema.processlist GROUP BY command;如果结果里Sleep占 90% 以上优先查连接池回收配置如果Query占大多数优先查慢 SQL 和锁等待。2.3 第三步定位占用连接的应用和代码位置光在数据库层面看到连接数高还不够你得知道是哪个应用、哪条代码路径占用了连接。如果只有单应用接入数据库直接看应用日志和连接池监控即可。但生产环境很少这么简单往往是十几个微服务共用同一个 MySQL 实例。这时候我建议按host和user维度分组SELECT user, host, COUNT(*) AS cnt FROM information_schema.processlist GROUP BY user, host ORDER BY cnt DESC LIMIT 20;这样能快速锁定是哪个应用实例的 IP 贡献了最多连接。锁定来源后再回应用侧看连接池的active连接都在做什么。通常我会在代码里临时加一条监控把“获取连接时当前线程栈”打出来连续采样几分钟// 伪代码仅在问题排查期临时开启线上不要常驻 MapStackTraceElement[], Integer snapshot new HashMap(); for (int i 0; i pool.getActiveConnections(); i) { // 记录当前活跃连接所属的业务线程 StackTrace }这个方法看起来粗暴但非常有效。它能直接告诉你连接是被哪个业务方法拿着是在执行 SQL还是卡在某个外部调用上没释放。2.4 第四步看看 MySQL 侧的承受能力应用层排查完还要确认 MySQL 当前能承载的最大连接数。执行SHOW VARIABLES LIKE max_connections; SHOW GLOBAL STATUS LIKE Max_used_connections;Max_used_connections是历史峰值。如果它已经非常接近max_connections说明数据库连接容量本身就不够需要提额或扩容如果离上限还很远说明问题完全出在应用侧连接池分配不均衡上。我见过一个比较典型的案例订单服务连接池 50库存服务连接池 50单独看每个应用都不算大但两个服务部署了 20 个实例加起来 1000 个连接全打到一台 4C8G 的 MySQL 上直接把连接数和内存干爆。所以这里记住一个要点连接数不是按“单个应用”算的而是按“所有应用实例加在一起”算的。3. 根因分类与解决实践3.1 慢查询把连接占满慢查询引发连接池爆满是最常见的一类。一个慢 SQL 执行 3 秒钟如果并发 100 个请求同时打过来100 个连接就被这 100 个慢查询同时占住。新的请求进来没有连接可用就开始排队连接池pending蹭蹭上涨。解决思路分两步第一步找到慢 SQL。开启慢查询日志或者用性能分析工具直接抓当前正在执行的 SQLSHOW FULL PROCESSLIST;重点看TIME大的连接对应的 SQL一般就是“元凶”。第二步针对性优化。最常见的是索引没建上。比如订单表查user_id字段但索引只有order_no那查询就会走全表扫描数据量大时必慢。给它加上索引问题可能立刻缓解ALTER TABLE order_info ADD INDEX idx_user_id (user_id);需要注意的是加索引本身会锁表线上大表加索引要选低峰期或者用在线 DDL 工具。不要在大白天流量高峰期直接ALTER TABLE一张千万级的大表否则你排查的问题还没解决反而把数据库写锁拖死了。3.2 连接泄漏用了不还连接泄漏比慢查询更隐蔽因为数据库端看到的连接状态大多是Sleep你甚至看不出它们有什么异常。但连接数就是只增不减直到把池子占满然后整个应用就“假死”在那里。我印象很深的一次是排查一个支付回调接口代码里用了一个DataSourceUtils.getConnection()获取连接但正常流程里没写释放逻辑只有异常分支才释放。平时流量小不明显一到活动流量上来连接数以每分钟几十个的速度往上爬40 分钟就把连接池占满了。排查连接泄漏除了看线程栈常规做法还可以在连接池配置里打开“泄漏检测”# HikariCP 示例 spring: datasource: hikari: leak-detection-threshold: 60000 validation-timeout: 3000leak-detection-threshold设置成 60 秒意思是连接被持有超过 60 秒就会输出一条告警日志内容包括获取连接的位置。这个参数对线上性能影响很小建议长期开启。如果用的是 Druid也有类似机制spring: datasource: druid: remove-abandoned: true remove-abandoned-timeout: 180 log-abandoned: true不过提醒一句remove-abandoned是强行回收连接效果简单粗暴但如果是业务代码里事务还没结束就被回收会导致数据一致性问题。我更推荐先开检测报警定位到代码位置修掉泄漏同时把物理连接超时时间收短做双保险。3.3 sleep 连接堆积MySQL 端参数没配对有时候应用代码没有问题连接池配置也合理但 MySQL 端的连接数依然持续走高。这时候要看的参数就是wait_timeout和interactive_timeout。在 MySQL 里非交互式连接的默认wait_timeout是 28800 秒也就是 8 小时。如果应用侧连接池设置的连接最大存活时间大于 8 小时或者应用侧干脆没有设置连接存活上限就会出现一个现象连接池里的连接已经被业务“抛弃”了但 MySQL 还不知道傻傻地给它们保持着状态。这种情况下我建议做两层收紧MySQL 端把wait_timeout调整到 60~300 秒之间比如SET GLOBAL wait_timeout300;让服务端主动清理闲置连接。应用端连接池参数里也需要设置连接最大存活时间和空闲超时比如 HikariCP 的maxLifetime设为 180 秒左右idleTimeout设为 60 秒左右。为什么要两层都做因为只调 MySQL 端应用连接池并不知道服务端已经断开了连接它从池里取到的可能是一个“坏死”的物理连接要去执行 SQL 时才发现连接失效又得重建连接增加额外开销。两边都设好连接才能真正高效地循环利用。3.4 突发流量打穿连接池这是最紧张的一种场景不是有什么 bug纯粹是流量瞬间冲到连接池上限。比如秒杀活动、热点事件业务请求量在几秒内翻几倍连接池 50 根本不够用排队的人越来越多最后表现为整个服务不可用。这种情况下你要做的不是死磕连接池大小而是先限流再扩容。Nginx 层、网关层、或者 Sentinel 这类限流组件都可以快速挡一波流量。优先保证核心交易接口可用非核心接口直接返回“系统繁忙”。等流量高峰过去后再用监控数据复盘连接池峰值用到了多少如果持续逼近上限说明连接池容量规划偏小如果只高了一下就回落可能是瞬时尖刺添加一个“预热”逻辑或者扩大一点池子容量就能解决。3.5 锁阻塞导致连接排队最后一个容易忽略的根因是数据库锁。比如事务 A 更新了某行数据但一直没提交事务 B 想去更新同一行就会在Waiting for lock状态排队。看起来像是连接被占满实际上是一个长事务把锁攥在手里后面所有操作这一行的请求全部阻塞。排查手法很简单SELECT * FROM information_schema.innodb_trx\G SELECT * FROM information_schema.lock_waits\G SELECT * FROM information_schema.innodb_lock_waits WHERE requesting_trx_id ...\G重点看trx_started字段如果事务启动时间很长而且一直没有提交基本就是长事务导致锁等待堆积。处理方法是把长事务杀掉或者在代码层对耗时较长的业务逻辑做拆解绝不能让一个事务里既查数据、又调外部接口、又写库。4. 参数配置与调优参考4.1 应用侧连接池参数怎么设连接池参数没有统一的“最佳实践”得结合业务接口的 QPS、单个 SQL 的执行时间和部署实例数来算。我以 HikariCP 为例给出一个常见的初始配置基线参数建议值说明maximumPoolSize10~50单实例最大连接数不要盲目设大minimumIdle等于maximumPoolSize避免连接频繁创建销毁实际按需调整maxLifetime180000 ms3 分钟必须小于数据库wait_timeoutidleTimeout60000 ms1 分钟空闲连接回收时间connectionTimeout3000 ms获取连接的超时时间在这个值内排队leakDetectionThreshold60000 ms连接泄漏监控阈值一个很常见的运维错误是某个应用高峰期连接池 50 就用得刚刚好结果一扩实例50 乘以 10 个实例数据库就受不了了。所以我反复强调要按“总连接数 实例数 × 单实例最大连接数”来估算并且给数据库连接留出至少 20% 的余量。4.2 数据库侧连接参数怎么调MySQL 端主要调这几个参数max_connections默认 151生产环境通常调到 300~1000。注意不是越大越好每个连接都会占用内存默认情况下每连接大约需要 2~3 MB 内存1000 个连接就是 2~3 GB。wait_timeout非交互连接闲置超时建议 60~300 秒。interactive_timeout交互式连接闲置超时一般同步调整为wait_timeout的值。thread_cache_size线程缓存建议设置为 16~64避免频繁创建销毁线程。调整示例SET GLOBAL max_connections 500; SET GLOBAL wait_timeout 300; SET GLOBAL interactive_timeout 300;注意SET GLOBAL只对新建连接生效部分参数需要写入配置文件才能在重启后保留。另外不要只调连接数上限还要关注数据库的 CPU、内存、磁盘 IO 能不能撑得住这个连接规模。连接数只是表象底层资源不够照样会挂。4.3 容量评估连接数怎么算这里提供一个简单的容量评估公式我现在新接一个项目都会先按这个过一遍预估峰值 QPS × 单请求平均耗时秒 需要的并发连接数举例系统峰值 QPS 1000接口平均耗时 200 ms那么理论上同时在进行数据库操作的连接数为1000 × 0.2 200。假设部署 4 个实例每个实例连接池上限设置为 60总连接数就是 240刚好覆盖需求还留了一点余量。但如果接口平均耗时因为慢查询变成 2 秒同样的 QPS 下需要的并发连接就变成 2000这已经不是调连接池能解决的了。所以这个公式也可以反过来用当你发现“连接池怎么加都不够用”的时候回过头去看 SQL 耗时是不是已经失控了。5. 长效防护让连接池不再成为事故爆发点5.1 监控与告警提前发现要比紧急止损更重要解决一次连接池爆满不难难的是以后不再发生。我总结了三个等级的监控规则你可以直接照着配置监控指标告警阈值处理建议应用连接池active达到池上限的 80%持续 5 分钟检查慢查询、锁等待、连接泄漏数据库Threads_connected达到max_connections的 75%持续 5 分钟按来源维度拆分定位占用方Threads_running连续 10 分钟超过 30优化 SQL排查长事务Connection refused或超时获取连接任何一条都算立即告警进入排查流程死亡连接/不活跃连接波动明显爬升调整连接池参数检查网络抖动很多团队只监控数据库 CPU 和磁盘忽略了连接数本身的监控。实际上连接数问题往往比 CPU 问题更早暴露也更容易成为一个系统性的信号连接数突然上涨通常意味着某条 SQL 变慢、某个外部服务变慢或者出现了连接泄漏。5.2 优雅降级与重试策略别让连接池成为雪崩起点连接池爆满最可怕的一点是“雪崩效应”数据库连接被打满应用拿不到连接请求超时超时的请求继续重试重试又继续拿连接形成一个正反馈的死循环最终整个应用集群都拖垮。我在设计系统时会强制要求所有数据库访问接口都具备快速失败能力。连接池connectionTimeout设 3 秒超过就抛异常而不是无限等待。同时重试要严格控制数据库操作的重试次数最多 1 次并且要配合退避策略。绝对不要写成“失败就循环重试”的代码那等于给事故火上浇油。另外可以考虑在连接池下方再加一层“熔断”的逻辑当某个核心接口的数据库连接等待率超过一定比例时直接放弃非核心接口的数据库访问优先保障支付、登录等核心链路。5.3 定期巡检和压测把问题消灭在流量进来之前连接池爆满这类问题最好的解决时机不是事故发生时而是大促前、发版前的巡检。我建议每隔一段时间做一次全链路排查检查所有应用的连接池配置确认maxLifetime小于 MySQLwait_timeout全量开启连接泄漏检测观察日志里有没有Leak detection告警在测试环境做一次连接数压测模拟平时的 2 倍流量观察连接数曲线和接口响应时间每次发版前重点检查改动过的代码里有没有“获取连接后未归还”的风险点特别是异常分支和事务边界。压测时我习惯加一个动作在压到一半时手动把某个应用的连接池上限临时调小看看系统会不会因为连接不足而引发级联故障。这样才能真正摸清系统在极端情况下的表现而不是只在顺风环境下做验证。最后分享一个实际经验连接池爆满表面上是个技术问题但处理的时候一定要冷静。先保存现场、再定位根因不要一上来就盲目重启。很多问题重启后就“消失”了但下次流量一来还会原样上演。当你把连接池监控、参数基线、泄漏检测、应急预案都配齐了再遇到“数据库连接数爆了”这种情况就不至于手足无措了。