ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

连接池爆满排查与根治:从原理到实战的完整指南

连接池爆满排查与根治:从原理到实战的完整指南 1. 连接池爆满到底是什么问题大概在半年前我负责的一个线上订单服务在某天晚高峰突然告警数据库连接池报出Connection pool exhausted错误紧接着大量请求超时报表显示活跃连接数直接顶到了上限。那是我第一次真正意义上被“连接池爆满”按在地上摩擦。事后复盘这个问题其实一点都不神秘连接池是一个固定的资源池子业务上每来一个请求要拿连接用完还回来一旦某个环节让连接“只借不还”或者借的速度远超还的速度池子就会被打穿后台数据库的可用连接数也会同步告急。先说清楚连接池到底是什么。应用与MySQL之间不会每次请求都新建物理连接那样开销太大所以中间加一个池子里面提前备好一批连接请求来了直接拿现成的用完放回去复用。连接池的核心几个参数就是初始大小、最大大小、最小空闲、等待超时时间等。爆满说白了就是“池子里的连接被全部占用还有新的请求在排队等连接”等待超时之后就抛出异常服务表现为接口大面积失败。这问题的麻烦之处在于表面上是连接池参数不够实际上往往是业务SQL、事务、代码逻辑或数据库本身出了问题连接池只是个背锅的。如果只知道调大连接池参数通常只是把炸弹延时触发后面爆得更惨。所以我更愿意把“连接池爆满”当成一个系统性症状而不是一个独立故障。这篇文章我会从原理到排查、从根因到修复把整个处理链路完整讲一遍。适合刚遇到这类问题的开发运维同学也适合想系统掌握数据库连接治理的进阶者内容直接对标生产环境实操不绕弯子。2. 先理解连接池的生命周期和爆满机制2.1 一条SQL从应用到MySQL的完整路径一条SQL要真正执行需要经历应用线程发起业务请求 → 从连接池获取连接 → 执行SQL → MySQL处理 → 返回结果 → 释放连接归还池子。每个环节都有可能让连接停留时间变长。连接池本身有几种状态判断池中空闲连接个数、已分配连接个数、等待获取连接的线程个数、创建和销毁连接的速率。当业务并发请求数大于“池中空闲连接数”时新请求就会进入等待队列等待时间受connectionTimeout等参数控制。如果等待超过阈值通常就会抛出连接获取超时异常。MySQL服务端视角也有自己的连接上限默认max_connections常见配置是151或更高。每个客户端连接在MySQL端都是一个线程内存占用按连接计算。应用连接池最大连接数如果远大于MySQL的max_connections就会造成“池子没爆MySQL先爆”的局面。这点很多人容易忽略。2.2 连接池爆满的两个直接原因第一种池子里的连接确实不够用。可能业务峰值确实超过了设计容量连接池最大连接数太小。这种情况最常见于“上线前没做压测”的项目平时看起来够用一到大促直接完蛋。第二种连接没有及时归还。一是在长事务中由于各种原因持锁不释放比如事务里查了慢SQL执行要5秒事务就要保持5秒那么连接也会被事务占用5秒。二是代码里写try { ... }之后忘了在finally里关闭连接出现了连接泄漏。三是异常路径没有释放连接比如获取连接后发生了未捕获异常连接没有归还。真正排查的时候两种情况往往同时出现慢SQL拉长了持有时间并发一高就把池子吃满再加上几条连接泄漏的代码池子慢慢被蚕食最终连健康检查都拿不到连接。2.3 影响范围不只是“报错”那么简单连接池爆满的直接后果是应用接口报错但这只是开始。连接池中的连接如果因为网络超时、MySQL崩溃等原因已经处于失效状态应用却不知道拿到一个坏连接执行SQL会报“Communications link failure”等错误触发额外的重试。重试又去连接池拿连接进一步加剧抢占。数据库端同样会受影响。连接数到达上限后新的连接请求会报“Too many connections”即使是运维想要登录数据库执行kill都可能进不去只能通过额外管理通道或者重启应用先释放连接。还有大量连接同时处于死锁等待或锁等待状态时会让InnoDB的行锁链表膨胀产生更多的锁等待和死锁形成恶性循环。所以处理连接池爆满首要目标是“止血”恢复系统可用第二步才是找根因第三步才是长期治理。下面先讲止血和快速定位。3. 快速定位和现场信息采集3.1 第一时间应该做什么遇到连接池爆满我的习惯动作是先看监控再抓现场最后动刀。不要一上来就重启应用重启虽然能快速清空连接池但如果没有保留现场你永远不知道问题是怎么发生的。而且线上服务通常多实例部署重启一个实例未必能解决整体问题。现场信息至少包括以下几类连接池监控活跃连接数、等待线程数、获取连接耗时、应用日志获取连接超时异常、慢SQL日志、MySQL监控Threads_connected、Threads_running、当前processlist、系统负载和慢查询日志。如果没有现成监控可以临时用几条SQL看数据库端的实时情况。这个非常关键。3.2 数据库端一分钟快速体检登录MySQL如果还能登录的话先执行这几条SQLSHOW GLOBAL STATUS LIKE Threads_connected; SHOW GLOBAL STATUS LIKE Threads_running; SHOW VARIABLES LIKE max_connections;Threads_connected表示当前所有客户端连接数Threads_running表示正在并发执行的线程数。如果Threads_connected长期接近max_connections说明连接数本身已经是瓶颈如果Threads_running远大于几十说明数据库端存在大量并发查询CPU和IO大概率也不健康。接着查看当前所有会话状态SHOW FULL PROCESSLIST;重点关注每条记录的Command和Time列。Command常见值有Query、Sleep、Locked等。Time越大越可疑。如果大量连接停留在Sleep状态说明拿到连接后没干活也没释放可能有连接泄漏如果大量Query状态而且时间很长说明有慢SQL在拖如果出现Waiting for table metadata lock这样的状态说明有DDL操作或者未提交事务占了元数据锁。SHOW PROCESSLIST的输出不是完整的SQL只是当前正在执行的语句摘要。想看完整慢SQL要结合慢查询日志或performance_schema。3.3 抓取锁等待和事务信息如果进程列表里大量线程处于锁等待要立即查事务和锁信息SELECT * FROM information_schema.INNODB_TRX WHERE trx_stateRUNNING\G SELECT * FROM sys.innodb_lock_waits\GINNODB_TRX能看到未结束事务的开启时间、执行SQL和线程ID。如果发现某些事务运行了几分钟甚至几小时基本可以确定是长事务占着连接、锁着数据不放开。这时候就需要评估如何安全结束这些会话一般是记录线程ID后与业务方确认再执行KILL thread_id。3.4 从应用日志侧反查线索连接池爆满的异常信息也会暴露细节。例如HikariCP的报错会提示Connection is not available, request timed out after 30000msDruid会提示wait millis 30000, active 100, maxActive 100。这些数字能告诉我们连接池的最大连接数和等待超时时间。应用日志里还要找业务线程栈。如果方便的话在出问题时执行jstack抓一次Java线程快照重点看有多少线程阻塞在HikariPool.getConnection或DruidDataSource.getConnection方法上。线程栈会直接告诉我们哪些业务代码最密集地获取数据库连接这时候根因范围大大缩小。4. 逐一根因分析谁在消耗连接现场信息收集完毕我们已经有了连接数、线程状态、锁等待、应用日志这些东西。接下来就是结合这些证据一条条确认根因。4.1 连接池参数配置不合理最常见的配置问题是“最大连接数设置得过小”或“最小空闲连接数设置得过大”。最小空闲数设得大意味着连接池常年保持着大量空闲连接而MySQL服务端只知道你有这么多客户端连接过来不会区分空闲还是忙碌Threads_connected一直偏高。最大连接数设得小业务一波动就容易触顶。举个例子一个服务有4个实例每个实例连接池maximumPoolSize设置成30总共120个连接。如果MySQL的max_connections只配了150再算上其他服务和运维连接其实已经没有余量了。更糟的情况是应用连接池的连接数上限是300而每个请求内部还会再嵌套调用另一个数据源比如多数据源场景总连接数翻倍。排查参数问题的关键是“对账”列出所有应用实例的连接池配置乘以实例数加上其他服务的连接看看是否接近或超过MySQL的max_connections。此外还要看连接池是否设置了connectionTimeout、validationTimeout等参数这些参数太小会导致“误杀”太大则会导致排队时间过长。4.2 慢SQL与长事务是隐形杀手很多连接池爆满不是连接数配置不合理而是单条SQL把连接“黏住”了。一次前端请求往往不只执行一条SQL可能是一个事务里包含多条SQL或者循环里批量执行每条SQL耗时200毫秒一个请求循环50次这个请求就要占用连接10秒钟。10秒看着不长但如果有100个并发这样的请求池子瞬间就没了。慢SQL怎么定位最直接的方法是打开慢查询日志。MySQL里设置SET GLOBAL slow_query_log ON; SET GLOBAL long_query_time 1;这里long_query_time1表示超过1秒钟的SQL都会被记录。线上如果流量大建议不要全量开太久抓取5到10分钟就够。再看日志里有没有重复出现的相同SQL如果有就是典型的“高频慢SQL”。解决手段无非是加索引、改写SQL、拆分批量操作、或者引入缓存。长事务的问题比慢SQL更隐蔽。哪怕每条SQL执行很快只要事务一直不提交它占着连接同时可能占用行锁或间隙锁导致其他SQL排队。定位方法参考前面说的INNODB_TRX和sys.innodb_lock_waits。有些长事务来自代码中事务嵌套、方法间传播事务导致事务范围变大或者异常被吞掉没触发回滚。4.3 连接泄漏只借不还的Bug连接泄漏是最让人头疼的问题因为它在连接池监控上表现为活跃连接数持续上升重启应用后连接数回落然后过一段时间再次上升直到爆满。如果用SHOW PROCESSLIST看大量会话是Sleep状态Time字段从几分钟到几小时不等而且这些连接的来源IP都是应用服务所在的主机。Java生态里最典型的就是用DriverManager.getConnection()获取连接后没有在finally或者try-with-resources中关闭。另一个经典场景是动态代理或AOP切面处理事务时如果目标方法抛出异常而切面没有正确回滚导致事务管理器的资源没有被释放。排查连接泄漏可以结合应用线程Dump和performance_schema的events_statements_history找到持有连接最久的线程及其最后的SQL。在实际工作中我会用一条SQL快速找出“疑似泄漏”的连接SELECT id, user, host, db, command, time, state, info FROM information_schema.processlist WHERE command Sleep AND time 300;把这条查询做成告警一旦某个来源IP的Sleep连接超过N条就提示连接池可能存在泄漏。另外连接池本身也有泄漏检测机制比如HikariCP的leakDetectionThreshold参数如果设置了会在连接被占用超过阈值时打印告警日志生产环境建议开启。4.4 突发流量和外部请求放大有些爆满场景没有代码Bug纯粹是流量太大。比如营销活动秒杀、数据同步任务整点触发、爬虫集中抓取等。这类情况有几个特征连接数在某个时间点急促上升持续一段时间后下降和业务流量曲线高度吻合。排查时先看网关或负载均衡的QPS再看服务接口的调用量除了数据库连接池之外线程池也可能同步飙高。如果能在监控上看到“连接池活跃连接数曲线”和“接口QPS曲线”几乎同频上涨那么解法就是限流、扩容和削峰。比如在网关层对热点接口设置令牌桶限流或者将整点任务改成随机延后执行加小抖动避免所有任务同时开跑。还有一种“请求放大”的情况很值得注意应用A调用应用BB发生超时后A做重试。一个请求超时被重试3次连接池的压力直接乘以3倍。这时候如果B的数据库连接池只够处理单次请求量重试风暴就会把B的连接池打满。我之前遇到过一个案例就是上游超时重试机制引发的“雪崩”最后是通过限制重试次数和设置全链路超时解决的。4.5 数据库端配置与资源瓶颈连接池爆满也可能是MySQL自己的连接数上限被顶到而非应用连接池设置得不够。当Threads_connected已经等于max_connections时新连接进不来应用侧连接池创建连接也会失败连接池只能一直拿不到新连接然后报超时。max_connections的值不是越大越好。每个连接在MySQL内部都需要线程栈空间MySQL 8.0默认每个线程thread_stack是256KB连接数一多内存占用非常高。在一个16GB内存的数据库实例上无脑把max_connections调到5000会让系统在连接数打满前先发生内存耗尽或SWAP。因此设置这个参数要结合innodb_buffer_pool_size、系统内存、以及实际并发需求来综合评估。另外要关注innodb_thread_concurrency的值它限制InnoDB内部并发执行线程数量。如果设置太小很多线程虽然建立了连接但无法实际执行SQL它们会堆积在等待队列中外观看上去就是连接数高、CPU低、请求全部挂起与连接池爆满表现几乎一样。排查时可以临时调大这个值或改为0不限制做对比验证。5. 从止血到根治的完整操作方案5.1 紧急重启与临时扩容的取舍如果线上服务已经大量报错第一要务是恢复可用。可选操作有重启应用实例、临时调整连接池参数、临时调整MySQL的max_connections、Kill掉异常会话。重启应用实例是最快的止血方式能立刻清空连接池里的异常连接。但要注意不要所有实例同时重启应该逐个滚动重启保证服务可用性。重启前抓一次线程Dump和数据库Processlist保留现场。如果应用支持动态配置可以临时把maximum-pool-size往上调比如从100调到300同时把connection-timeout缩短到3秒快速失败而不是无限排队。这种做法只用于应急因为调大连接池只是推迟爆满而且会加重数据库端压力。数据库端也可以临时调高max_connections但不要超过物理内存能承受的范围。更安全的做法是Kill掉那些长时间处于Sleep的会话和明确是异常的长事务释放连接资源。Kill操作前务必确认会话对应的业务影响以免误杀正在执行重要任务的线程。5.2 连接池参数设定的参考原则连接池参数不能拍脑袋填一般按照这个思路来先评估业务接口的QPS和单接口平均DB执行时间计算需要多少并发连接能力。公式非常简单并发所需连接数 平均QPS × 平均单次DB访问耗时秒比如平均QPS 500单次DB访问耗时50ms那么所需并发连接数就是500×0.0525。当然这只是理论值还要考虑峰值、慢SQL、外部接口阻塞等所以一般再加上2~3倍缓冲。以HikariCP为例我通常的配置基线是这样maximumPoolSize根据上面公式计算出来的值再往上取整初始值可以设成和最大值一样避免运行过程中频繁创建连接。minimumIdle保持和最大值一致因为频繁收缩和扩容连接池没有意义反而增加连接创建开销。connectionTimeout设为3000到5000毫秒太长会把超时错误拖成雪崩。maxLifetime建议设置为数据库wait_timeout的70%到80%例如数据库wait_timeout是28800秒则maxLifetime设为1800000毫秒30分钟保证MySQL没有先回收连接。validationTimeout不要小于1000毫秒。Druid的配置思路类似但它有更多监控能力。在生产环境我建议开启Druid的filter.stat监控并通过监控页查看活跃连接数、SQL执行次数、慢SQL次数等指标用数据来判断参数是否合理。Druid还有一个参数removeAbandonedtrue和removeAbandonedTimeoutSeconds300可以在连接被占用超过5分钟后自动回收并打印堆栈信息这是排查连接泄漏的利器。不过在核心交易系统上开启强制回收要小心可能会截断正常的长事务需要结合业务评估。5.3 慢SQL和索引治理实操慢SQL是连接池爆满最常见的诱因所以治理慢SQL是长期有效的解决方案。基本流程是收集慢日志 → 按执行次数和耗时排序 → 分析执行计划 → 添加或优化索引 → 改写SQL或调整业务逻辑。查看单条SQL的执行计划EXPLAIN SELECT * FROM orders WHERE order_no abc123\G重点看type字段从好到差分别是const、eq_ref、ref、range、index、ALL。如果看到ALL说明是全表扫描数据量一大必然慢。另外看rows预估扫描行数和Extra中是否出现Using filesort、Using temporary这两个词出现通常意味着排序或分组操作没有用到索引需要优化。比如有一张订单表经常按user_id和status查询但索引只建在user_id上那么status条件就会在回表后过滤数据量大了查询自然慢。这时候一个(user_id, status)的联合索引就能解决问题。但索引不是越多越好每个索引都会降低写入性能、占用存储空间我一般只给高频查询的where条件和order by字段建联合索引。还有一个容易忽略的问题是隐式类型转换。比如字段是varchar类型查询条件是order_no 123456MySQL会自动把字符串转成数字导致索引失效。夜里面排查慢SQL时看到明明有索引却不走十有八九是这个原因。解决办法就是参数也传字符串或者把字段类型改掉。对于复杂的多表join如果驱动表选错也容易产生慢查询。某些情况下把大表的过滤条件提前下推或者改写成子查询、拆成多条查询反而更快。这个要结合具体业务不建议无脑用ORM生成的复杂联表查询。5.4 连接泄漏的场景化修复连接泄漏通常不是一个点而是某几个类或某几个操作都存在类似问题。修复前一定要先定位到具体的代码位置。排查步骤是这样的首先确认数据库端有大量Sleep连接并且来自应用服务的IP然后在应用侧开启连接池的泄漏检测HikariCP设置leakDetectionThreshold60000Druid设置removeAbandonedtrue并配置removeAbandonedTimeout接着等触发告警后查看日志里的异常堆栈堆栈会指明是哪个类哪个方法获取了连接却没有关闭。定位到代码后用try-with-resources或finally确保连接关闭。举一个实际例子一个导出Excel的功能在循环中每读一批数据就执行一次查询然后往Excel写入如果ResultSet或者PreparedStatement没有关闭连接就被占用到最后。这类问题在低并发下根本看不出来一旦导出请求变多就出问题。修复方法就是把查询封装到一个独立方法里使用try-with-resources保证自动关闭同时把循环查库改成一次性查询或分批查询并主动关闭。另一个典型的泄漏来自ThreadLocal。有的人会把Connection放到ThreadLocal里做同一线程内的数据库操作如果线程是线程池复用的而请求结束后没有清除ThreadLocal中的连接那么下一次请求会拿到上次残留的连接。这类问题更隐蔽只能在线程池的execute前后清理ThreadLocal。排查时可以用日志追踪同一个线程在不同请求中持有了同一个连接ID。5.5 容量规划、限流与故障演练连接池治理不能只靠出问题时救火更重要的是提前做容量规划和限流设计。先做容量基线用压测工具模拟线上流量逐步加大并发观察连接池活跃连接数、响应时间、数据库负载三个维度的变化确定系统能承受的最大QPS。根据压测结果再去设定连接池大小和网关限流阈值。比如压测发现单实例最大支撑200QPS一共4个实例那么整体容量就是800QPS网关层限流可以设定为这值的80%也就是640QPS留出缓冲。限流不只是网关层的任务应用层也要有限流。最简单的做法是在Spring Boot里配合Sentinel或Resilience4j对关键接口设置线程池隔离和信号量隔离。线程池隔离能防止某个慢接口耗尽整个服务的线程资源信号量隔离更适合控制并发数据库操作的规模。在故障演练方面我的经验是定期模拟连接池爆满、数据库连接数打满、慢SQL拖垮连接池等场景验证告警、自动扩容、熔断降级是否真的能生效。曾经有次演练就发现告警规则虽然存在但监控平台连接数据源的账号权限不足告警根本没触发。这种问题如果不演练永远发现不了。6. 典型问题速查表与避坑心得6.1 连接池爆满常见场景对照我把几个高频场景整理成一张表方便大家遇到问题时对号入座现象可能原因快速确认方法处理方向连接池活跃连接数缓慢上升重启后回落连接泄漏查processlist大量Sleep连接开启leakDetection定位未关闭连接代码并修复连接数瞬间打满与流量峰值同步容量不足或突发流量对比监控曲线QPS与连接数集群扩容、限流削峰大量线程处于Waiting for table metadata lock未提交长事务或DDL查INNODB_TRX提交或回滚事务、优化DDL窗口Threads_running很高CPU打满慢SQL或锁竞争查慢查询日志和processlist索引优化、SQL重写Threads_connected接近max_connections但running不高连接数配置过度或空闲连接过多查看各应用连接池配置收缩连接池调整连接池和数据库上限报错Communications link failure连接被MySQL中途断开查数据库timeout和网络调整maxLifetime和网络参数只有部分实例报错单实例GC异常或容器资源不足查GC日志和宿主监控运维排查实例健康状态这张表不能覆盖所有情况但能覆盖十之八九。实际排查时往往会同时命中多行比如“慢SQL导致连接持有时间变长叠加突发流量最终触顶”这时候要分清主次优先消除最耗连接的因素。6.2 我踩过的几个真实深坑第一个坑连接池只调大不调优。以前遇到爆满第一反应是把maximumPoolSize从100改成500当时确实缓解了。但第二天数据库连接数飙升MySQL内存直接涨到告警Threads_connected到了400多一堆连接都在Sleep。后来发现是有个定时任务忘了释放连接调大连接池只是让泄漏的更慢被打出来。所以紧急扩容可以但后续必须清理泄漏代码否则就是养虎为患。第二个坑maxLifetime大于数据库wait_timeout导致连接被服务端断开。之前把HikariCP的maxLifetime设置成30分钟但MySQL的wait_timeout默认配置短结果是MySQL主动断开了空闲连接应用却在池子里拿着失效连接执行SQL报错后重试白白增加连接池压力。后来统一把maxLifetime调成比wait_timeout短的时间问题消失。第三个坑Kill会话前没确认业务影响。一次排查锁等待时看到一个会话跑了20多分钟我以为是无主长事务直接Kill结果那个会话对应的是一个数据仓库的跑批任务跑了一半被干掉下游结果全错。后来再遇到这种事强制约定Kill前必须通过应用CMDB查清这个线程对应的接口和调用方。第四个坑只看数据库端不看应用线程Dump。有次连接池爆满数据库侧一切看似正常连接数不是特别高但应用侧大量线程卡在创建连接上。后来抓线程Dump发现是某个接口在循环里每次都新建数据库连接压根没用连接池。这只有应用线程Dump才能看出来。6.3 实战中的排查顺序建议最后给你一套我觉得最顺手的排查顺序照着走基本不会乱先看监控大屏连接池活跃连接数、线程数、QPS、数据库Threads_connected、慢SQL数30秒内确定大方向。再抓数据库进程列表SHOW FULL PROCESSLIST统计Command状态分布。查慢查询日志和INNODB_TRX锁定可疑SQL和长事务。抓应用线程Dump找到阻塞在连接获取上的线程定位到具体代码Class。结合业务发布记录和流量事件确认是否有变更、定时任务或上游重试触发了变点。先止血滚动重启、Kill异常会话、限流再修根因最后调参数。事后补监控告警连接池活跃连接数超过阈值、Sleep连接数量异常、获取连接超时都要有告警。我个人在实际操作中的体会是连接池爆满其实并不可怕可怕的是把连接池当万能宝箱缺了就一通乱调。每一次爆满都是系统在提醒你要么容量规划不足要么代码质量有问题要么缺少必要的保护机制。真正解决完一次系统会比之前结实很多。最后再分享一个小技巧在压测环境里故意制造一次连接池爆满把当时的线程Dump、数据库进程列表、慢SQL日志全部存下来做成一个“故障复盘包”。下次再遇到类似问题直接拿这套包比对定位速度能快一半。这个习惯我保留到现在每次复盘都受益。
返回列表