资讯动态

MySQL连接池爆满排查指南:从告警到快速恢复

发布时间:2026/10/18 4:40:06 来源:尧图企业网站定制
周五下午三点十七分告警群突然炸了。运维直接在群里喊了一句MySQL 数据库连接池爆满登录接口全都超时了。我翻出监控面板Threads_connected已经长时间顶在max_connections上数据库直接拒绝新连接应用侧刷出来清一色的Too many connections。从核心交易接口到后台管理系统凡是需要落到这台库上的流量几乎全卡在“获取数据库连接”这一步超时、报错、失败率飙升连带一堆依赖它的下游服务也跟着遭殃。这不是我第一次处理类似的事故了。说实话MySQL 连接池爆满算是数据库运维里最有“压迫感”的故障之一现象明显、影响面大而且根因往往不止一个。稍微处理不慎要么是数据库被拖垮要么是应用起来以后再次被打满反复折腾好几个小时。这篇文章不打算从教科书概念讲起直接结合我实际踩过坑的排查过程把“从报警到大恢复”的完整链路、每一步命令、每一个判断逻辑、以及那些常规文档里不会写的细节完整展开来讲。1. 事故现场连接池爆满的表现与影响1.1 从一条告警说起先聊聊最直接的触发画面。连接池爆满最典型的信号就是应用报错里出现Cannot get connection、TimeoutException或 MySQL 驱动的Too many connections。这三个报错出现的顺序通常有规律连接池内部排队等待超时在前MySQL 自身拒绝新连接在后。如果你先看到大量Too many connections说明连接池外的连接包括运维工具、定时任务、其他服务直连已经把 MySQL 的连接数上限占用了应用连接池即使想申请也申请不到。我当时看到的第一个异常是 HikariCP 的connectionTimeout超时。日志里每秒钟刷几百条HikariPool-1 - Exception during pool initialization java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 30000ms紧接着就是SQLException: Too many connections。这说明两个层面都出了问题连接池没有空闲连接可用数据库本身也到了连接数上限。这两个问题往往是先后触发的不能只看后一个就只处理 MySQL更不能只改连接池参数就以为万事大吉。1.2 连接池爆满给业务带来的连锁反应连接池一旦打满影响是“链式放大”的。第一层是直接依赖该数据库的应用接口超时第二层是这些接口所在的服务线程池被占满因为 Tomcat 或业务线程在等待数据库连接时不会立刻释放线程第三层是调用方看到下游超时开始触发重试重试流量又反扑回来把数据库和连接池进一步压死。我记得有一次排查时数据库连接数才两千多但整个服务的线程数已经涨了快三倍。很多线程挂在socketRead或WAITING状态卡在等待连接的队列里。这个连锁反应还会把一些无关功能拖下水比如同一个连接池被订单、库存、用户三个业务共用仅仅是某个查询慢就可能让另两个业务完全不可用。所以在实际处置时第一优先级永远是“先止血、保核心接口”不要一上来就埋头找根因。2. 根因分析为什么连接池会被打满2.1 两类常见根因连接泄漏与突发流量根据我的经验连接池爆满的根因八成逃不出下面两类连接泄漏(Connection Leak)和突发流量冲击。连接泄漏是指业务代码申请了连接但没归还或者事务异常后连接没有被正确关闭。比如代码里用了Connection手写了PreparedStatement在业务异常路径上忘了调用close()又比如用了Transactional但内部调用了别的方法导致事务过长连接持有时间比预期长得多。泄漏的特征是连接数在流量平稳时也会缓慢上升MySQL 的Threads_connected曲线是一条逐渐向上的斜直线积少成多直到某一天突然触顶。突发流量造成的爆满则不一样。它的特征是连接数曲线在几分钟内直上直下往往伴随着特定接口的大幅流量上涨、慢查询增多或者外部系统大规模重试。我曾经遇到过一次一个新上线的数据报表功能跑了一批大查询每个查询都要跑二三十秒连接被长事务长期占用连接池很快就空了。如果只看“连接数过多”的结论去扩大连接池反而会让 MySQL 上的并发变高越扩越慢。2.2 慢SQL是如何把连接占满的很多人不理解慢 SQL 和连接池爆满之间的关系其实链路很清晰一个慢查询占用一个连接的时间很长假设连接池上限 50业务每秒要执行 200 个查询每个查询只要执行时间超过 250 毫秒连接池就会持续排队。如果数据库机器 CPU 打满查询执行时间会从 250 毫秒恶化到几秒那么即使不是恶意流量连接池也会在几十秒内被打满。慢 SQL 的隐患还在“后置发现”。业务性能压测时可能查的是单行主键线上真实场景是复杂多表 join、深度分页、大范围 in 查询执行计划走上全表扫描。这类问题积累在代码里平时看不出来一旦某个大促、定时任务触发连接占用迅速放大。排查连接池爆满的时候不看慢查询只看连接数基本等于白查。2.3 配置不当连接池与MySQL参数之间的“摩擦”第三种根因很隐蔽连接池参数和 MySQL 参数互相“打架”。比如 MySQL 的wait_timeout默认是 8 小时而连接池认为连接一直有效长时间不检查空闲连接一旦网络设备、MySQL 或者应用中间有连接被服务端关闭连接池里的连接已经成为死连接应用拿到后执行查询直接报Communications link failure再重试时吞吐量骤降连接数反而越堆越高。另一个常见摩擦是连接池的maxLifetime大于 MySQL 或中间设备的连接空闲超时。HikariCP 建议maxLifetime要小于数据库和网络设备的wait_timeout否则连接可能会被数据库提前断开而连接池还蒙在鼓里。Druid 这边的timeBetweenEvictionRunsMillis、testWhileIdle、validationQuery如果设置不当同样会出现在空闲回收时反而增大了获取连接的延迟。这类配置问题往往不是“爆满”的直接触发点但会在故障恢复时拉长战线值得单独排查。3. 排查思路用最快的路径定位问题3.1 第一件事先看进程列表连接池爆满时稳定心态后我会严格按照“先看进程、再看状态、后看代码”的顺序来自己检查顺序错乱会浪费大把时间。第一步是登录 MySQL 执行下面这条命令SHOW FULL PROCESSLIST;不仅要看state还要看command、time、info和host。我见过不少人排查时用SHOW PROCESSLIST结果info字段被截断看不到完整 SQL等于白看。优先关注commandSleep且time很大的连接以及commandQuery且time不断增长的连接。如果连接列表太多可以直接用information_schema.PROCESSLIST做聚合分析SELECT command, COUNT(*) AS cnt, MAX(time) AS max_time, ROUND(SUM(time) / COUNT(*), 1) AS avg_time FROM information_schema.PROCESSLIST GROUP BY command, state ORDER BY cnt DESC;这个结果能快速告诉你是大量 Sleep 空闲连接在堆还是大量 Query 正在并发执行。前者更偏向连接泄漏、连接池配置或客户端未关闭连接后者更多是慢 SQL、热点 SQL 或数据库资源瓶颈。3.2 检查连接池状态与SQL执行情况第二步查看 MySQL 全局状态确认当前到底处于什么水位SHOW GLOBAL STATUS LIKE Threads_connected; SHOW GLOBAL STATUS LIKE Threads_running; SHOW GLOBAL VARIABLES LIKE max_connections; SHOW GLOBAL VARIABLES LIKE wait_timeout; SHOW GLOBAL VARIABLES LIKE interactive_timeout;Threads_connected当前所有客户端连接数包括连接池创建好的连接。Threads_running当前正在执行 SQL 的线程数最能反映数据库真实忙碌程度。max_connectionsMySQL 允许的最大连接数默认通常很小需要根据实例规格调整。如果Threads_connected接近max_connections而Threads_running并不高那大概率是连接泄漏如果Threads_running也很高那问题多半出在 SQL 层面或硬件资源层面。接着可以按来源 IP 聚合看哪些端口的连接最多SELECT SUBSTRING_INDEX(host, :, 1) AS host_ip, COUNT(*) AS cnt FROM information_schema.PROCESSLIST GROUP BY host_ip ORDER BY cnt DESC;同时也可以查看最近一段时间按平均耗时排序的 SQL 摘要SELECT SCHEMA_NAME, DIGEST_TEXT, COUNT_STAR, ROUND(AVG_TIMER_WAIT / 1000000000000, 3) AS avg_sec, ROUND(MAX_TIMER_WAIT / 1000000000000, 3) AS max_sec, ROUND(SUM_ROWS_EXAMINED / SUM_EXEC_COUNT, 2) AS avg_rows FROM performance_schema.events_statements_summary_by_digest ORDER BY avg_sec DESC LIMIT 20;这里单位是皮秒除以 1 后面 12 个零就是秒。看到某个 SQL 平均执行时间几秒基本就能把它锁定为本次连接打满的重要帮凶。3.3 锁定连接泄漏代码视角如果进程列表里全是 Sleep 连接接下来要把问题定位到应用代码。最容易泄漏的写法是“手动拿连接但异常分支没关闭”比如下面这类伪代码Connection conn null; try { conn dataSource.getConnection(); // 业务逻辑 } catch (SQLException e) { // 只记日志没有关连接 }正确的做法是使用带资源的try-with-resources或者至少保证conn.close()在finally里。我知道很多团队用的是 MyBatis、Hibernate 或 JPA框架层面一般不会漏但手写 JDBC 的旧项目以及调用存储过程的代码里这类问题是重灾区。还可以在应用侧做一次快速统计用 jstack 抓线程栈看看有没有大量线程卡在获取连接或 SQL 执行上。通常等到线程栈里能看出是同一个业务方法比如所有线程都在同一个 Mapper 方法上那再结合 SQL 文本就能锁定到具体代码了。当然给 HikariCP 打开leakDetectionThreshold是一个长期有效的手段设置后连接被占用超过阈值但未归还时日志会打印连接获取时的堆栈这比事后翻代码高效得多。4. 解决方案止血、根治、恢复4.1 立即止血临时扩容与定向清理先声明一个原则任何临时手段都必须评估对业务的影响。最直接的“扩容”方案是把 MySQL 的max_connections调大但如果你只有一台数据库并且自身资源已接近上限盲目调大只会让数据库更快崩溃。max_connections调的每一档都应该结合数据库的内存和 CPU 来算不能拍脑袋。一种相对安全的止血操作是先定向清理空闲连接。对大量处于Sleep状态且持续时间超过阈值比如 30 秒以上的连接可以先在从库或业务低峰环境下测试然后批量终止SELECT CONCAT(KILL , id, ;) FROM information_schema.PROCESSLIST WHERE command Sleep AND time 30 LIMIT 5000;把输出的 SQL 复制出来执行即可或者在 MySQL 命令行里用存储过程循环处理。这里要特别提醒一句千万不要一次性把所有 Sleep 连接都干掉。应用连接池里的空闲连接本身也是 Sleep全杀了以后数据库认为连接断开应用侧要重新建连瞬间会有一大波建连请求飞到数据库可能把数据库又压出新的瓶颈。我见过有同事图省事执行了KILL CONNECTION的一刀切脚本结果连接数十分钟没降下来应用线上反而出现大量超时。填写 MySQL 本身参数的时候要注意max_connections的修改分两类临时方案用全局动态修改重启后失效SET GLOBAL max_connections 800;如果确认要长期保留记得同步修改配置文件里的max_connections。还有一类做法是修改wait_timeout把空闲连接超时缩短比如SET GLOBAL wait_timeout 60; SET GLOBAL interactive_timeout 60;这个动作能让 MySQL 主动回收长时间不用的连接但它对已经建好的老连接不会立刻生效而且只针对后续新建立的会话。所以它更多是“配合”手段不能当主要止血手段。4.2 根治连接泄漏与异常处理临时把水位降下来以后必须解决连接泄漏。首先是代码层面的修复把所有手写 JDBC 获取连接的地方全部改成 try-with-resources 或保证finally关闭。第二是给连接池开启泄漏检测HikariCP 配置如下spring: datasource: hikari: leak-detection-threshold: 60000 connection-timeout: 30000 maximum-pool-size: 50 minimum-idle: 10 max-lifetime: 1800000leak-detection-threshold: 60000表示连接被占用超过 60 秒未归还日志里会输出获取连接时的堆栈。这个参数只对从池中借出后未归还的连接生效不会影响正常长时间事务。需要注意的是开启后有一定的性能开销生产环境建议设置在 60 秒以上不要设得太小造成误报。另外连接泄漏往往和事务滥用有关。比如Transactional加在 Service 类的方法上内部做了远程调用、文件上传、MQ 发送导致数据库事务长时间不释放。这类问题只靠关闭连接没用必须把非数据库操作移出事务或者事务边界内绝不执行耗时操作。4.3 调整连接池配置连接池参数不是越大越好。很多人以为把maximum-pool-size调成 500 就一定能让业务更快实际上连接池过大数据库侧并发也会抬高上下文切换加剧锁等待扩大整体查询延迟反而恶化。这里需要引入一个连接数估算思路假设一个库实例上有 20 个应用节点每个节点连接池maximum-pool-size 50那 MySQL 侧最多可能建立 1000 个连接。如果数据库max_connections设置的是 500那么连接池还没跑满 MySQL 就会先拒绝连接。因此连接池大小、应用节点数量、MySQLmax_connections三者必须联动设计。我有一个比较务实的建议对于绝大多数 OLTP 型系统单实例连接池不要配置超过 100最好控制在 10 到 50 之间可以通过压测找到一个“连接数增加但 QPS 不再上涨”的点。若应用是多实例部署连接池数量要按“单实例池大小 × 实例数 预留连接”来算比如 20 个实例、每个 30预留 100 给运维监控和管理用那么max_connections至少要有 700 以上留 10% 到 20% 的余量。当紧急恢复时临时把连接池调大的场景我已经被折腾过很多次了。因为连接池调大后应用创建的连接是增量增长的数据库侧如果能承受就没事数据库不能承受就会加剧数据库负载。比较稳妥的做法是先确认数据库当前Threads_running不高再小幅调整连接池大小观察 5 分钟不要一步到位从 50 调成 300。4.4 优化MySQL服务端参数MySQL 侧和连接池强相关的参数主要就是下面几个参数默认值建议思路max_connections151根据实例规格和连接池整体规模设置同时注意操作系统ulimit -n上限wait_timeout28800建议与连接池的maxLifetime联动通常 60-300 秒即可但要兼顾定时任务等业务场景interactive_timeout28800对交互式客户端生效一般保持和wait_timeout一致或稍大thread_cache_size9如果连接频繁创建销毁可以适当调大减少线程创建开销max_connect_errors100防止大量连接错误导致 IP 被屏蔽可以根据安全要求适当放大修改这些参数一定要结合压测结果不能照抄网上的“万能配置”。比如某些业务有定时任务一天只在凌晨跑一次连接空闲时间可能很长如果wait_timeout设成 60 秒连接池里所有空闲连接会在业务低峰被 MySQL 全部清理第二天定时任务集中来临时应用侧大量重建连接可能瞬间把数据库连崩。所以wait_timeout要大于连接池的maxLifetime并且大于最长空闲等待时间一般来说 180 秒到 300 秒在 Web 系统里比较常见。还有一点容易忽略的是max_connections不仅受 MySQL 自身限制还受操作系统的文件描述符限制。如果系统里ulimit -n是 1024那么 MySQL 最多也就同时维持几百个连接即使改了max_connections4096照样会拒绝连接。排查时可以查看 MySQL 进程 fd 数量ls /proc/$(pidof mysqld)/fd | wc -l如果这个数已经接近ulimit -n连接数必然上不去。在配置文件里设置LimitNOFILE65535再重启才能让 MySQL 真正支持更多连接。4.5 处理慢SQL和热点查询把连接水位降下来之后如果不处理慢 SQL故障大概率会复现。我从头到尾对待慢 SQL 的思路是三步先抓出来、再分析执行计划、最后做针对优化。抓慢 SQL 最简单的是打开慢查询日志SET GLOBAL slow_query_log ON; SET GLOBAL long_query_time 2;线上排查阶段可以先通过performance_schema的语句汇总表定位高频慢语句再对单个 SQL 执行EXPLAIN SELECT ... \G重点看type列如果出现ALL全表扫描或index全索引扫描且表行数很大那基本就是慢的元凶。常见的解决手法包括补复合索引、改写 join 逻辑、把大范围IN查询拆批、把聚合统计类 SQL 丢到从库或单独的分析库执行。我印象最深的一次导致连接池打满的是一条分页查询LIMIT 100000, 20每次深层分页都要扫描前面十万行。后来把分页方式改成基于 ID 条件的“游标分页”扫描行数从十万级降到几十行连接占用瞬间降低了一半以上。这类优化在常规压测里很难发现但是线上非常致命。5. 监控与预防别等到爆满才去救火5.1 连接池关键指标监控理想情况下连接池爆满之前就应该有报警先到。最要紧的四个指标是活跃连接数 / 连接池当前大小等待获取连接的平均耗时等待获取链接的超时次数数据库侧Threads_connected与Threads_running监控工具有很多但更重要的是阈值设定。连接池使用率不建议只看“是否接近 100%”更好的报警线是“连接池使用率达到 80% 并且持续 5 分钟以上”。等待获取连接的平均耗时一旦超过几百毫秒说明连接池已经明显吃紧这个时候还没打满是介入的最佳窗口。另一个常用的服务端监控数据是SHOW ENGINE INNODB STATUS里的事务信息重点看事务持有行锁的时间以及是否有长时间未提交事务。长时间未提交事务和连接泄漏是同义词的场景但视角不同它会导致连接被长时间占用也可能带来 undo log 膨胀和主从延迟值得单独拿出来看。5.2 定期巡检与容量评估连接池问题的本质是容量管理和代码质量所以预防手段也要从这两方面落。容量评估我用过比较笨但有效的办法每次大促或版本发布前都做一轮全链路压测压测期间分时段记录连接池使用率、活跃连接数曲线、数据库Threads_running曲线观察是否存在“连接数在高位徘徊但 QPS 没有明显提升”的阶段。定期巡检可以考虑纳入以下动作每周检查一次数据库连接数曲线特别是是否有缓慢上升的泄漏趋势。每周抓取一次慢查询日志对比慢 SQL 的数量和平均耗时关注新增的慢语句。每月检查一次连接池参数和 MySQL 参数的匹配情况判断是否需要同步调整。每次发布前审查变更里有关数据库访问的代码重点看事务注解、批量循环 SQL、嵌套调用。我还习惯把连接池监控和告警直接接入整个监控系统而不是只看 MySQL 本身。因为连接池的优势指标往往比数据库侧指标早出现 10 到 30 秒。这几十秒的提前量足够在很多场景下避免一次线上故障。6. 常见问题与排查技巧实录6.1 遇到过的怪问题与解决办法下面这几个问题是我在实际处置中反复遇到的可以作为速查参考现象可能原因解决思路连接池状态正常但应用查询偶发Communications link failureMySQL 或中间设备主动断开空闲连接调低连接池maxLifetime确保小于服务端wait_timeout开启连接有效性检测调整max_connections后重启 MySQL 失效只改了全局变量没改配置文件修改配置文件并检查ulimit -nThreads_connected不高但应用报连接不够某个应用节点的连接池泄漏或池子配置过小查看每个节点的连接池使用率和线程栈数据库 CPU 占用很高连接数也在涨慢 SQL 或热点行锁用慢查询日志定位 SQL分析执行计划并优化索引必要时限流大量连接处于Sleep但应用侧连接池空闲连接并不多有其他系统或工具直连 MySQL占用连接后不释放按host聚合连接来源确认直连方并推动接入连接池连接池数量调整后数据库反而变慢连接数增加导致并发飙升、锁竞争加剧回退连接池配置优先排查 SQL 层面的瓶颈6.2 处理连接池爆满时千万别踩的坑第一不要在故障高峰期反复重启应用。连接池爆满时应用里堆积了大量等待线程重启虽然会临时把连接池清掉但重启后的应用会在很短时间内把所有堆积流量重新投递到数据库包括大量重试请求。数据库在已经承载力不足的情况下还要面对建连风暴很容易再次被打爆。第二不要为了“省事”把连接池的获取超时时间调得很大。比如connection-timeout原本是 3 秒故障时一着急改成 60 秒业务线程在这个时间段内全部阻塞等待线程池瞬间耗尽。更合理的策略是缩短超时时间让请求尽快失败这样流量端会以更快速度熔断降级反而保护了数据库。第三不要在没看Threads_running的情况下直接调大连接池。200 个连接打满是因为 20 个慢查询占着你把池子调到 1000意味着同一时间可以有更多慢查询一起跑数据库 CPU 只会更忙连接数会继续往上顶。连接池扩容的唯一前提是数据库还有空闲资源判断依据就是Threads_running没满、CPU 没打满。第四连接泄漏的修复不是“关掉异常 SQL”就结束。很多连接泄漏发生在重试机制里比如代码里用 for 循环重试插入每次循环都拿了一个新连接循环没有回收连接池里的连接。这类代码写得很隐蔽必须结合 HikariCP 的泄漏检测堆栈来查靠肉眼 review 基本看不出来。6.3 一个可复用的快速恢复清单为了方便以后快速响应我整理了一个恢复顺序强烈建议在故障演练里跑一遍确认 MySQLmax_connections和当前连接数先评估是否马上会拒连。根据Threads_running判断是连接堆积还是 CPU 瓶颈。如果是大量 Sleep 连接批量清理超过 30 秒的无人使用连接。如果是大量慢查询优先在数据库侧限流或终止长时间运行的查询同时通知业务方降级。确认数据库负载降低后再评估是否临时调整连接池大小和max_connections。恢复稳定后从代码层修复连接泄漏、优化慢 SQL、调整配置。复盘并补充监控告警避免下次靠人来发现。这套流程我每次都会在故障复盘后更新现在已经稳定用了几年。你不需要完全照搬但至少应该提前把“谁有权在故障时执行 kill 连接”“最大能调到多少连接数”这些决策权明确下来否则到现场大家都在等审批几个小时的故障时间就浪费在流程上了。最后说一句个人体会连接池爆满并不可怕可怕的是每次都只做临时扩容、不查根因。如果你把连接池理解成应用和数据库之间的“水管”爆满那根水管永远不只是水管本身的问题——要么是后面的水池数据库太慢要么是前面的水泵应用代码一直在漏水。这也是我写这篇内容的初衷希望下次你遇到同样的告警能少走几个弯路。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑