资讯动态

MySQL连接池爆满排查指南:从报错到根因,一次讲透

发布时间:2026/10/17 18:29:21 来源:尧图企业网站定制
先说结论MySQL 数据库连接池爆满十次里有八次不是数据库本身挂了而是连接借出去之后迟迟不归还或者归还的速度赶不上请求涌入的速度。前阵子帮某业务团队排查一个线上故障日志里刷得最多的就是这句话HikariPool-1 - Connection is not available, request timed out after 30001ms。从看到这条日志到定位到根因前后花了大概一个半小时事后复盘时发现如果一开始就用对排查顺序这个时间能压缩到二十分钟以内。这篇文章把整个排查过程、根因分析和落地修复方案完整写出来适合正在维护线上服务、被连接池问题困扰过的后端开发者参考也适合刚入门的同学理解连接池的运作逻辑。文中的案例都是处理过的真实问题只是隐去了业务名和细节方法和结论完全可以照抄。1. 连接池爆满的事故现场从这条报错说起1.1 典型报错长什么样不同连接池的报错解读先解读那行经典的报错Connection is not available, request timed out after 30001ms。HikariPool-1 是 HikariCP 连接池在 Spring Boot 默认配置下的池名Connection is not available的意思是连接池里当前已经没有可用连接request timed out after 30001ms表示这个请求在池子里等了 30 秒还没等到连接超过了connectionTimeout配置。如果用 Druid 连接池报错措辞又是另一种风格常见的是wait millis 10000, active 50, maxActive 50翻译过来就是池子已经达到最大活跃数 50新请求等了 10 秒还是没位置。Tomcat JDBC 连接池则报Timeout: Pool empty. Unable to fetch a connection。不同框架的报错措辞不一样但本质上都在说同一件事连接池的“借出”速度大于“归还”速度新请求在池子里排不到队。这条报错还有一个容易被忽略的信息点超时时间。如果报错里显示等待了将近 30 秒说明应用侧给连接获取设置的超时比较长。很多团队习惯用默认值这会导致一个问题当连接池打满时请求不会立刻失败而是集体阻塞在“等连接”这个环节整体表现就是接口大面积超时、线程池被占满故障范围进一步扩大。1.2 链路上下游的连带症状接口、数据库与业务影响连接池爆满从来不是单个请求的问题。连接池通常被整个服务共用池子满了之后所有需要操作数据库的接口都会开始排队。线上表现通常是接口 P99 延迟从几十毫秒飙升到几秒错误率同步上升业务方反馈查询变慢、超时。有个很容易被忽视的特征如果服务是多副本部署故障往往只出现在部分实例上而不是所有实例同时挂。每个进程各自维护自己的连接池某台实例的池子被打满只会拖垮路由到这台实例的请求表现为“部分请求失败”。这个特征在排查时很关键——如果所有实例同时爆大概率是数据库或某个公共依赖出问题如果只有几台实例爆那就更倾向于是应用进程内部的问题。数据库侧的表现要看根因。如果根因是慢查询数据库 CPU 和 IO 通常会同步升高如果根因是连接泄漏数据库侧可能一片平静Threads_connected甚至远低于max_connections。也就是说连接池爆满和“数据库连接数达到上限”并不是同一个概念后者只是前者的一个子集。1.3 为什么连接池会“不够用”借出与归还的底层机制要理解连接池爆满得先把连接池的工作原理捋清楚。连接池本质是应用进程内维护的一批数据库连接的集合目的是省掉每个请求重新建立 TCP 连接、进行 MySQL 认证的开销。正常的一次数据库操作流程是请求进来后从池子里borrow一条连接执行 SQL用完return回池子。池子容量受maximumPoolSize限制一旦同时借出的连接数量达到上限后续请求就只能排队等待超过connectionTimeout就抛出超时异常。用一个餐厅的比喻座位是连接客人是请求后厨上菜速度是 SQL 执行速度。如果一桌客人点太多菜迟迟不走一条慢 SQL 占着连接不放翻台率就会下降门口排队的人越来越多等不及的客人只能离开。这个机制同时解释了为什么“把连接池调大”不是万能药。连接池是进程内的共享资源调大只是增加了座位数如果后厨出菜慢的问题没解决客人依然会越积越多只是爆的时间往后推迟了一点。真正要解决的是“谁借走了连接”“为什么归还这么慢”这两个问题。2. 排查链路的核心动作日志、processlist 与慢查询三道关卡2.1 第一步从应用日志定位报错来源与时间点排查连接池爆满第一个动作永远是看应用日志别一上来就连数据库。原因是日志里记录了报错类型、发生时间窗口、涉及哪些实例这些信息直接决定后续排查方向。先确认报错出现的起始时间再回看这个时间点前后有没有发布事件、流量高峰、依赖变更。如果只有某几台实例报错说明问题是局部性的如果所有实例同一时间开始报错更要怀疑某个公共依赖或数据库本身发生了变化。这里有一个新手容易踩的坑应用日志时间和数据库服务器时间不一致。有些团队的应用容器和数据库主机没做时间同步日志里显示 14:00 开始报错数据库慢日志却定位在 13:00中间差出一个小时很容易把排查方向带偏。2.2 第二步在数据库侧抓现场processlist 是最快的信息入口连接池爆满时数据库侧最重要的工具是show full processlist。它会列出当前所有连接以及它们的状态是判断“连接到底在干什么”的最直接手段。SHOW FULL PROCESSLIST;我习惯每隔两三秒抓一次连续抓五六次因为单次快照可能正好错过关键状态。重点看三列Time、State、Command。Time特别大的 SQL要么是慢查询要么是事务没结束State里出现Sending data、Copying to tmp table、Sorting result通常意味着 SQL 在真干活出现Waiting for table metadata lock则是典型的锁等待Command这一列很关键大量Query状态说明 SQL 在跑大量Sleep状态则说明连接被借出去之后空闲挂起这往往是连接泄漏的信号。再配合下面两条命令看整体水位SHOW STATUS LIKE Threads_connected; SHOW VARIABLES LIKE max_connections;如果Threads_connected都在正常范围但应用侧大量报连接池超时那基本可以断定池子的瓶颈在应用进程内部而不是数据库连不上了。2.3 第三步慢查询日志与 performance_schema 交叉验证processlist只能看到“当前”的瞬间要还原时间窗口内的全貌还得看慢查询日志和 performance_schema。慢查询日志能看到执行时间超过阈值的 SQL把时间窗口内出现次数最多的慢 SQL 拉出来基本能锁定几条嫌疑。拿到慢 SQL 之后不要急着下结论先用EXPLAIN看执行计划有没有走索引、有没有Using filesort、有没有全表扫描。EXPLAIN SELECT * FROM orders WHERE status 1 ORDER BY create_time DESC LIMIT 100000, 20;这里更推荐的一种做法是直接查 performance_schema 里的语句聚合表SELECT SCHEMA_NAME, DIGEST_TEXT, COUNT_STAR, SUM_ROWS_EXAMINED, SUM_TIMER_WAIT FROM performance_schema.events_statements_summary_by_digest ORDER BY SUM_TIMER_WAIT DESC LIMIT 20;这个视图按 SQL 模板聚合统计执行次数和总耗时适合发现那种“单条不慢但被高频调用累计耗时可观”的 SQL。这类问题在processlist里几乎看不到因为任何一瞬间它都只是几百毫秒的普通查询。2.4 顺手好用的定位技巧从 innodb_trx 找长事务与锁等待补一个我常用的技巧。连接池爆满不一定只有一条根因很多时候是慢 SQL 叠加连接泄漏再加配置过小三个因素共同作用。为了不遗漏可以查一下当前事务表SELECT * FROM information_schema.innodb_trx\G它能列出正在运行的事务、事务启动时间和 SQL 文本。如果发现某个事务的trx_started时间很长再结合SHOW FULL PROCESSLIST里对应的连接基本就能定位到谁在占着连接不放。整个过程归纳下来就是三件事先看现象确认方向再在数据库侧抓现场定位嫌疑最后用日志和系统表交叉验证把嫌疑坐实。按这个顺序走大多数连接池问题都能在一个小时内定位到根因。3. 根因分类讨论慢SQL、连接泄漏、配置失配与外部拖累3.1 根因A慢查询把连接“借走不还”的时间拉长慢 SQL 是连接池爆满最常见的原因。一条 SQL 执行 2 秒池子大小 50那它最多同时容纳 25 条这样的查询剩下 25 条连接被其他请求占用后面的请求只能排队。处理过一个典型案例如下一条报表查询对三张大表做LEFT JOIN执行计划里出现Using join buffer实际是全表扫描执行一次要两分多钟。这条 SQL 一上线连接池立刻被打满。这类问题的特征很鲜明processlist里能看到大量Time很大的Query数据库 CPU 和 IO 会同步升高。深分页也是重灾区比如select * from orders limit 1000000, 20MySQL 会先把前 100 万行读出来再丢弃耗时自然爆炸。索引不是万能的但绝大多数慢 SQL 的根本解法还是索引设计和 SQL 改写。3.2 根因B连接泄漏——借出去之后再也没回来连接泄漏的表现和慢 SQL 完全不同。慢 SQL 至少还在执行泄漏是连接被取走之后压根没还回来。常见的触发场景包括手写 JDBC 代码在结果集处理完后忘了close在方法里return之前没用finally把连接归还把Connection对象放到静态变量或者缓存里反复使用。泄漏有个非常典型的周期性特征数据库侧Threads_connected会缓慢上升重启应用后立刻下降过一段时间又缓慢涨上来周而复始。processlist里会看到大量Sleep状态的长连接。HikariCP 提供了leakDetectionThreshold参数连接被借出超过阈值仍未归还时日志里会输出连接泄漏相关告警。这一项建议在测试环境常开线上环境确认无误报后再开。3.3 根因C连接池大小与实际并发严重失配还有一类问题纯粹是配置不对。比如某服务的 Tomcat 线程池配置了 200HikariCP 的maximumPoolSize却只有 10高峰期 200 个线程同时进来只有 10 个能拿到连接其余 190 个全部阻塞等待连接池队列瞬间堆积。这里要打破一个直觉连接池不是越大越好。连接数太多会导致数据库侧线程数膨胀锁竞争和上下文切换加剧整体吞吐反而下降。连接池大小应该和应用的并发模型、数据库的性能测试结果匹配而不是拍脑袋填一个 100 或 300。HikariCP 官方文档给过一个估算起点公式connections ((core_count * 2) effective_spindle_count)其中core_count是 CPU 核数effective_spindle_count是磁盘数。这只是起点最终要拿压测数据修正。3.4 根因D数据库锁等待和事务边界过宽锁等待和长事务也是隐藏的凶手。一条UPDATE在事务里一直不提交行锁就不会释放其他请求如果也要更新同一行就会停在Waiting for lock状态。数据库连接被占着却不干活池子自然越挤越满。更常见的是长事务业务代码在事务里调了外部接口比如发短信、调支付网关一个事务要跑几十秒甚至一分钟连接被这几十秒的空等白白占用。information_schema.innodb_trx里能看到这些长时间未提交的事务trx_started字段直接暴露了启动时间。这类问题比慢 SQL 更隐蔽因为数据库 CPU 可能完全正常processlist里可能只是显示一条Sleep或空Query看起来人畜无害实际上已经拖住了整个连接池。3.5 根因E持连接期间调用外部依赖连接被“偷走”最后一类根因要特别警惕业务代码在持有连接期间做与数据库无关的事情。最典型的是在事务中调用第三方 HTTP 接口、缓存、消息队列一旦外部依赖响应变慢连接的持有时间就被无限拉长。遇到过的一个案例某个接口在数据库事务里调用支付网关超时时间设了 60 秒支付网关在高峰期响应要 30 秒以上一条连接被占住 30 秒。池子大小 20高峰期 50 QPS 直接把连接池打穿。问题定位到这一步基本已经进入修复阶段了。排查方法也很简单翻代码看去拿连接和释放连接之间到底是不是只做了数据库操作。只要中间出现远程调用、文件读写、线程等待就都有嫌疑。4. 组合拳修复连接池参数调优、代码层治理与数据库侧整改4.1 连接池参数调整HikariCP 核心参数怎么改修复的第一步是调连接池参数但这里要强调一个顺序先止血先生效再根治。线上已经爆满时最快的手段是重启服务让连接全部归还同时临时调大maximumPoolSize把排队时间先压下来。如果一个服务用的是 HikariCP核心参数的调整逻辑如下参数默认值调整建议作用maximumPoolSize10按压测结果设置池子最大连接数minimumIdle与 max 保持一致建议两者一致避免动态伸缩引发建连开销connectionTimeout30000ms治本前可调短至 5000ms获取连接的最大等待时间idleTimeout600000ms保持默认空闲连接回收时间maxLifetime1800000ms小于数据库wait_timeout连接最大存活时间leakDetectionThreshold0关闭测试环境建议 30000ms连接泄漏检测阈值这里有两个容易踩的坑。一是maxLifetime必须小于数据库的wait_timeout否则连接会被数据库服务端先断开应用还在池子里用拿到的是坏连接。二是leakDetectionThreshold不能大于maxLifetime否则泄漏告警还没来得及输出连接已经被池子回收了检测形同虚设。4.2 代码层治理从 try-finally 到统一连接管理参数调整只能缓解症状根治要回到代码。最基础的治理是统一连接获取与释放能交给框架管理的就不要手写 JDBC。比如使用 MyBatis 管理SqlSession连接生命周期由框架保证使用 Spring 的JdbcTemplate连接也由模板类统一处理。实在需要手写也必须使用try-with-resources或者try-catch-finally确保连接归还。另一个容易被忽略的点是数据库操作超时。连接池超时只是排队超时SQL 本身执行多久还取决于数据库。建议给事务方法设置Transactional(timeout ...)或者在 JDBC 上设置setQueryTimeout避免一条跑三分钟的 SQL 拖着连接不放。此外“事务里禁止调用远程接口”这条规则应该写进团队规范。凡是需要调外部服务的操作一律先提交事务再调。这一点能从根上消掉一大半的长事务问题。4.3 数据库侧整改索引、执行计划与事务边界数据库侧的整改重点围绕慢查询展开。第一步是给高频查询和关联字段建合适的索引注意联合索引的字段顺序要匹配查询条件。第二步是改写 SQL深分页场景可以用游标分页或者延迟关联替代offset过大的写法大范围IN查询可以拆分为多次小范围查询。第三步是设置执行保护。MySQL 5.7 之后的版本支持max_execution_time可以给只读账号设置超时上限让超长 SQL 直接被数据库拒绝而不是在业务侧傻等SET GLOBAL max_execution_time 10000; SET SESSION max_execution_time 10000;针对报表类的重型查询更彻底的做法是把实时查大表改成查询预聚合的汇总表。跑批任务定时把结果算好前端只查结果表既能保证响应速度又不占连接。还有一个常见的架构手段把读流量拆分到只读实例降低主库的连接压力这需要在压测充分之后再上。4.4 案例复盘一次修复中应用的组合策略拿之前提到的某业务系统串一遍完整处理过程。该系统的连接池配置是maximumPoolSize20故障表现为部分实例接口超时报连接池错误。排查发现processlist里有一条深分页 SQL 每次执行 4 秒同时一个批处理模块手动获取连接却没有关闭两个问题叠加把 20 条连接全部吃光。处理顺序是先重启出问题的实例临时把连接池调到 60 止血然后把深分页 SQL 改成延迟关联执行时间从 4 秒降到 50 毫秒再修复批处理模块的连接泄漏最后压测确认 60 的池子对数据库无压力后逐步下调到 40 并稳定运行。这个顺序值得记住止血、治因、验证、回稳。如果一上来就只顾调大参数泄漏和慢 SQL 还埋在那里下一次故障只是时间问题。5. 从救火到防火连接池监控指标与容量规划5.1 必盯的几个连接池监控指标连接池问题靠人盯是盯不住的必须靠监控。应用侧重点盯四个指标活跃连接数、等待获取连接的线程数、连接获取等待时间、连接创建和销毁速率。这些指标在 Spring Boot 场景下可以通过 Micrometer 暴露由监控系统抓取并在看板上展示是比较通用的方案。数据库侧要看Threads_connected、Threads_running、慢查询次数、连接数触顶比例。告警阈值不要等爆了才设活跃连接数达到池大小的 70% 就该提醒等待线程数超过 0 持续一分钟也要提醒。监控的意义在于连接池爆满之前通常已经经历了从 50% 到 80% 再到 95% 的爬坡过程。早发现就能早干预而不是深夜被叫起来看现场。5.2 连接池容量规划从经验公式到压测验证容量规划是决定连接池设多少的底层依据。一个健康的服务在日常峰值流量下活跃连接数不应该长时间超过池大小的 60% 到 70%要为突发流量留出缓冲。压测是唯一可靠的验证方式用压测工具按预估峰值流量的 1.5 倍打流观察活跃连接数和响应延迟直到延迟拐点出现那个点对应的连接数就是这台实例的连接池上限。经验公式connections ((core_count * 2) effective_spindle_count)可以作为初始值但不要死守最终以压测为准。另外要区分场景连接持有期主要是在等 IO 的服务连接池可以略大大量占用 CPU 的服务连接池要保守。前者连接是“在等待”后者连接是“在干活”两种模型对池大小的需求完全不同。5.3 长效告警与例行巡检最后一件事是建立例行巡检。每周花半小时看三样东西慢查询 Top 列表、连接池使用率趋势、连接泄漏告警。配置里保留连接的打开与关闭日志配一条定时任务把连接池关键指标拉出来画趋势图出现持续上升的曲线就要留意。发布流程里也建议加一道检查把“本次变更是否涉及数据库连接使用”列为必填项涉及连接变更的必须走代码评审。这套机制不需要很重的平台一个定时脚本加一个监控面板就能跑起来但遇到实际故障的时候能省掉大量时间。连接池问题的本质是资源管理问题和内存泄漏一样预防永远比抢救容易得多。最后分享一个我自己的经验这些年排查过的 MySQL 连接池爆满案例几乎没有一个是一句“把连接池调大”就能真正解决的大多数都是慢 SQL、连接泄漏、配置失配、长事务这几类问题叠加出来的结果。所以我现在遇到连接池报错第一反应永远是先看processlist的Time和State三分钟就能判断这波是 SQL 在埋头干活还是连接被悄悄“偷走”了。另外有个小习惯可以分享把connectionTimeout从默认的 30 秒调短到 5 秒左右。线上如果出问题报错会来得更快、更明显虽然会让部分请求快速失败但总比所有请求都卡死要好。希望这篇排查思路能帮你在下次看到连接池报错时少走弯路。

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

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

免费获取报价 →
↑