
凌晨两点手机被连续告警震醒。某个内部系统的数据库连接池在十分钟内被拉满应用层开始连环报错用户页面卡成白屏部分接口直接 500。打开监控一看MySQL 的Threads_connected已经顶到几千连接池那边的空闲连接趋近于零所有新请求都在排队等连接。这类“数据库连接池爆满”的问题在业务高峰期太常见了但每次发生都让人手心冒汗。表面上是“连接不够用”实际上背后藏着完全不同的根因可能是连接泄漏可能是慢 SQL 拖住了连接也可能是事务边界太长把连接占死了甚至可能是连接池参数和数据库参数互相打架。这篇文章就把我多次排查连接池爆满的经验完整梳理一遍从表象到根因再到止血、治根、防复发一步步讲清楚。适合刚接触数据库和后端开发的朋友也适合每次遇到连接告警都不知道从哪下手的人。1. 连接池爆满的典型信号先从现象避开调参误区1.1 应用层和数据库层的异常表现连接池爆满时最先报警的通常是连接池客户端。用 HikariCP 的应用日志里会出现类似Connection is not available, request timed out after 30000ms用 DBCP 或 C3P0 的则是Cannot get a connection, pool exhausted或者是Timeout: Pool empty. Unable to fetch a connection in 30 seconds, none available这些错误都有一个共同点应用侧拿不到可用连接后续请求只能排队等待等到超时就直接抛异常。用户看到的现象就是页面转圈、接口超时、部分功能不可用。数据库侧的指标更直接。登录 MySQL 执行SHOW STATUS LIKE Threads_connected; SHOW STATUS LIKE Threads_running;正常情况下Threads_connected应该稳定在几十到几百的量级爆满时会直接逼近甚至顶到max_connections此时再新进连接就会收到“Too many connections”的报错。这就是为什么很多人在这个时间点会下意识去改max_connections。1.2 最大的坑一上来就调大连接池和 max_connections遇到连接爆满人的第一反应往往是“连接不够用那就加连接”。于是把 HikariCP 的maximumPoolSize从 50 调到 200再把 MySQL 的max_connections从 500 调到 2000重启应用。这种做法在低峰期可能管用但高峰期大概率会“死得更快”。原因很简单连接数提高每个连接都要占用 MySQL 服务端的内存和线程资源数据库的负担反而更重。如果根因是连接泄漏新的连接池上限只是让池子从“2小时爆满”变成“4小时爆满”问题还在时间问题而已。如果根因是慢 SQL那么连接从 50 加到 200慢 SQL 占用的连接也同比放大并且会在数据库端制造更多的锁竞争和上下文切换。所以遇到连接爆满千万不要急着动参数。先把“连接都消耗到哪里去了”查清楚再决定怎么处理。工具其实很简单MySQL 自带的一张进程列表就是最初的突破口。2. 用 show processlist 和线程堆栈给连接“画像”2.1 SHOW PROCESSLIST 字段怎么看Command 分布决定排查方向在数据库侧执行SHOW FULL PROCESSLIST;这是排查连接问题的第一板斧。它会把当前数据库上每一个连接列出来核心字段如下字段含义排查价值Id连接线程 ID后面 kill 连接时用User账号来源区分是业务账号还是运维账号Host客户端 IP 和端口找到连接来自哪个应用节点db当前连接的默认库确认是不是目标库Command连接当前的状态Query、Sleep 等是关键Time当前状态持续秒数数字越大越可疑State具体执行状态Sending data、Waiting for lock 等Info正在执行的 SQL慢 SQL 的现场证据这里最重要的一步是看 Command 列的分布。如果连接里有大量Sleep说明连接是被“占住但没干活”的状态带爆的——重点怀疑连接泄漏、事务未提交、连接未归还。如果大量是Query且Time很长说明是慢 SQL 或锁等待拖住了连接导致后续请求全部排队。简单说Sleep 多查代码漏连接Query 多查 SQL 和锁。这两条方向基本决定了后面所有排查动作。2.2 从应用线程堆栈追踪连接获取点如果怀疑连接泄漏光在数据库侧看到一堆 Sleep 连接还不够必须找出是应用代码里哪个位置拿了连接没还。这时需要把 Java 线程堆栈倒出来看jstack 应用PID thread.log然后在日志里找连接池等待和获取连接的栈。例如 HikariCP 下搜索grep -n HikariCP thread.log | head -100或者看是否存在at com.zaxxer.hikari.pool.HikariPool.getConnection如果大量业务线程同时卡在getConnection等待并且你看到业务代码的调用链停在一个具体 Service 方法上那么那个方法附近大概率就是连接发生泄漏的位置。再配合查看这段代码里是否手写了Connection获取和关闭基本就能锁定来源。2.3 用连接池监控与 leakDetection 坐实泄漏多数连接池组件都提供活跃连接数的指标。HikariCP 的监控里可以看到active、idle、pending的实时值。如果在流量平稳甚至下降时active仍然不断上升那就是泄漏的典型曲线。HikariCP 还提供了泄漏检测阈值参数leak-detection-threshold: 60000单位是毫秒。含义是一个连接从池中借出超过 60 秒未归还就记录一条泄漏告警日志。典型输出是Apparent connection leak detected需要注意这个参数不应设得过短否则一些本来耗时较长的合法请求也会误报一般设成 30 秒到 60 秒比较合理。它不能自动修复泄漏只能帮你把问题代码暴露出来在下一节我们会展开几种最常见的泄漏写法。3. 四个真实根因每种都有不同的“爆法”3.1 连接泄漏池被“僵尸连接”填满连接泄漏是连接池爆满的第一大户也是最容易在代码评审中漏掉的场景。最常见的写法是// 错误写法finally 里没有 close public ListUser queryUsers() throws Exception { Connection conn dataSource.getConnection(); Statement stmt conn.createStatement(); ResultSet rs stmt.executeQuery(SELECT * FROM user); while (rs.next()) { // ... } return list; // conn、stmt、rs 都没有关闭 }这里面只要stmt或rs没有关闭连接归还池子就会延迟如果异常发生在中途连接就直接“丢”了。等连接池里的连接被这种代码借走但不还池子很快就会被耗干。正确做法是用 Java 的 try-with-resources让连接、语句、结果集都自动关闭// 正确写法 public ListUser queryUsers() throws Exception { String sql SELECT * FROM user; try (Connection conn dataSource.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { while (rs.next()) { // ... } return list; } }排查技巧也很实用先搜全工程里所有getConnection()的调用点逐个确认是否有配套的 close然后重点检查 try-catch-finally 结构看 finally 里是否真的关闭了。很多老项目用的是手写的 JDBC 工具类这种地方最容易藏连接泄漏需要重点盯。3.2 慢 SQL 与长事务一个查询占住窗口不放连接池就像银行柜台每个窗口同一时间只能服务一个客户。一条 SQL 跑了 30 秒就相当于一个客户占着柜台 30 秒后面排队的人只能干等。慢 SQL 触发连接池爆满的典型表现就是Threads_connected没到顶但Threads_running很高且有大量连接卡在同一个 SQL 上。在SHOW FULL PROCESSLIST里你会看到 Info 列是同一个查询模板Time 字段都是几十秒甚至几分钟State 常见为Sending data、Creating sort index、Copying to tmp table等。这类问题的处理顺序是先拿这条 SQL 执行EXPLAIN看是否走了索引、扫描行数是不是太大。看是否因为深分页导致扫描了大量数据例如LIMIT 1000000, 20。看是否在查询里做了大范围的排序或临时表操作。一个很常见的例子是给大表做统计报表SQL 里写法是SELECT province, COUNT(*) FROM order_table WHERE create_time NOW() - INTERVAL 1 DAY GROUP BY province;如果create_time上没有索引这个查询会全表扫描上百万行跑个几十秒很正常。一个这样的查询就能让整个连接池的可用连接迅速下降。长事务和慢 SQL 经常成对出现。一个事务里包了多次查询用户中途不操作也不提交事务连接就一直挂在那里表现同样是连接池被占满。3.3 锁等待连接在排队中耗干锁等待是连接池爆满里最隐蔽的一种。前面两类是“连接被代码占用”或“被 SQL 跑满”锁等待则是“连接在等锁前端一直占着连接不释放”。比如库存扣减场景UPDATE inventory SET stock stock - 1 WHERE product_id 123;如果这条语句在某个事务里执行而另一个事务已经锁住了这行记录没提交后续所有更新同一行的连接都会卡在Waiting for lock状态。数据库侧看到的是大量连接都在等待连接池侧看到的是所有连接都处于活跃但“假死”状态。排查锁等待需要看三张表SELECT * FROM information_schema.innodb_trx; SELECT * FROM information_schema.innodb_lock_waits; SELECT * FROM information_schema.innodb_locks;重点看trx_state是RUNNING但trx_started时间很长的旧事务那个事务基本上就是持锁大户。处理方式通常是先把这个旧事务处理掉锁一释放后面的连接就会继续跑连接池爆满现象自然缓解。防止这类问题的核心是做短事务UPDATE 之前先确认 WHERE 条件用了索引不要在事务里做远程接口调用不要在一个事务里更新大量行。这些习惯比事后调参重要得多。3.4 连接池参数和 MySQL 参数互殴还有一类爆满是“配置打架”造成的。最典型的是连接池的maxLifetime大于 MySQL 的wait_timeout。MySQL 默认wait_timeout是 28800 秒也就是 8 小时。如果连接池让一个连接活过 8 小时后数据库端已经把它断掉了但连接池不知道还会继续拿这个连接去查询。表现就是应用运行一段时间后偶发性地报连接失败之后连接数突然暴涨那是因为连接池在疯狂重建连接一下子把连接数顶了上去。正确的参数协同思路如下参数推荐方向MySQL wait_timeout28800 秒默认或按实际情况调整连接池 maxLifetime应小于 wait_timeout比如 180 秒到 1400 秒之间connectionTimeout获取连接超时默认 30 秒可按响应要求收紧idleTimeout / minIdle空闲连接回收策略避免大量空闲连接占住 DB这类问题排查时有个特点不是稳定复现而是“隔一段时间爆一次”。如果连接池是 HikariCP它的默认maxLifetime是 1800000 毫秒30 分钟小于 MySQL 的 8 小时所以一般不容易踩到但用某些连接池组件时maxLifetime默认是 0代表永不超时这时就很容易和数据库的wait_timeout冲突。4. MySQL 侧的连接上限到底该怎么定4.1 max_connections 不是第一个要动的旋钮不少人一看到Too many connections就急着去改max_connections。实际上max_connections更像是一个“风险防火墙”而不是性能调优参数。合理的做法是正常情况下max_used_connections应该离max_connections有足够余量一旦距离被拉近就要警惕而不是把墙后移。要查看历史峰值占用SHOW STATUS LIKE Max_used_connections;如果这个值和当前Threads_connected都快贴到max_connections了那连接池爆满是必然结果因为数据库端根本没有余量给新连接。4.2 Threads_connected 和 Threads_running 要分开看这两个指标被很多人混用其实含义完全不同Threads_connected当前打开的连接数。它高说明“有很多连接挂着”。Threads_running当前正在执行查询的线程数。它高说明“数据库真的在忙”。如果Threads_connected高但Threads_running低多半是连接闲置或泄漏数据库本身不忙是连接管理出了问题。如果两者都高说明数据库处理能力到瓶颈了要么优化 SQL要么对读写做拆分和限流。如果Threads_running高但Threads_connected不高那反而是健康的——表明连接都被有效利用只是数据库确实在超负荷工作。这两者结合起来看才能判断“连接爆满”到底是资源问题还是代码问题。4.3 连接数与内存、线程调度之间的联动每个 MySQL 连接都有成本。连接建立后服务端要给它分配线程、内存尤其要考虑各类 buffer。例如sort_buffer_sizejoin_buffer_sizetmp_table_size如果这些参数设置得偏大比如sort_buffer_size是 2MB那么 2000 个连接在极端情况下可能同时占用数 GB 内存。这就是为什么把小内存机器上的max_connections盲目调大反而可能直接把数据库 OOM 拖垮。正确的做法是先估算机器内存再看全库连接数所需的 buffer 总量最后反向推导合理上限。例如 16GB 内存的实例InnoDB buffer pool 分走 8GB给每个连接的平均 buffer 按 2MB 估那连接数控制在 1000 以内都不算激进但如果机器只有 4GB连接数就得压到 300 以下。这个计算不需要很精确目的是让你心里有数连接数从来不是越大越好它是有资源约束的。连接池的作用是“复用”而不是“无脑堆量”。5. 止血、治根、验证一套能落地的完整流程5.1 先止血临时 kill 的取舍连接已经爆满的现场首要任务不是排查而是让业务先恢复。最直接的手段是杀掉一批可疑连接。杀连接之前必须确认两点连接是谁的、连接在干什么。前面用SHOW FULL PROCESSLIST看到的结果就是依据。可以优先杀掉Command Sleep且Time超过 60 秒的连接这类连接大概率是空闲的即使是事务未提交kill 之后事务也会回滚宁可损失极小范围的数据一致性也不能让整个服务持续不可用。执行格式KILL 12345;生产环境更稳妥的方法是写脚本按条件批量处理但要设置白名单排除管理账号连接。不推荐一上来就重启数据库或者重启应用。重启只能让当前连接清零如果根因是连接泄漏或慢 SQL重启之后很快又会满。它只适合在完全无法定位问题时的应急手段。5.2 治根代码修复、SQL 治理、事务边界止血只能维持几个小时的稳定真正要处理的是根因。连接泄漏把所有 JDBC 操作统一改成 try-with-resources可以做一个封装的 DAO 基类避免团队里各写各的。数据源获取统一走 Spring 的Autowired或标准池 API不要再写“自己 new Connection”的工具类。慢 SQL先开慢查询日志slow_query_log 1 long_query_time 1把超过 1 秒的 SQL 全部收集起来挑出高频和高耗时的逐条优化。常见手段包括加索引、改写查询条件、把深分页改成游标或基于上次最大 ID 的分页方式。事务边界事务尽量只包裹核心的写操作不要在事务内做远程 HTTP 调用、循环查询等慢操作。如果做批量更新控制在合理行数内并分批提交长事务是连接占用的隐形杀手。5.3 验证压测 监控告警 灰度发布修复之后不能直接宣布结束必须验证。先做一轮压力测试模拟高于平时的 QPS观察连接池曲线是否平稳、Threads_running是否处于合理范围、连接获取时间 P99 是否达标。我常用的做法是压测时把连接池的active、idle、pending三个指标都打出来。如果流量上升时active同步上升、流量平稳后active能降回去说明池子是健康的。如果active只升不降说明还会漏继续找代码问题。监控告警也要跟上。至少给这几个指标配告警Threads_connected与max_connections的比值超过 80%。连接池获取连接等待时间超过 1 秒。连接池pending长期大于 0。慢 SQL 数量突增。6. 用 QPS 反推连接池大小连接数规划的计算方法6.1 连接占用公式与计算示例连接池大小不该拍脑袋可以用一个粗算公式来估算连接池大小 ≈ 平均 QPS × 单请求平均数据库耗时 × (1 排队系数)举例说明。某个业务接口每秒请求量 500这个接口每次请求访问数据库的平均耗时是 20 毫秒。那么单请求占用的连接时间就是 0.02 秒。同一时刻需要的连接数大约是500 × 0.02 10再考虑一部分请求可能因为等待锁、结果集处理等原因多占一点时间加一个 30%50% 的排队系数10 × 1.5 15也就是说这个接口的预留连接池大小 1520 已经够用。如果实测连接一直在 20 以上而不下降说明耗时估算偏低或存在泄漏而不是配置不够。6.2 三种典型负载下的池大小设计不同业务形态对连接池的需求差异很大业务形态特征池大小建议低频管理类QPS 低、单次查询简单1020高频 OLTP 类QPS 高、单次查询几毫秒到几十毫秒核心接口单独估算池总大小 50100报表/批量类单次查询秒级、并发不高池不用太大但 SQL 必须优化否则池再大也堵这里面最关键的理念是连接池解决的是“复用连接”的问题不是“让更多请求同时跑”的问题。单个 MySQL 连接的查询速度取决于索引和执行计划而不是池的大小。池子设得再大SQL 本身要跑 10 秒照样拖垮。6.3 需要长期盯的监控指标最后建议把这几个指标做成日常巡检项连接池活跃连接数看涨跌趋势是否与流量一致。连接获取等待时长 P99超过 500ms 就要警惕。连接创建/销毁速率如果频繁创建销毁说明maxLifetime或idleTimeout配置不合适。MySQLThreads_connected与Threads_running的比值。这些指标可以在看板上做成两张图一张展示连接池状态一张展示数据库状态。连接池爆满不是突发事故大多是缓慢恶化的过程。有了这些图基本都能在用户感知之前提前处理。最后分享一个印象深刻的排查经历。某次连接池持续告警我把应用、数据库、参数都翻了个遍仍然时不时涨满。最后发现是一个第三方 SDK 内置了自己的连接池每次调用都新建一个池实例而不复用流量稍高就把数据库连接数顶上天。那次之后我学会了排查连接问题时要把中间件、SDK、框架的隐藏连接全部考虑进去不能只盯业务代码。希望这篇排查顺序能帮你少熬几个夜少掉几根头发。