1. 先从一场告警说起:连接池爆满到底是什么
你有没有在凌晨三点接过这样的电话:应用监控跳出红色告警,数据库连接池耗尽,线上接口大面积超时。反正我有过,而且不止一次。今天这篇就把"MySQL连接池爆满"这件事彻底讲清楚——它本质是什么、怎么一步步排查、根因有哪些、参数该怎么调,以及我踩过哪些坑。
先说连接池本身。MySQL的一条连接不是免费的,客户端发起连接要经历TCP握手、协议协商、认证鉴权、权限校验、内存初始化一整套流程。在局域网里顺畅时也得几十毫秒,跨机房甚至能到两三百毫秒。如果应用每次执行SQL都现连现断,系统的吞吐天花板会低到让人怀疑人生。连接池的作用,就是提前建好一批连接放在池子里复用,谁要用谁取,用完归还,省掉反复建连的开销。
我习惯把它类比成银行柜台:池子里有固定数量的柜员,客户就是业务请求,柜员办完一位再接待下一位。连接池爆满,就是所有柜员都被占用,新客户既排不进去,又没人办完业务腾出柜台,后续请求只能干等或直接被请出去。表现到业务层就是大量"获取连接超时"、接口变慢、错误率飙升。
理解这层逻辑,排查方向就不会跑偏:连接池爆满从来不是单一变量的问题,而是连接的生产、占用、释放、回收、池容量配置这一整条链路上某一环卡住了。先搞清楚连接在谁手上、在干什么,比急着把池子调大重要得多。
1.2 爆满时的三个典型现场
结合我见过的几十次事故,连接池爆满虽然触发原因各不相同,现象却基本逃不出三种形态。
第一种是应用日志里出现获取连接超时。HikariCP会报:
Connection is not available, request timed out after 30000msDruid则会报类似:
wait millis 30000, active 200, maxActive 200这类报错说明池子里所有连接都被借走,且超时时间内没有归还。
第二种是数据库端Threads_connected飙升,逼近max_connections,新连接直接报"Too many connections"。这种情况往往伴随应用侧错误率上升,因为连接池想新建连接来扩容,但数据库本身已经拒绝接入。
第三种最有迷惑性:数据库CPU不高、磁盘不忙,连接池照样打满。查一下processlist会发现大量Sleep状态的空闲连接,明明没人用,却把连接占着不放。这是典型的连接泄漏或回收机制失效,硬件资源没被打满,瓶颈纯粹在"连接不释放"。
遇到现场,我不会只看单个指标,而是把应用连接池活跃数、数据库Threads_connected、接口平均耗时、错误率四张图叠在一起看:四者同时抬头的,多半是慢SQL;只有前两者抬头而CPU和耗时平平的,优先怀疑连接泄漏;只在某个时间点脉冲式冒顶的,考虑定时任务或突发流量。这一套看下来,范围能缩小一大半。
2. 排查手段:先分清楚是哪一种"满"
2.1 第一件事,看数据库这边的连接数
接到报警别急着重启,更别一上来就改配置。第一步先登数据库查连接数:
SHOW GLOBAL STATUS LIKE 'Threads_connected'; SHOW GLOBAL STATUS LIKE 'Threads_running'; SHOW VARIABLES LIKE 'max_connections';Threads_connected是当前所有客户端连接数,Threads_running是真正在执行语句的连接数,max_connections是上限。如果Threads_connected顶着上限,而Threads_running很小,基本能断定大量连接是空闲的,问题在"连接被长期持有不释放";如果Threads_running也不小,说明真有一堆语句在执行,慢SQL或大事务的嫌疑更大。
再看每个连接来自哪里:
SELECT host, db, user, command, time, state, info FROM information_schema.processlist ORDER BY time DESC;info字段太长时会被截断,重要场景不要依赖它看完整SQL,它的价值在于快速归类。把processlist按command和时间排序:Command是Query且time很大的,是正在跑的语句;Command是Sleep且time很大的,是空闲挂着的连接。这两类对应的处理思路完全不同,分错方向会白费功夫。
2.2 抓一张连接快照,按状态归类
只看当前瞬间还不够,连接池爆满是动态过程,最好连续抓几屏processlist,间隔几秒一次,观察状态有没有变化。
我习惯用这样一条查询,把连接按关键维度聚合:
SELECT command, COUNT(*) AS cnt, SUM(CASE WHEN time > 10 THEN 1 ELSE 0 END) AS long_cnt FROM information_schema.processlist GROUP BY command;结果非常直观。如果Sleep数量巨大且长期存在,优先查连接泄漏;如果Query十几个、每个time都几十秒,去抓慢SQL;如果Command是Query,State长时间停在"Sending data"或"Copying to tmp table",多半是大查询或排序在拖。
还要检查有没有事务没提交。连接池里的连接一旦开启事务不提交,SQL执行完也不会归还。查一下:
SELECT trx_id, trx_state, trx_started, trx_mysql_thread_id FROM information_schema.innodb_trx ORDER BY trx_started;碰到trx_started很早的事务,再回processlist用trx_mysql_thread_id反查它的会话。很多时候连接池爆满的幕后黑手,不是某一条SQL慢,而是一个开着事务遗留的会话,把池子里的连接一条条占死。
2.3 顺着慢SQL和锁等待把根挖出来
连接只是表象,真正要回答的是"连接被什么占用"。看库里现成的慢查询日志最直接:
SHOW VARIABLES LIKE 'slow_query_log'; SHOW VARIABLES LIKE 'long_query_time';如果没开慢日志,生产系统排查只能靠猜。我强烈建议线上MySQL都开,long_query_time设1秒,复杂场景甚至可以设0.5秒,再配合定时分析工具定期看。
还要看锁等待。锁等待导致的连接占用最容易被忽略:一个事务持锁不提交,其他事务的update语句全部卡在等锁上,连接被一个接一个消耗,数据库线程堆满,应用连接池跟着爆。可以快速查锁等待关系:
SELECT r.trx_id AS waiting_trx_id, r.trx_mysql_thread_id AS waiting_thread, b.trx_id AS blocking_trx_id, b.trx_mysql_thread_id AS blocking_thread, TIMESTAMPDIFF(SECOND, r.trx_started, NOW()) AS wait_seconds FROM information_schema.innodb_lock_waits w JOIN information_schema.innodb_trx r ON w.requesting_trx_id = r.trx_id JOIN information_schema.innodb_trx b ON w.blocking_trx_id = b.trx_id;找到blocking_thread后,回processlist看它在执行什么、事务开了多久。如果指向一个长时间不提交的业务事务,这已经不是连接池调参能解决的,要把事务边界当成代码Bug来修。
3. 根因拆解:五种最常见的爆满姿势
3.1 慢SQL把连接占住不放
慢SQL是连接池爆满的头号原因。一条连接单位时间内只能服务一个请求,SQL执行要10秒,这条连接就被独占10秒;池子总共50条连接,几个慢SQL就能吃掉大半个池子,剩下的正常请求只能抢少数连接,排队、超时、报错是必然结果。
我处理过一个案例,现象是每天下午连接池都会满一下,持续半小时恢复。后来从慢日志捞出来一条按时间区间查订单明细的SQL,日期列没走索引,直接全表扫,单次执行五六秒。业务侧还有个界面组件会自动循环刷新这个报表,等于把慢SQL以固定频率持续喂给数据库。补上索引后,查询从5秒降到50毫秒,连接池再也没满过。
这类问题的本质是SQL把连接占用时间拉得太长,不是连接池配错了。定位做法很成熟:开慢日志、用performance_schema的events_statements_summary_by_digest按平均耗时排序找TOP N,逐条explain。优化不了的复杂查询,考虑拆小批量、加缓存、走从库分担。
3.2 连接泄漏:借了不还
连接泄漏比慢SQL更隐蔽,它不占CPU、不跑慢SQL,就是静静地把每条连接挂在Sleep上占坑。最经典的是代码里获取连接后没有在finally归还,一旦业务抛异常,连接就永远丢在外面。ResultSet或Statement没关闭的情况也存在,很多连接池在归还连接时会尝试关闭,但某些框架和驱动组合下并不一定。
我见过一个实际的泄漏案例,泄漏点在一个老工具类里:先getConnection,执行查询,把ResultSet数据拷到集合,结果查询结果为空时提前return,把关闭连接忘掉了。平时流量低看不出问题,流量一上来,泄漏速度超过池子下限的回收速度,连接数就不断上涨。后来用Druid的removeAbandoned兜底,再把代码改成try-with-resources,才彻底解决。
判断是不是连接泄漏:看processlist里Sleep连接数量是否随时间单调上涨,同时应用侧线程栈里卡在"waiting on connection"的线程数量也在涨。修复手段就是统一用try-with-resources或finally关闭,并且别在事务里干与业务无关的重活。
3.3 突发流量与池容量失配
还有一种爆满,没有烂SQL,也没有泄漏,纯粹是流量超过了池子承载上限。典型场景是秒杀活动、定时任务高峰、外部系统重试风暴、爬虫抓取,瞬时把池子打空。特征非常明显:过程快、波形尖、持续时间短,水峰一过自己恢复。
这种场景要不要扩容,先想瓶颈在哪。池子大不代表系统吞吐高,如果数据库CPU已经在80%以上,加大池子只会让更多请求同时压向数据库,整体延迟反而更差。更合理的做法是给入口加限流保护后端,或者把高峰期的读请求分流到只读从库甚至缓存,把连接让给核心写链路。
如果确认数据库侧余量充足、池子确实配小了,再扩容。注意一个细节:不是改大最大连接数就完事,还要同步观察数据库max_connections够不够,应用实例数乘以单实例池大小是否超过数据库上限。多实例场景尤其要算总账。
3.4 空闲连接被回收,反而引发重建风暴
这是很多团队容易踩的暗坑:数据库wait_timeout把空闲连接踢掉,应用侧连接池不知道,继续使用已经失效的连接,业务请求拿到死连接,报"Communications link failure"或"Connection is closed",连接池为了补偿快速新建连接,大量建连请求同时打向数据库,秒级内连接数冲顶。
我在某个系统上见过这种古怪现象:系统每10分钟稳定爆一次连接池,数据库CPU不高,其他指标都正常。后来抓时间点,发现正好卡在数据库wait_timeout(某次配置被改成60秒)和连接池idleTimeout(默认10分钟)叠加的窗口里。应用侧连接在空闲第60秒被服务端踢掉,连接池却要等更久才做空闲回收,这个窗口内凡是拿到旧连接的请求全部失败,失败触发重试,重试触发建连,建连风暴就此爆发。
解决思路是让连接池里连接的存活时间必须小于数据库wait_timeout,同时开启保活检查和借用前校验。参数层面的配合后面详细说,这里先记住结论:连接池里任何一条连接的生命周期,都要比数据库服务端允许的空闲寿命短,否则池子里养的就是僵尸连接。
3.5 数据库端的隐形限制
很多团队排查连接池爆满,从头到尾只盯着应用侧,却忘了数据库端本身就卡着几道上限。max_connections是全局总闸,应用侧一个实例配100个连接、部署10个实例,就需要1000个连接,数据库max_connections只有800,连接池再努力也白搭。
还有max_user_connections,是按账号维度的限制,比全局上限更隐蔽。同一个业务账号在所有连接上共享这个数字,应用扩容、加实例、加连接池大小时,总量很容易悄悄撞到这道墙。我遇到过数据库连接数远没到全局上限,但某个专用账号的连接就是上不去,一查才发现是max_user_connections设了100,而那个账号背后挂了三个应用。
查这类问题,我通常直接跑一条SQL把关键阈值列出来:
SHOW VARIABLES LIKE 'max_connections'; SHOW VARIABLES LIKE 'max_user_connections'; SHOW VARIABLES LIKE 'wait_timeout'; SHOW VARIABLES LIKE 'interactive_timeout';还要注意操作系统层面:文件描述符限制、防火墙会话数限制、数据库所在机器可用内存,在极端情况下都会变成连接瓶颈。这些隐形限制排查单价不高,但容易被忽略,建议纳入巡检清单。
4. 连接池参数到底怎么调
4.1 池大小不是拍脑袋,是算出来的
到底配多少连接合适,是很多人最爱问的问题。先泼盆冷水:没有万能数字,但有现成的估算方法。最简单的经验公式是HikariCP官方文档提过的:
poolSize = (核心线程数 * 2) + 有效磁盘并发数这个公式对大部分单机应用够用,隐含条件是数据库不是瓶颈,机器核心数和磁盘队列决定了最高并发。4核机器按这个公式大概在8到12之间。很多人看到这数字会怀疑太小,实际上普通OLTP应用几十条连接已经足够,关键在于连接用得快、还得快,不是池子大。
更严谨一点可以用Little定律估算。假设接口平均QPS是500,每个请求平均持有数据库连接50毫秒,并发需要的连接数是:
连接数 = QPS * 平均占用时间 = 500 * 0.05 = 25再留1.5到2倍余量应对抖动,池子上限设40到50是合理的。这里用的是"持有连接的平均时间",不是接口响应时间,很多接口大部分时间在等下游、算逻辑,并没有占着数据库连接。池子太大并不会提升性能,反而会让更多并发语句同时打到数据库,加剧锁竞争和上下文切换。
4.2 超时和保活参数要联动起来看
连接池调优不只是调最大连接数,超时和保活类参数才是事故重灾区。以HikariCP为例,这几个参数必须配合着看:
maximumPoolSize=50 minimumIdle=10 connectionTimeout=5000 idleTimeout=600000 maxLifetime=1800000 validationTimeout=5000connectionTimeout是获取连接的等待时间,默认30秒太长,发生连接池打满时业务会卡死30秒才报错,加重故障。建议设3到5秒,让失败快速暴露。idleTimeout是空闲连接最大存活时间,默认10分钟,必须小于数据库wait_timeout。maxLifetime是连接最大生命周期,默认30分钟,也要小于数据库wait_timeout,留出足够余量,一般设为数据库wait_timeout的60%到80%。
保活机制方面,HikariCP默认不需要显式写testWhileIdle,它自带基于连接心跳的逻辑,但要注意maxLifetime和数据库wait_timeout的联动。Druid用户则要显式配置timeBetweenEvictionRunsMillis、testWhileIdle、testOnBorrow这些参数。我的建议是:testOnBorrow生产环境不用开,每次借用都校验损耗大;把testWhileIdle打开,配合合适的回收间隔,已经能挡住绝大多数僵尸连接。
4.3 多数据源与读写分离的隔离思路
系统里有多个数据源或做了读写分离,连接池不能一个配置走天下。不同用途的库,要独立配置连接池参数。
从库连接数可以比主库小一些,大部分读请求如果走了缓存和分库分表,真正落到从库的量是可控的。主库连接池要保守一点,尤其写操作多的场景,宁可小一些让请求排队,也别让太多写并发同时打到主库,否则锁竞争和磁盘IO压力成倍放大。
另一个容易被忽视的是多个消费者共用一个数据库账号的场景。某个库被三个服务共享,每个服务都按自己的峰值配了连接池,三个峰值叠加,数据库端连接数就可能爆。我是坚持一个服务一个独立账号的,至少在账号层面隔离;这样无论从监控、权限还是连接数管理,都能把问题边界划清楚。实在无法拆分时,也要在数据库端针对这个账号设合适的max_user_connections,防止一个应用把全库连接耗光。
5. 两个完整排查实录
5.1 案例A:大促期间HikariCP被打满
有一次某电商系统做大促压测,压到一半监控告警,连接池耗尽,大量请求报超时。我的第一反应是看四个图:连接池活跃数顶到200,数据库Threads_connected跳到接近600,数据库CPU升到70%,接口耗时曲线直线拉高。
再查processlist,大概有130多个会话的State是"Sending data",一多半在执行同一条SQL。把SQL拿出来explain,结果让人头皮发麻:一个带order by的深分页查询,limit从200000开始取10条,MySQL不得不先把前面20万行找出来排序再丢弃,扫描行数几千万,单条平均执行3秒以上。这条SQL来自后台运营导入工具,压测流量一起,直接把写库连接池打满。
处理分两步:第一步止血,先杀死耗时最长的会话,再给运营工具加接口级并发限制,把QPS压下来;第二步根治,给查询字段加联合索引,把深分页改成基于游标的分页方式,用上一页的最大id做条件往下翻,绕开offset。改完后同样的查询从几秒降到几十毫秒,连接池活跃数恢复正常,再也没在压测中爆过。
这个案例的教训很直接:连接池爆满只是症状,深分页和索引缺失才是病根。不解决SQL本身,把池子从200调到400,结果只是让数据库更快地被打死。
5.2 案例B:批处理后连接数居高不下
另一个印象深刻的案例来自一个报表服务:每天凌晨跑批处理,批处理结束后连接池不立刻恢复,保持高水位一个多小时,期间偶发连接获取超时。
接手时查processlist,发现大量Sleep连接,却看不到具体SQL。再查innodb_trx,抓到一个事务从凌晨开始就没提交过,trx_started时间很早。顺着事务线程号翻代码,定位到一个用JDBC手动开启事务的批处理方法:事务内循环处理几千条数据,每处理一条调用一次远程接口,远程接口偶尔超时,整个事务就僵在那里不回滚不提交,对应的连接和事务被无限期占用。
修复方案分三层:第一层,把批处理里远程调用挪到事务外,缩短单事务持有时间;第二层,给事务方法加超时控制,超过阈值强制回滚;第三层,数据分批提交,每500条做一次commit,而不是攒一个超长事务。改完后批处理一结束连接池就回落,凌晨告警再也没有出现。
这类问题的排查关键是不要只看连接数,要把事务信息、线程栈、代码逻辑串起来。连接池对外像黑盒,但里面的行为很多时候由业务代码的事务边界决定。
6. 排查工具与日常巡检建议
6.1 常用SQL与脚本
把排查过程中高频用到的SQL整理成一个小清单,遇到连接池告警直接照着跑,能省不少现场时间。
-- 1. 连接总览和上限 SHOW GLOBAL STATUS LIKE 'Threads_connected'; SHOW GLOBAL STATUS LIKE 'Threads_running'; SHOW VARIABLES LIKE 'max_connections'; SHOW VARIABLES LIKE 'max_user_connections'; SHOW VARIABLES LIKE 'wait_timeout'; -- 2. 当前会话快照 SELECT id, user, host, db, command, time, state, LEFT(info, 200) AS info FROM information_schema.processlist ORDER BY time DESC LIMIT 100; -- 3. 按命令类型聚合,区分Sleep和Query SELECT command, COUNT(*) AS cnt, SUM(time > 10) AS long_cnt FROM information_schema.processlist GROUP BY command; -- 4. 当前未提交事务 SELECT trx_id, trx_state, trx_started, trx_mysql_thread_id FROM information_schema.innodb_trx ORDER BY trx_started; -- 5. 锁等待关系 SELECT r.trx_id AS waiting_trx_id, r.trx_mysql_thread_id AS waiting_thread, b.trx_id AS blocking_trx_id, b.trx_mysql_thread_id AS blocking_thread, TIMESTAMPDIFF(SECOND, r.trx_started, NOW()) AS wait_seconds FROM information_schema.innodb_lock_waits w JOIN information_schema.innodb_trx r ON w.requesting_trx_id = r.trx_id JOIN information_schema.innodb_trx b ON w.blocking_trx_id = b.trx_id;我一般把这几条存成notes文件,告警时逐条跑。如果启用了performance_schema,用sys库的session视图查更舒服:
SELECT * FROM sys.session WHERE conn_id IS NOT NULL ORDER BY time DESC LIMIT 50;这个视图把连接、事务、语句信息揉在一起,省得在多个表之间跳来跳去。
6.2 连接池组件怎么选
连接池组件选型影响排查体验。Spring Boot默认HikariCP,优势是快、稳、参数简洁,对绝大多数场景足够。Druid在监控和SQL拦截上更全面,自带web监控页,能看到活跃连接、SQL执行次数、慢SQL统计,对不想搭额外监控体系的团队很友好。DBCP在现在高并发场景下用得少,新项目基本不建议。
我的建议是:除非确实需要Druid的SQL审计、防注入、监控面板这些内置能力,否则优先用HikariCP,它更接近连接池本职,少一层代理就少一分定位复杂度。不管用哪个组件,生产上都别忘把连接池活跃数、等待获取连接时间、创建连接耗时这几个指标打进监控系统,它们是诊断连接池问题的核心数据。
6.3 常规巡检建议
排查连接池问题最怕的是"配了一次就再也不管"。连接池需要持续巡检,我建议发布或大促前把几件事过一遍。
第一,跑一遍慢日志分析,把超过1秒的SQL整理出来,确认有没有全表扫描、有没有该建的索引。第二,看连接池配置和数据库wait_timeout的匹配关系,确保maxLifetime、idleTimeout都小于wait_timeout。第三,核对多实例、多数据源场景的连接数总和,不超数据库max_connections和账号级max_user_connections。第四,压测时主动观察连接池活跃曲线,出现长时间满水位,说明要么有慢SQL、要么池容量失配,不要用"反正能恢复"来糊弄。
我个人还有一个习惯:每次排查完连接池问题,一定把过程写成简短记录,包括现场现象、用过的SQL、最终根因和改动参数。这看起来花时间,但下次再遇到类似告警,查自己的记录比从头分析快太多。连接池问题大多有共同套路,经验是可以复用的。
最后说点实在的。我处理过的连接池爆满不下几十起,真正因为池子配小了导致问题的,比例非常低;绝大多数爆满背后,要么是慢SQL,要么是连接泄漏,要么是事务和空闲连接的生命周期没对上。每次有人说"把连接池改大点试试",我都会先劝他冷静:先查连接被谁占用,再决定动不动池子大小。调大连接池在短期内也许能掩盖症状,但它会把压力向下传导,数据库负载上去后,系统只会以更惨烈的方式再次倒下。
如果这篇文章只能让你记住一件事,我希望是这句话:连接池是一个信号灯,不是事故源头。学会看它的颜色,更要学会顺着它去定位真正堵车的那段路。希望这些思路和工具,能帮你下次避开一次慌张的告警处理。