一提到“MySQL连接池爆满”,经历过的人都知道那种感觉:先是应用日志里飘出一两条获取连接超时的报错,然后是监控面板上的活跃连接数直线飙升,紧接着整个服务像被掐住喉咙一样,接口越打越慢,最后一大片请求全部卡死在等待连接上。这种故障不致命,但它能把你从凌晨的睡梦中直接拽起来。
这篇文章就围绕MySQL数据库连接池爆满这个问题,完整梳理我在实际运维和性能排查中总结的思路:从现象确认、连接池工作原理,到逐层定位根因、手工处置和参数调优,完整走一遍。适合正在维护Java Web应用、微服务系统,或者自己折腾过MySQL但没遇到过这类问题的开发者参考。这不是教科书式的内容整理,里面大部分结论都来自线上真实事故的复盘。
1. 连接池爆满的典型现场:现象识别与监控确认
1.1 第一眼看到的异常特征
连接池爆满的故障现场,通常有几个高度一致的信号。首先是应用日志中高频出现“Connection is not available, request timed out after 30000ms”或“DataSource.GetConnection()”超时异常,这类报错直接指向获取数据库连接失败。其次是连接池监控数据中的ActiveCount(活跃连接数)持续等于MaxActive(池最大连接数),并且长期不回落。
第三个信号出现在数据库侧。登录到MySQL实例执行show status like 'Threads_connected';,会发现当前连接数已经逼近甚至触顶max_connections。这里要区分两个概念:Threads_connected是当前MySQL服务器实际维持的连接线程数,而连接池中配置的连接数是应用层面的逻辑连接。池中的每一个物理连接都会在MySQL侧对应一个线程,所以当两部分同时达到上限时,故障就已经非常严重了。
我见过很多团队在排查时只看应用日志里的超时异常,然后直接去调大连接池的maxActive参数,结果根本没有解决问题,反而把MySQL压垮。正确的第一步不是调参数,而是把现象钉死,判断爆满到底发生在应用连接池侧,还是MySQL服务器侧,或者两侧同时爆满。
1.2 用哪些监控指标确认故障边界
要快速确定故障范围,建议在故障发生时收集几类指标并打上时间戳。应用侧重点关注:连接池ActiveCount、IdleCount、PendingCount(等待获取连接的线程数)、GetConnection耗时分布;数据库侧重点关注:Threads_connected、Threads_running、Max_used_connections、Connections(累计连接次数),以及show processlist里的State和Command列。
有一项非常关键的指标是Threads_running,它表示当前正在执行SQL的线程数。如果这个数值是连接数的十分之一甚至更低,说明大部分连接都处于Sleep状态,也就是连接建立后一直没有干实事——这是连接泄漏或连接长期占用的典型特征。如果Threads_running和Threads_connected接近,说明确实是高并发请求把连接全部占满,同时SQL执行缓慢。
把这几组数据收集完整,你才具备往下排查的基础。很多人连接池爆满后急急忙忙重启应用,等恢复后又当作什么都没发生,这等于把最有价值的证据扔掉了。
1.3 一份好用的故障信息收集清单
结合之前处理过的多个案例,我列了一个故障发生时优先收集的信息清单,直接照着做可以节省大量时间。
- 出问题的时间点(精确到分钟)和持续时长
- 应用当时的总请求量、耗时分布(特别是P99耗时)
- 连接池实时状态(Active/MaxActive/Pending/Idle)
- MySQL侧连接数与线程数快照
show processlist输出结果(尤其是非Sleep状态的长事务)- 最近是否有上线变更、定时任务、爬虫或批量任务触发
这些信息不需要事后从一堆曲线里翻找,如果在故障发生前就提前设置好采集脚本或监控页面集成,那么复盘时可以快速定位到具体时间区间,定位效率会高很多。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 连接池参数与连接生命周期:先搞清楚连接是怎么被占满的
2.1 连接池的工作原理与内部流转
连接池本质上是一个物理连接的管理容器。应用启动时,连接池会按配置预先创建若干连接放入池中,业务线程需要操作数据库时,从池中借出一个连接(borrow),用完后归还(return),而不是每次新建和销毁TCP连接。这样一个简单的共享复用机制,避免了频繁创建连接带来的TCP握手、MySQL认证、线程创建等开销。
但连接池不是无限供给的。池中连接数的上限由maximumPoolSize或maxActive决定,取决于你使用的是HikariCP、Druid、C3P0还是DBCP。当池中的连接全部被借出且未归还时,新的请求线程只能进入等待队列,等待时间超过connectionTimeout/maxWait就会抛异常。这就是连接池爆满时应用层报错的直接来源。
连接池还负责连接的保活和回收。空闲时间超过阈值(如Druid的minEvictableIdleTimeMillis)的连接会被清理,然后按minIdle配置补充新连接。这个过程如果配置不当,会出现一种隐蔽的“池内连接全部失效”的情况:数据库侧因为超时关闭了空闲连接,而应用侧连接池没有及时感知,业务请求返回一个已经断开的连接,导致大量报错。
2.2 常见连接池的关键参数含义
不同连接池的参数名不完全一样,但核心概念是相通的。我用HikariCP和Druid举例,两者是目前Java生态中最常用的连接池。
HikariCP核心参数:
maximumPoolSize:池中最大连接数,默认10minimumIdle:池中维护的最小空闲连接数,默认等于maximumPoolSizeconnectionTimeout:客户端等待池中连接的最大毫秒数,默认30000idleTimeout:空闲连接被回收前的最长空闲时间,默认600000毫秒maxLifetime:连接在池中的最大存活时间,默认1800000毫秒validationTimeout:连接有效性检测的超时时间
Druid核心参数:
maxActive:最大连接数,默认8minIdle:最小空闲连接数,默认0maxWait:获取连接时的最大等待毫秒数,默认-1表示无限等待connectionErrorRetryAttempts:连接错误后的重试次数timeBetweenEvictionRunsMillis:检测空闲连接的间隔时间testWhileIdle:获取连接时是否检测连接有效性
很多线上故障的根源出在maxWait配置成-1这样无限等待的极端场景,或者maximumPoolSize设置过大而数据库侧max_connections没跟上,形成应用侧和数据库侧容量的错配。
2.3 连接池“爆满”的本质是供需失衡
理解连接池爆满,可以从供需关系切入。池中可用连接是“供给”,业务请求是“需求”。正常情况下供给大于需求,连接池处于健康状态。爆满时,要么是供给被削减,比如数据库侧主动断开了一批连接而应用不自知;要么是需求暴增,比如瞬时流量是平时的几倍;要么是单个连接被占用的时间无限拉长,比如一次慢查询执行了30秒,连接在这30秒里一直被占用不释放。
这三种情况对应的处理思路完全不同。连接被数据库侧断开导致的爆满,要检查空闲回收策略和网络中间设备;瞬时流量导致的爆满,要考虑限流、扩容和参数调整;慢查询导致的爆满,优化SQL和索引才是根本解法。所以排查的第一步是对号入座,避免用一套方案应对所有情况。
3. 逐层排查的完整链路:从应用日志到数据库侧反向定位
3.1 从应用日志中提取关键时间线
当连接池爆满发生后,不要一开始就去翻MySQL的配置文件和连接池参数,而是先梳理应用日志中异常出现的时间线。这一步的目的是找到“先有鸡还是先有蛋”:是应用接口先变慢导致连接被长时间占用,还是连接的获取本身就先开始超时。
我会看两个维度的日志。第一个是应用框架层面的连接获取日志,比如Druid的GetConnectionTimeoutException或HikariCP的PoolInitializationException;第二个是业务接口的耗时日志,尤其是服务有没有在故障发生前就出现GC抖动、外部接口调用超时、批量任务启动之类的征兆。
有一次我们排查一个连接池爆满问题,发现异常日志出现前15分钟,正好有一个定时任务开始跑,这个任务会用多线程批量处理大量数据,而每个线程内部拿到的连接没有在finally块中释放。这就是一个非常典型的“定时任务+连接泄漏”组合,如果只盯着主业务接口看,很难定位到问题源头。
3.2 用show processlist看数据库侧的真实状态
show processlist是排查数据库问题的利器。这个命令会把当前MySQL实例所有连接线程的状态列出来,包括Id(线程ID)、User(连接用户)、Host(客户端地址)、db(使用的数据库)、Command(当前命令类型)、Time(执行时间)、State(线程状态)、Info(正在执行的SQL)。
连接池爆满时,我通常会加上\G参数以竖向方式输出,方便查看字段详情。重点关注以下几类情况。
- 大量
Command=Sleep且Time很大的连接:说明连接被借出后长时间没有执行SQL,这是连接泄漏的常见信号 - 大量
Command=Query且Time很大的连接:说明SQL执行本身很慢,查询长时间未返回 - 同一个Host/IP出现几百个连接:说明某个应用节点的连接池占了绝大多数连接数
State=Waiting for table metadata lock或State=Waiting for lock:说明存在锁等待或元数据锁阻塞
执行show processlist的同时,还可以查information_schema.processlist表,用SQL做些聚合统计,比如按User和Host分组统计连接数、按Command分组统计连接数。我经常用的几条诊断SQL如下。
sql复制-- 统计各用户连接数
SELECT user, host, count(*) FROM information_schema.processlist GROUP BY user, host ORDER BY count(*) DESC;
-- 统计各状态连接数
SELECT command, state, count(*) FROM information_schema.processlist GROUP BY command, state ORDER BY count(*) DESC;
-- 查看耗时最长的连接
SELECT id, user, host, db, command, time, state, LEFT(info, 100) FROM information_schema.processlist WHERE command != 'Sleep' ORDER BY time DESC LIMIT 20;
3.3 从连接池监控反向追踪请求来源
如果应用接入的监控框架有连接池的埋点数据(比如Spring Boot Actuator暴露的HikariCP指标,或Druid的监控页面),定位会更快。Druid的监控页面上可以直接看到每个连接当前的持有线程、最近执行的SQL、活跃连接占比等,能非常直观地判断连接是被哪一个业务逻辑占用的。
但并不是所有团队都配置了这类监控,这时可以从MySQL侧反推。比如show processlist里看到了某个Host对应的大量Sleep连接,就可以登录到那个应用节点,用jstack命令查看JVM线程栈,找出哪些业务线程正在等待或持有数据库连接。
我记得有一个线上案例,我们用jstack抓取线程栈后发现,大量线程卡在某个外部HTTP接口调用的等待响应上,而调用前已经从连接池里借出了连接,外部接口超时时间设的是60秒,导致连接被白白占住一分钟。这属于上游服务响应慢引发的连接池耗尽,单纯看数据库侧是永远找不到原因的,因为数据库侧这些连接都是Sleep状态,没有执行任何SQL。
3.4 借用MySQL性能库精准定位活动会话
除了show processlist,performance_schema库里面也有连接维度的信息。比如events_statements_current表记录每个线程当前正在执行的语句,events_waits_current表记录当前等待事件,可以结合起来分析连接到底卡在哪个环节。
sql复制-- 查看当前非Sleep线程最近执行的SQL
SELECT t.THREAD_ID, t.PROCESSLIST_USER, t.PROCESSLIST_HOST, t.PROCESSLIST_DB,
t.PROCESSLIST_COMMAND, t.PROCESSLIST_TIME, e.SQL_TEXT
FROM performance_schema.threads t
LEFT JOIN performance_schema.events_statements_current e
ON t.THREAD_ID = e.THREAD_ID
WHERE t.PROCESSLIST_COMMAND = 'Query'
ORDER BY t.PROCESSLIST_TIME DESC;
使用performance_schema的前提是已经开启相关采集项,默认在生产MySQL 5.7+中通常是开启的。如果还没开启,show processlist配合定时采样也基本够用。
整体来说,排查连接池爆满的链路是:应用日志建时间线 -> 连接池监控定位持有者 -> MySQL侧processlist看状况 -> 必要时jstack和性能库下钻。四个层级层层递进,能从宏观到微观完整还原事故现场。
4. 四个高发根因与对应处置:泄漏、并发、慢查询与回收失效
4.1 根因一:连接泄漏,代码拿连接不归还
连接泄漏是连接池爆满最常见的根因,几乎每个跑Java Web应用超过一年的人都会遇到。它的特征非常鲜明:数据库侧有大量Sleep状态的连接,且Time字段持续增长,连接池ActiveCount常年偏高,服务的TPS越差但连接数不降。
代码层面的原因通常是以下几种。
- 获取连接后没有在finally块中释放
- 使用了
commons-dbutils等工具类时,查询结果是null导致提前return,连接未归还 - 事务切面只对RuntimeException回滚,对受检异常不生效,导致连接在事务提交和回滚之间徘徊
- 多线程并发处理时,子线程中获取的连接,由主线程归还,造成线程与连接绑定错乱
一个很实际的排查思路是,在Druid连接池开启removeAbandoned=true并配置removeAbandonedTimeoutMillis,让连接池自动回收超过设定时间未归还的连接。这个机制在Druid中默认是关闭的,开启后可以在分析问题时强制清理泄漏连接,但要注意不能长期开启,因为它会把一些合法的长事务连接也一并回收掉,造成数据一致性问题。
更彻底的修复是代码审查和连接使用规范的强制化。最基础的一条铁律:连接用完后在finally块中关闭,或者用try-with-resources语法确保自动关闭。配合连接池监控里的“活跃连接栈”功能(Druid支持打印持有连接时的线程栈),可以抓到泄漏点。
java复制try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(sql);
ResultSet rs = ps.executeQuery()) {
// 业务处理
} catch (SQLException e) {
// 异常处理
}
4.2 根因二:并发峰值超过池上限和数据库上限
瞬时流量打满连接池的情况也很多见,尤其是促销活动、定时任务批量处理、突发爬虫流量。这类问题的特征是连接池ActiveCount瞬间冲顶,show processlist中大量连接处于真实执行SQL或立刻变为Sleep的状态,但连接数持续不释放。
这个时候,如果只是把连接池的maxActive从50调到200,很可能导致MySQL实例自身max_connections被击穿,造成更严重的“too many connections”错误。正确的做法是评估数据库实例的承载能力,确认单连接能支撑的QPS,再按比例确定连接池上限。
可以做一个简单的容量判断:如果MySQL实例的CPU没有打满,慢查询也不多,那么适当调大连接池是可行的;如果数据库CPU已经接近100%,调大连接池只会让SQL排队更严重,因为可用CPU资源不足以支撑更多并发SQL。
同时要从流量入口侧入手:对接口做限流,对批量任务做分片,对爬虫来源做封禁。连接池的参数是防御能力,而不是业务容量本身。正常情况下,连接池的maximumPoolSize并不是越大越好,过大的连接池会导致MySQL侧上下文切换开销增加,性能反而下降。
4.3 根因三:慢SQL堆积,连接被长时间占用
慢SQL是连接池爆满里最隐蔽的一个根因,因为它的表象是池满,本质是SQL性能问题。一个执行5秒的查询,如果并发量只有20,它就能占住20个活跃连接;如果池上限是50,它很快就会把池子占满。更麻烦的是,慢SQL通常是间歇性出现,连接池会在一段时间内被占满,SQL一旦结束又立刻恢复,这种“时好时坏”的特征让人很难和连接池配置联系起来。
排查慢SQL依赖两类数据:慢查询日志和processlist中的耗时SQL。慢查询日志可以设置阈值,低于阈值的SQL会记录到日志中,DBA可以据此分析执行计划。show processlist中的Time字段可以辅助判断当前正在执行的SQL已经耗时多久,对快速止血很有用。
解决慢SQL占满连接池,核心是定位具体的慢SQL,然后通过explain分析执行计划,检查是否缺少索引、是否发生全表扫描、是否使用了不可下推的函数导致索引失效。SQL层面的优化解决了,连接池的压力自然缓解。
临时方案是给慢SQL设置超时时间和最大执行时间,比如使用max_execution_time提示符限制单个SELECT语句的执行时间:
sql复制SELECT /*+ MAX_EXECUTION_TIME(1000) */ * FROM orders WHERE status = 'PENDING';
这种方式可以防止某一条极端慢SQL无限占用连接,适合用在特殊场景下的快速止血,但不适合替代真正的SQL优化。
4.4 根因四:空闲连接被数据库或网络设备回收
这种根因非常容易误导人,因为从应用日志上看,连接池看起来是满的,但实际池里的很多连接已经“死”了。具体场景是:MySQL侧的wait_timeout和interactive_timeout到了默认的8小时,把空闲连接主动断开;或者防火墙、负载均衡设备对TCP空闲连接设置了回收策略,在一定时间后静默断开。
而应用侧的连接池没有及时感知到这些连接已经失效,仍然把这些连接放在池中。等到请求来了从池里取出一个死连接,业务执行时发现连接不可用,抛异常后再由连接池框架重新创建新连接。如果业务线程在等待连接时恰好池内死连接未被完全清空,就会表现为连接获取超时。
这个问题的防护方案,是在连接池层面配置连接有效性检测。比如Druid的testWhileIdle配合timeBetweenEvictionRunsMillis,在每次获取连接时验证连接有效性;HikariCP则是通过maxLifetime强制连接不超过数据库的wait_timeout,主动在连接失效前更新换代。
一个典型的推荐配置:MySQL的wait_timeout保持默认值或更短(如300秒),连接池的maxLifetime设置为小于wait_timeout的值(如280秒),这样连接池会主动淘汰已经空闲过久的连接,而不是等数据库侧来单方面断开。同时idleTimeout也要配合设置,避免池中出现大量闲置连接。
5. 连接池与数据库两侧的参数调优清单
5.1 应用侧连接池参数设置建议
连接池参数没有一套放之四海而皆准的数值,但可以基于几个原则来设置。
maximumPoolSize(HikariCP)或maxActive(Druid)不是越大越好。经验公式可以参考:CPU核数x2加上磁盘IO等待因子,但更务实的做法是根据高峰期QPS、单查询耗时来估算。假设单连接每秒能处理约20个简单查询,如果高峰期需要2000 QPS,那大约需要100个连接。但数据库CPU、磁盘IO、网络带宽都会限制实际的单连接处理能力,所以建议从一个小值(如50)开始,结合压测逐步调大。connectionTimeout/maxWait不要设置无限大,否则请求线程会无限阻塞,最终拖垮整个应用。一般建议3~5秒,业务高峰期可以放宽到10秒。minIdle不要设置得过高,空闲连接过多会浪费数据库资源。建议与maximumPoolSize分开,让连接池在低峰期自动缩容。- 启用连接有效性检测,但检测本身也有额外开销,不要把
timeBetweenEvictionRunsMillis设置得太短。
下面是一份基于Spring Boot + HikariCP的常见配置示例。
yaml复制spring:
datasource:
hikari:
pool-name: MainDBHikariPool
minimum-idle: 10
maximum-pool-size: 50
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
connection-test-query: SELECT 1
validation-timeout: 3000
leak-detection-threshold: 60000
这里要特别说下leak-detection-threshold。HikariCP的这个参数会在连接被借出超过阈值仍未归还时,输出一条包含线程栈的警告日志。默认值0表示关闭,生产环境建议设为20~60秒,一旦发生连接泄漏,日志里就会打出堆栈信息,直接指出是哪个方法拿的连接没还。这是我在实际排查中定位泄漏问题最省力的一个工具。
5.2 MySQL侧连接相关参数调整
数据库侧的参数主要体现在max_connections和超时时间上。
max_connections的默认值在MySQL 5.7中是151,MySQL 8.0中也是151。生产环境一般建议根据实例规格和业务规模调整到500~2000之间,但并不建议盲目调高,因为每个连接背后都有内存和线程资源消耗。可以通过Max_used_connections和max_connections的比值来判断当前是否处于接近上限的状态。
sql复制-- 查看最大使用连接数
SHOW GLOBAL STATUS LIKE 'Max_used_connections';
-- 修改最大连接数
SET GLOBAL max_connections = 500;
wait_timeout和interactive_timeout控制非交互和交互连接的空闲超时时间。默认8小时太长了,会让数据库侧堆积大量无效空闲连接。建议调整为300秒左右,同时让连接池的maxLifetime小于这个值,形成连接池主动换连接的良性周期。
5.3 调整参数时的注意事项
调连接池参数和数据库参数时,最大的坑在于没有全局联调。比如你把连接池的maximumPoolSize调到了300,但MySQL侧max_connections只留了200,结果连接数一旦冲高,MySQL先死,应用跟着报too many connections。反过来,如果你把MySQL的wait_timeout调短了,但连接池的maxLifetime没有相应调短,那么连接很可能在池中存活超过数据库侧的回收时间,变成死连接。
调整顺序建议是:先确认数据库实例的规格和负载情况 -> 确定max_connections -> 再根据应用并发量设计连接池参数 -> 最后调整超时类参数并开启有效性检测和泄漏检测。改完以后,一定要做压测或高峰期的观察,确认连接池状态曲线平稳,ActiveCount不会频繁触顶。
6. 监控告警与快速预案:爆满之前和爆满之后该做什么
6.1 连接池关键监控指标与告警阈值
在连接池爆满问题中,监控做的有没有用,直接决定了故障影响范围。如果能在连接数刚到峰值的80%时就触发告警,就有时间在业务受损前干预;如果等连接数已经触顶才开始告警,往往只能被动重启。
建议监控以下指标并设置合理阈值。
- 连接池ActiveCount / maximumPoolSize:比值超过0.8持续3分钟,触发警告;超过0.95触发严重告警
- 连接池PendingCount:大于0说明请求已经在排队,持续超过10秒就应该告警
- 连接获取等待时间:通过采样或埋点统计P99耗时,如果连接等待耗时超过1秒,说明池已接近饱和
- 数据库Threads_connected / max_connections:比值超过0.8时告警
- Threads_running:超过CPU核数的10倍时告警,说明数据库正在承受过量并发执行
监控工具方面,Prometheus + Grafana可以采集Java应用的JVM指标(包括HikariCP和Druid的连接池指标),MySQL侧可以使用mysqld_exporter暴露连接数、线程数等状态。也可以直接利用Spring Boot Actuator暴露的health和metrics接口做基础监控。
6.2 连接池爆满时的快速止血动作
一旦确认连接池已经爆满,有几个止损动作可以按顺序执行,但要注意操作风险。
- 不要急着重启数据库。重启MySQL会把所有连接全部断开,看起来是释放了,但应用侧会自动重连,瞬间建立大量新连接,可能把数据库直接压垮。
- 先通过processlist或连接池监控判断连接是被谁占用。如果是某个特定IP或应用节点占满,可以把该节点的流量切走,或者临时将该节点的连接池上限调低,让其他节点承担流量。
- 如果确认存在慢SQL占满连接,找到该SQL并在数据库侧用
KILL QUERY或KILL <thread_id>终止它。需要特别小心:KILL QUERY只终止当前正在执行的查询,连接还在;KILL <thread_id>会断开整个连接。如果连接的Command是Query,可以先试KILL QUERY。 - 应用侧应急时可以临时调大
maxActive/maximumPoolSize,但这只是权宜之计,前提是数据库侧有足够余量。如果数据库侧的Threads_connected已经很高,这个动作可能加速数据库崩溃。 - 最稳妥的止血方式之一,是把应用节点的连接池做一次“热重建”,也就是重启单个应用节点,而不是重启整个集群。这样影响面最小,也能让连接池中的死连接被清空。
6.3 故障后的复盘与固化措施
故障解除不代表问题结束。我每次处理完连接池爆满,都会要求团队输出一份包含时间线、根因分析和变更记录的复盘文档,并把对应的监控项和告警阈值固化到监控系统中。
常见的需要固化的措施包括:连接池泄漏检测必须开启;连接池和MySQL侧超时参数必须对齐;慢查询日志阈值不能设置得太松(建议设置为1秒);上线前必须做流量峰值的压测;定时任务类操作要评估数据库连接占用情况,必要时单独配置一个较小的连接池。
7. 实战案例复盘:一次凌晨的数据库连接池耗尽
7.1 故障现象还原
那次事故发生在凌晨两点,一个电商系统的订单模块开始出现大范围请求超时。监控告警先弹的是数据库连接池活跃连接数超过90%,随后订单查询接口的失败率迅速飙升。登录跳板机后执行show processlist,发现大部分连接都处于Sleep状态,但其中夹杂着约20个Time超过300秒的Query连接,执行的全是同一条统计报表SQL。
应用日志显示,HikariCP连接池在故障发生前大约15分钟开始出现“Connection is not available, request timed out after 30000ms”的异常。数据库侧的Threads_connected在那一刻已经达到了1600,而实例的max_connections只有2000。
7.2 排查定位与紧急处置
通过show processlist的输出,我们很快定位到那条统计报表SQL来自一个每天凌晨定时执行的批量报表任务,但正常运行时它应该很快执行完。为什么这次执行了300多秒还没结束?
进一步查看执行计划发现,该SQL涉及的业务表在前一天白天刚加了一个索引,但因为数据量增长,表统计信息过期,优化器选择了一个完全错误的执行计划,导致全表扫描加嵌套循环,性能骤降。连接池中的连接被这批慢SQL消耗掉一大半,剩下可用的连接很快被业务高峰下的正常请求占满,形成连接池耗尽。
紧急处置分三步。第一步,用KILL QUERY终止那批超时的慢SQL线程,让连接池中的连接尽快释放;第二步,把定时报表任务临时停掉,避免再次触发;第三步,对相关表执行ANALYZE TABLE刷新统计信息,然后验证SQL是否恢复正常执行计划。
7.3 根因修复与后续改进
排查的根本原因其实有两个层面。表层是那批慢SQL占用连接时间过长导致连接池被耗干,深层是定时任务使用的连接池与主业务连接池共用一个,一个异常任务就能拖垮整个系统的数据库连接资源。
后续的改进做了三件事。第一,把定时报表任务拆到独立的数据源,使用单独的连接池,避免影响在线业务;第二,对涉及大表的统计类SQL设置max_execution_time,超时自动终止;第三,给连接池加上leakDetection和慢SQL监控,确保下次类似问题能在前5分钟内被发现,而不是等到业务大面积报错。
这次复盘之后,团队对所有需要跑大查询的定时任务都做了一次审查,凡是可能全表扫描或执行时间超过10秒的任务,全部迁移到只读从库上执行。这一改动看起来简单,实际上把连接池爆满这类风险从架构层面降低了很多。
8. 个人体会:连接池爆满问题的高效排查心法
踩过几次连接池的坑之后,我的一个深刻体会是:连接池爆满很少是连接池本身配置的问题,绝大多数是外部因素在某个瞬间把连接池打穿了。配置只是静态的防御,真正决定系统稳定性的,是你能不能快速判断出到底是哪一类流量、哪一类SQL、哪一段代码在消耗连接资源。
排查速度影响最大的,其实是监控数据的完备程度。如果之前没有接连接池监控,故障发生后再去查日志、看show processlist,等于在事故迷雾里找方向。提前把HikariCP或Druid的指标接入Prometheus,把leakDetection打开,把慢查询日志阈值设置为1秒,这三件事加起来不超过2小时,但在关键时刻能省下半夜排查的3个小时。
另外,我不建议在平时就把max_connections和连接池参数调得特别大。连接数越大,数据库的上下文切换和锁竞争越明显,单条SQL的耗时也会随之上升。峰值时期能够恰好容纳业务并发量的连接数,就是最好的配置。
如果手头还没有一套成熟的连接池监控体系,这篇内容可以直接当检查清单用:先确认报错和监控指标,再看processlist,然后对号入座判断根因,最后调整参数并固化监控告警。把这条路径走通了,下次连接池再报警,你就能比多数人更早下结论。
