
公司业务半夜报警数据库连接池直接打满用着好好的服务突然就“Too many connections”随后页面超时、接口504紧接着一堆任务队列堆积告警。这种情况我处理过不止一次每次原因都不完全相同但排查思路是共通的。这篇就完整回顾一次连接池爆满的排查过程和修复手段把每一层的判断逻辑、关键命令、避坑点全部捋一遍。不管你是开发、DBA还是运维看完都能直接照着这套流程去定位。先说清楚一个概念连接池爆满不是数据库本身挂掉而是“能建立连接的通道”被占满了。就好比酒店前台只有10个接待窗口所有客人挤在窗口前不走后面来的人连取号的机会都没有。MySQL 默认的max_connections通常是 151不同版本略有差异一旦并发请求同时占用的连接数达到上限新的连接请求就会直接报错。应用层如果用了 HikariCP、Druid、C3P0 这类连接池还会叠加一层自己的连接上限两层限制一起挡住反应到业务上就是“连接不到数据库”“服务不可用”。1. 先看现象再定排查方向1.1 报错信息反推问题层面我见过的连接池爆满报错大致分三类每一类对应的处理起点都不一样。第一类是中间件报错比如 HikariCP 抛出Connection is not available, request timed out after 30000ms或者 Druid 报wait millis 30000, active 200, maxActive 200。这类报错说明应用层连接池已经把所有连接都借出去了且等待队列里还有请求在排队30秒没等到空闲连接就超时。问题出在“应用拿不到连接”但根因可能在应用自身也可能在数据库端——如果慢SQL把连接长期占住连接池再大也会被打满。第二类是数据库端报错ERROR 1040 (HY000): Too many connections。这是 MySQL 自己到达了max_connections上限直接拒绝新连接。这时候应用连接池申请新连接也失败于是应用侧同步触发连接池排队超时。这条报错能确定问题在数据库侧但数据库侧连接多有可能是业务流量增长、慢查询堆积、长事务未释放也可能是排查者自己在命令行连不上去。第三类最隐蔽是连接池内核健康但接口还是慢。比如连接池正常但 MySQL 的 CPU 打满、磁盘IO异常SQL 执行速度变慢导致单个连接被占用的时间变长连接周转率下降在并发量不变的情况下连接数被慢慢推到高位。这种情况连接池和max_connections都没到上限但业务感知到的是“数据库越来越慢”最终也会爆。拿第三条举例数据库 CPU 使用率 99% 时一条原本1毫秒的查询可能变成1秒。如果业务并发是200连接池上限也是200每个连接都被慢SQL霸占新的请求只能在池外排队。池子看似没爆实际已经“功能性爆满”了。1.2 一边查一边压——排查命令怎么用进入服务器终端后先别急着重启MySQL重启虽然能临时清掉连接但根因没找到过一会儿又会爆。首要任务是抓现场。连接不上数据库时先用一个“逃生窗口”登录。MySQL 会给super权限的账号预留一个连接通道即使max_connections已满SUPER权限账号依然可以连进去。用 root 或者有SUPER权限的账号执行mysql -u root -p -e SHOW STATUS LIKE Threads_connected; mysql -u root -p -e SHOW VARIABLES LIKE max_connections;这两个命令分别看当前已用连接数和最大连接数。如果Threads_connected已经贴近max_connections那就是物理连接被占满。再看连接都是从哪来、在干什么SELECT user, host, db, command, time, state FROM information_schema.processlist ORDER BY time DESC;这个查询把当前所有连接按执行时间倒序排优先看time特别大的会话。如果发现大量Sleep状态的连接说明应用拿完连接不释放或者连接空闲超时时间设置太长如果大量连接卡在Query状态且time很大多半是有慢SQL在跑如果连接数不大但都在Locked状态要考虑锁等待。我个人的习惯是第一轮先抓三样东西processlist、慢查询日志位置、连接数变化曲线。有了这三样大部分问题能定位到具体范围。2. 按三层拆解根因——应用层、数据库层、网络层2.1 应用层连接泄漏、参数配置、连接池大小连接泄漏是应用层最常见的爆池原因。所谓泄漏就是应用从连接池借了连接用完没有归还。每次请求泄漏1个在高并发下几分钟内就能把池子填满。代码里常见的泄漏场景有从连接池获取连接后在 try 块里执行SQL但 finally 没有调用close()使用TransactionTemplate或者Transactional时事务内抛异常但连接未正确释放多线程并发使用同一个Connection对象用完只关了一个副本使用JdbcTemplate查询返回ResultSet后没有关闭Statement和ResultSet。排查时重点看连接池的活跃连接曲线。Druid 有监控页面HikariCP 可以通过HikariPoolMXBean拿到getActiveConnections()和getIdleConnections()。如果活跃连接数持续高位不下降基本可以断定有代码路径在泄漏连接。还有一个土办法把连接池的最大连接数调到一个相对小的值观察报错时应用的线程堆栈用jstack抓线程看哪个线程一直持有着连接对象。应用层参数配置也是重灾区。常见的坑是maximum-pool-size设得过大。很多团队喜欢把连接池上限设成 500、800 甚至 1000觉得越高越好。实际上连接池越大数据库侧的max_connections也要跟着调大连接本身的创建和销毁都有开销。大量空闲连接也会占用数据库内存。更重要的是连接池设置过大并不等于 QPS 上限高——如果 SQL 执行慢再多的连接也只是把等待队列从应用层挪到了数据库层。连接池大小的核心是保证“并发执行中的SQL数量”超出数据库能并行处理的量多余连接全是排队。比较合理的配置思路是先压测单条核心SQL的耗时估算数据库能支撑的并发查询数SHOW ENGINE INNODB STATUS能看到并发线程数再按这个数字设置连接池的上限。别盲目堆连接数。在连接池参数上还有几个高频坑位要排查connectionTimeout/maxWait设置太短比如 HikariCP 的connectionTimeout默认30秒如果业务 SQL 偶尔有慢查询连接池请求就会大量超时引发雪崩idleTimeout和maxLifetime不匹配比如maxLifetime默认30分钟但idleTimeout设成了10分钟连接刚被创建、空闲不到10分钟就被回收又马上被新请求建连造成频繁连接创建validationTimeout设得太短导致连接校验经常失败主动断掉健康连接。2.2 数据库层连接数配置、慢查询、锁等待数据库层的排查顺序建议是连接配置 → 慢SQL → 锁状态 → 大事务。先看配置SHOW VARIABLES LIKE max_connections; SHOW VARIABLES LIKE wait_timeout; SHOW VARIABLES LIKE interactive_timeout; SHOW VARIABLES LIKE thread_cache_size;wait_timeout指非交互连接的空闲超时时间默认8小时。如果应用连接池没有正确回收空闲连接很多连接池默认不处理空闲或者空闲超时设得比数据库还长大量Sleep连接会堆积直到把max_connections占满。这种堆积的特点非常明显processlist里全是Sleep状态、time都是几千秒、来源IP集中在应用服务器内网IP。临时处理方案是把wait_timeout调小SET GLOBAL wait_timeout 300; SET GLOBAL interactive_timeout 300;这个操作等连接被回收后生效不需要重启 MySQL但注意设小了会影响所有客户端如果应用本身存在合法的长连接业务要结合场景调整。慢SQL导致连接爆满的场景更常见。一条执行10秒的慢查询占着连接不放10条这样的查询就能把连接池的并发通道全部堵死。排查慢SQL最直接的方式是开启慢查询日志SET GLOBAL slow_query_log ON; SET GLOBAL long_query_time 2; SET GLOBAL slow_query_log_file /var/log/mysql/slow.log;定位到慢SQL之后用EXPLAIN看执行计划。如果 type 是 ALL全表扫描、rows 扫描行数巨大、没有走索引优先考虑加索引如果走了索引但 rows 还是很大就要看是不是索引区分度不够或者查询条件本身有问题。锁等待造成的连接占用也要重点排查。执行SELECT * FROM information_schema.innodb_trx; SELECT * FROM information_schema.innodb_locks; SELECT * FROM information_schema.innodb_lock_waits;如果发现长时间未结束的事务找到源头事务对应的连接通过KILL trx_mysql_thread_id终止它再观察业务是否恢复。锁等待的典型表现是processlist里多个连接同时处于Locked状态time还都很大但 CPU 和慢SQL都不明显。这类问题多源于业务代码里事务边界过大事务里除了SQL还做了HTTP调用、文件读取等耗时操作导致锁的持有时间过长。我之前遇到过一段代码在事务里调用外部接口外部接口超时5秒事务就挂了5秒表锁被5秒一度把连接池打爆。修复方式很简单把外部调用移出事务。2.3 网络层连接数被代理/防火墙耗尽很多人会忽略网络层的问题。MySQL 前面如果挂了代理比如 ProxySQL、MyCat、MaxScale、云平台的连接代理或者应用通过 LVS/HAProxy 转发那一层也有连接数限制。实例的max_connections没到上限但代理层的连接池满了业务同样报连接错误。这类问题有一个特征直接通过 MySQL 客户端从堡垒机连数据库没问题但应用连代理就报连接失败。排查方法分几个方向查代理机器的连接数——ss -s看当前 socket 数量ss -ant | grep 3306 | wc -l看当前到数据库端口的连接数查代理的连接池配置——ProxySQL 的mysql-connections参数、HAProxy 的maxconn配置查云数据库控制台的“当前连接数”监控——云厂商往往会提供连接数上限比如 RDS 默认根据规格给几百到几千不等的最大连接数超过后同样报 too many connections 类错误查防火墙或安全组规则有时候安全策略会限制单IP并发连接数比如单IP最大1000条TCP连接应用一旦达到就会被丢弃。网络层面排查的最佳方式其实是“对比法”。把应用服务器上发起的连接数、代理服务器上看到的连接数、MySQLprocesslist里的连接数三个数字放在一起三者本应一一对应。只要有一个数字明显偏少或者偏多断点就在那儿。比如 MySQL 侧只有100条但代理上有1000条连接差额就是代理上挂死未释放的。3. 实操复盘一个完整的爆池处理流程3.1 第一现场收集信息而不是盲目重启有一次排查电商系统的连接池爆满我拿到手的信息就一句话“凌晨2点大促预热订单服务数据库连接池打满订单接口大面积超时。”到了服务器上我先做了四件事看 MySQL 当前连接数、看活跃线程、看慢查询日志、看应用日志报错。当时得到的数据是Threads_connected380max_connections400活跃查询有40多个其余全是Sleep。慢查询日志里没有特别大的SQL平均查询耗时都在几十毫秒以内。按这个现场判断连接数几乎触顶但查询本身没有大问题那问题多半出在连接数量的堆积上——活跃查询只有40个却有340个空闲连接。这就不是慢SQL而是连接没有被及时回收或者并发请求瞬间冲高把池子撑满了。接着查连接来源和会话年龄SELECT user, host, COUNT(*) FROM information_schema.processlist GROUP BY user, host ORDER BY COUNT(*) DESC;结果显示来自订单服务节点A的连接有210条节点B有90条其他服务加起来几十条。而我线上订单服务的连接池最大配置只有150。A节点有210条连接说明A节点的连接池配置被改过或者有双重连接池叠加。再往下挖发现服务里接了两个数据访问组件一个是 MyBatis 的主连接池另一个是历史遗留的一个定时任务框架里的独立数据源连接池。两个池子各自配置了150叠加起来就超过了数据库侧的单机比例。组件没排查过没人知道它也占连接数。这次爆池的根因就两条一是定时任务框架在凌晨跑批批任务一次性起了大量线程并发申请连接每个线程池运行时间还很长二是我方连接池配置未联调两个连接池叠加后的并发峰值远超 MySQL 允许的400。修复动作分两步走先把定时任务改为分批次执行限制最大并发为20再把两个连接池的maxSize按实际需要重新定义主连接池120任务框架连接池30。改完后再看Threads_connected稳稳停在150以内。3.2 紧急恢复手段的优先级遇到正在发生的爆池恢复业务是第一优先级的。我的建议顺序是用SUPER权限账号登录执行KILL清掉一部分非活跃连接-- 杀掉空闲超过300秒的连接注意先确认再执行 SELECT CONCAT(KILL , id, ;) FROM information_schema.processlist WHERE commandSleep AND time 300;把查出来的 KILL 语句复制出来执行。这条操作能迅速腾出连接名额。但不建议直接在命令行一次执行大量动态SQL容易误杀有权保留的长连接。稳妥做法是查出来确认后再杀。临时调大max_connections给应用和排查留出缓冲空间SET GLOBAL max_connections 1000;这里有个注意点max_connections不是想调多大就调多大。MySQL 为每个连接分配线程和内存thread_stack、net_buffer_length等连接数越大内存开销越高。如果机器只有4GB内存max_connections调到2000光连接缓冲就可能吃掉2GB。所以调大之前用free -h看一眼内存余量结合SHOW VARIABLES LIKE max_connection_memory%或者文档给出的单连接内存估算值算一下。如果是慢SQL导致可以直接定位慢SQL对应的连接IDKILL掉对应线程SELECT id, time, user, info FROM information_schema.processlist WHERE commandQuery AND time 10\G大于10秒的查询如果是大促抢购类活动SQL可以直接终止顶掉保证核心链路先通。注意KILL SELECT 不影响数据一致性但 KILL UPDATE/INSERT 可能导致事务回滚要评估影响。如果业务能接受重启应用释放连接池中的异常连接。但是重启前先确认数据库侧没有大事务避免重启后应用重新发起请求把同一个慢SQL再打一遍。重启解决的是“连接还不上”的临时现象不是根因。3.3 事后压测验证连接池参数修复之后我习惯做一轮连接池压测验证参数是合理的。压测工具我用的是简单的mysqlslap加应用层 JMeter 组合。先说明一点压测的目的不是把数据库打爆而是验证在目标并发下连接数是否会失控。mysqlslap的用法大概是mysqlslap --create-schematest --querySELECT * FROM t_order WHERE status1 -c 200 --iterations10 --number-of-queries10000 --concurrency100这条命令模拟100个并发客户端、执行1万次查询。跑完后看数据库侧的Threads_connected峰值和查询耗时分布。如果峰值稳定在某个值附近连接池参数就没问题。更关键的是压测时要盯两个指标活跃连接数和空闲连接数。用监控脚本定期采样while true; do mysql -e show status like Threads_connected /tmp/conn.log mysql -e show status like Threads_running /tmp/conn.log sleep 2 done压测期间如果Threads_running长期超过数据库CPU核数说明SQL执行得不够快需要从SQL优化端入手而不是加连接。如果Threads_connected和活跃请求数不匹配就要从连接泄漏方向去查。4. 长期治理监控、参数调优与应急预案4.1 连接池参数的最优实践参考衡量连接池配得好不好不是看峰值有多高而是看在峰值下请求能否有序排队并及时拿到连接。这里给出一份我实际调优后比较稳妥的参数基准具体数值要根据业务压测调整连接池类型参数名建议值说明HikariCPmaximumPoolSize30~50常规业务建议不要超50压测后按峰值调整HikariCPminimumIdle10~20等于最大池时不会回收空闲连接HikariCPconnectionTimeout3000~5000连接申请等待毫秒数太长容易拖垮调用方HikariCPidleTimeout60000010分钟空闲回收必须小于数据库 wait_timeoutHikariCPmaxLifetime180000030分钟强制换连接避免连接被数据库端断开DruidmaxActive50等价于 HikariCP 的 maximumPoolSizeDruidminIdle10保留的常驻连接数DruidmaxWait3000获取连接的最大等待毫秒数连接池大小的经验公式业界有个参考基准connections ((core_count * 2) effective_spindle_count)其中core_count是数据库服务器CPU核数effective_spindle_count是磁盘数SSD 可视为1。这个公式来自 PostgreSQL 社区的推荐MySQL 同样适用。它是给高峰期算的连接数参考而不是最大值。更贴近业务的估算方式压测得到单条核心SQL的 P99 耗时比如 20ms数据库能支撑的并行 SQL 数量约等于CPU核数 × 50 ÷ P99(ms)即单核每秒钟大概能执行50条20ms的SQL。8核数据库的理想并发大约是8 * 50 / 20 20所以连接池20~30足够支撑常规流量。当然这只是估算带宽、锁、磁盘IO都会影响但至少能帮助避免“拍脑袋设500”这种操作。4.2 数据库侧参数调优清单max_connections不是唯一需要看的参数。连接池爆满了通常还涉及两个隐蔽参数第一个是wait_timeout和interactive_timeout。建议线上设置600秒10分钟这样连接池里空闲的连接会定期被MySQL断开促使连接池补建新连接避免“死连接”堆积。但同时要让应用连接池侧的idleTimeout比这个值小一些比如300秒让应用先回收而不是等MySQL掐断。第二个是max_execution_time这个参数可以给 SELECT 语句设置最大执行时间单位毫秒。超出时间的查询会被自动终止避免一条烂SQL把连接占几分钟。只对 SELECT 生效不影响事务。设置示例SET GLOBAL max_execution_time 3000;对个别慢查询也可以在语句上加/* MAX_EXECUTION_TIME(3000) */提示作用范围更精确。注意这个参数对存储过程里的 SELECT 不生效生产环境变更前先在测试环境跑一轮。第三个容易忽略的是thread_cache_size。连接频繁创建销毁时MySQL 需要为每个连接创建线程。thread_cache_size控制线程的缓存数量默认是-1等于max_connections / 2向上取整但如果连接数波动很大建议设成 50~100让被断开的连接线程能迅速复用降低建连成本。4.3 连接数监控和告警配置连接池爆满这种事靠人半夜爬起来处理不是长久之计。我的标准做法是三层监控第一层MySQL 侧监控连接数使用率。Prometheus 的mysqld_exporter直接暴露mysql_global_status_threads_connected指标用 Grafana 做曲线的阈值告警。建议告警阈值设成max_connections * 0.770% 就要出动。等到了90%再告警基本是事故已经在发生了。第二层应用侧监控连接池健康度。HikariCP 可以通过micrometer暴露指标Druid 有自带的监控页面DruidDataSourceStat。关注active、idle、waiting三个指标。waiting长时间大于0说明连接池排队了。第三层慢SQL和锁监控。mysqld_exporter提供mysql_global_status_slow_queries、mysql_global_status_innodb_row_lock_waits建议做一个独立看板。连接数告警出来时第一件事看这个看板排查是不是慢SQL引起。另外云数据库用户要特别注意控制台上的“连接数使用率”指标和“最大连接数配额”。云实例的 max_connections 往往由规格决定比如4C8G规格的max_connections是400买了大规格但临时调大了本地参数也可能被云侧策略覆盖。我的经验是用到云 RDS 时先在控制台把最大连接数配额调高再核对本地参数否则本地 SET GLOBAL 改了也白改。4.4 一套可以直接落地的应急预案脚本这套脚本我按生产环境整理了一套遇到告警直接执行不需要现场敲命令。核心逻辑统计空闲连接 → 按阈值杀掉 → 记录现场。#!/bin/bash # 连接池爆满急救脚本 MYSQL_USERroot MYSQL_PASSyourPassword MYSQL_HOST127.0.0.1 KILL_THRESHOLD300 # 空闲超过300秒的连接 # 第1步记录现场 mysql -h$MYSQL_HOST -u$MYSQL_USER -p$MYSQL_PASS -e SELECT NOW() AS time, COUNT(*) AS total_conn FROM information_schema.processlist\G /tmp/conn_crash_$(date %F_%H%M%S).log mysql -h$MYSQL_HOST -u$MYSQL_USER -p$MYSQL_PASS -e SHOW STATUS LIKE Threads_connected; /tmp/conn_crash_$(date %F_%H%M%S).log # 第2步生成杀空闲连接SQL mysql -h$MYSQL_HOST -u$MYSQL_USER -p$MYSQL_PASS -N -e SELECT CONCAT(KILL , id, ;) FROM information_schema.processlist WHERE commandSleep AND time $KILL_THRESHOLD AND user ! event_scheduler; /tmp/kill_sleep.sql # 第3步提示确认后执行 echo Generated kill script: /tmp/kill_sleep.sql. Review then execute: mysql -h$MYSQL_HOST -u$MYSQL_USER -p$MYSQL_PASS /tmp/kill_sleep.sql脚本里我故意把杀掉空闲连接拆成两步先输出SQL文件人工确认后再执行。因为有些合法的空闲连接比如长连接存活的报表服务不应该被杀一刀切容易误伤。生产环境要谨慎自动化的前提是确认过连接来源。5. 排查小结与速查表5.1 报错现象对应根因判断逻辑把遇到的爆池现象和根因对应起来形成一张速查表能省很多时间现象优先怀疑方向核心排查命令/指标Too many connectionsSleep堆积应用连接未释放 / 空闲超时太长processlist按time排序确认wait_timeoutToo many connections 大量Query且 time 大慢SQL / 无索引慢查询日志、EXPLAIN检查long_query_timeToo many connectionsLocked状态锁等待 / 大事务innodb_trx、innodb_lock_waits视图连接池超时 MySQL侧连接数正常应用连接池配置过小 / 连接泄漏活跃连接曲线、线程堆栈、代码评审应用连不上 命令行能连代理/防火墙连接耗尽代理连接监控、ss -ant、安全组规则高峰期爆 平日正常容量规划不足 / 突发流量压测峰值、Threads_connected历史曲线连接数量正常但接口依然慢CPU/IO瓶颈SHOW PROCESSLIST、top、iostat5.2 需要沉淀复盘的关键输出每次处理完连接池爆满问题建议把三样东西沉淀下来后续同类问题能做到开箱即用。第一是故障时间线精确到分钟记录报警触发、开始排查、执行干预、业务恢复、参数变更完成这几个时间点方便大促前复盘。第二是连接数峰值记录把Threads_connected的峰值、max_connections的实时值、活跃查询数、慢SQL数量都保留下来。这些数据是后续调参数的依据。没有数据支撑谁也不敢说“调到100够了”还是“要调到200”。第三是变更记录。谁在什么时间改了连接池参数、改了数据库配置、上线了新代码这些都要记录。很多连接池爆满其实就是配置变更引起的——某次上线把连接池maximumPoolSize从50改成了500某次DBA把max_connections调小了这类变更在故障复盘里是最常见的元凶。我个人处理这类问题最深的一点体会是连接池爆满了不要第一反应去“加连接”而是先问“这些连接为什么还不释放”。多数情况下连接数不是不够是流转不动。慢SQL、锁等待、空闲连接不回收才是核心症结。盲目把max_connections从400调到4000只会让数据库在故障时承担更多无效连接最终拖垮整个实例。正确的方向永远是让连接“快借快还”其次才是加容量。最后分享一个小技巧如果你用的是 HikariCP可以直接把poolName配置加上比如poolName: OrderServicePool。这样连接丢失时日志里能看到“OrderServicePool - Connection is not available...”而不是光秃秃的HikariPool-1。这个看起来不起眼的配置在排查多服务、多数据源的时候能帮你省下大量核对的时间。线上排查遇到连接池爆满日志里能直接定位到是哪个服务、哪个连接池出了问题剩下的就是按流程走了。