1. 项目概述
1.1 核心需求解析
周四下午两点半,监控大屏上突然飘红了一片。数据库连接池活跃连接数像坐了电梯一样往上窜,三分钟之内打满了上限。紧接着业务侧开始报错,一堆"Get connection timeout"的异常刷满了日志。这不是什么高并发大促流量把数据库打爆的剧本,而是一个在线交易系统在平平无奇的午后遇到的真实故障。我在这行干了十几年,从MySQL 5.6一路用到8.0,连接池爆满这种问题见过太多次,每次根因都不一样,但排查思路是有章可循的。
先说清楚"连接池爆满"到底是个什么事。应用层为了减少频繁创建和销毁数据库连接的开销,会维护一个连接池,比如HikariCP、Druid、dbcp这些。连接池里放着若干个到MySQL的物理连接,业务线程来申请连接,用完了归还。当池子里所有连接都被借走且没有及时归还时,新的请求就只能等待,等待超过阈值就直接报错。这就像一家餐厅只有十张桌子,客人都坐着不走,门口排队的越来越长,最后新来的客人一看排队时间太长直接走了。
这篇内容适合谁?后端开发、运维工程师、DBA,或者那些正在被线上连接池打满问题折磨的技术人。我会把排查套路、根因分析、解决方案和预防手段一次性讲透,包含我实际踩过的坑和验证过的手段。排查连接池爆满的核心就三件事:看懂监控数据、定位连接去向、揪出占用连接不还的元凶。
1.2 涉及技术栈与问题特征
连接池爆满问题的典型特征非常统一:应用日志里大面积出现连接获取超时,数据库端的Threads_connected指标飙到数千甚至上万,应用响应时间从几十毫秒恶化到几秒。但表象一致不代表病因一致。我遇到过慢SQL把连接全占死的场景,遇到过连接泄漏导致连接只借不还的场景,也遇到过连接池参数配置不合理导致高峰期不够用的场景。同一张病脸,四种完全不同的病根,对症下药之前必须先做鉴别诊断。
这次的故障排查涉及的核心技术点包括:MySQL连接管理机制、连接池参数调优、慢查询日志分析、线程状态解读、网络层和防火墙策略排查、以及应用代码层面的连接使用规范。我不打算写一篇教材式的原理长文,而是按照真实故障发生的时间线,一步步复盘当时是怎么查的、怎么看出来的、最后怎么解决的。
2. 连接池爆满的底层逻辑与根因排查
2.1 连接生命周期与池化机制
要理解连接池为什么会爆,得先理解MySQL服务端是怎么看待连接这件事的。每个到MySQL的连接,在服务端都是一个独立的线程,负责处理这个连接上发来的SQL请求。max_connections参数决定了MySQL最多能同时接受多少个连接,默认值通常是151,线上环境一般调到500到2000不等。当Threads_connected达到max_connections时,MySQL会直接拒绝新连接,报"Too many connections"错误。
连接池的作用是在应用端维护一批已建立的连接,避免每个请求都走一遍TCP握手、MySQL认证、连接初始化的完整流程。这个过程听起来不复杂,但连接创建的开销其实不小,尤其是在HTTPS环境下,一次完整建连可能要消耗几十毫秒。连接池的核心价值就是把"建连"这个动作提前做完,业务请求来了直接拿现成的连接用。
连接池的工作模型是"借"和"还"。业务代码里从连接池获取连接,执行SQL,然后释放连接。HikariCP这类高性能连接池还做了连接复用和保活检测,空闲连接超过idleTimeout会被逐步回收,使用中的连接如果超过maxLifetime会被强制替换。这套机制正常情况下运转良好,但一旦业务代码里出现"借了不还"的情况,池子里的连接就会被慢慢耗尽。
有个数据可以说明问题:假设连接池配置maximumPoolSize = 200,每个连接从获取到归还平均耗时50毫秒,那么这套池子每秒理论上能处理4000个请求。但如果代码里有个事务执行了5秒还没提交,期间一直占着连接不放,那每秒能处理的请求数就骤降到40。如果同时有多个这样的慢事务并发,连接池瞬间就能被打满。这就是为什么连接池爆满的问题,第一嫌疑永远是慢SQL和长事务。
2.2 根因分类与快速定位策略
我在处理这类故障的时候,习惯先把可能的原因分成三类:第一类是连接池容量不足,说白了就是池子太小,高峰期并发一上来就不够用;第二类是连接被长期占用,包括慢SQL、长事务、锁等待、连接泄漏,这类问题导致连接周转率极低;第三类是连接创建失败,比如网络分区、认证失败、MySQL拒绝连接,导致应用不断尝试新建连接然后失败,形成雪崩。
快速定位的第一步不是去看应用代码,而是先看MySQL端的状态。命令很简单:SHOW STATUS LIKE 'Threads_connected';,再看SHOW STATUS LIKE 'Threads_running';。Threads_connected是当前连接数,Threads_running是正在执行SQL的线程数。如果Threads_connected很高但Threads_running很低,说明大量连接处于空闲状态,但连接池不释放——这是连接泄漏的典型特征。如果Threads_running也很高,说明确实有大量SQL在并发执行,要么是流量真的很大,要么是有慢SQL积压。
第二步是看SHOW PROCESSLIST或者查information_schema.processlist表,重点看每个连接的Command列和Time列。Command为Sleep且Time很大的连接,就是空闲连接,理论上可以被回收。Command为Query且Time很大的连接,就是正在执行的慢查询,这是重点排查对象。Command为Locked表示在等待锁,这类连接如果不处理,会引发连锁反应。
用一个具体案例来说明。上次排查一个电商系统的连接池爆满问题,先看Threads_connected超过800,Threads_running只有不到20,明显不对劲。再查processlist,发现有300多个连接处于Sleep状态,Time普遍超过300秒,而且这些连接的Host都指向同一台应用服务器。这就很蹊跷了,连接池的idleTimeout配的是60秒,按道理空闲连接早该被回收了,为什么还能看到300多个老连接?后来查应用日志发现,这台服务器当天刚发过版本,新代码里有个@Transactional注解加错了位置,一个读多写少的接口被整体包进了事务,事务里的连接长时间不释放,连接池还认为这些连接是"活跃使用中"的,于是不断创建新连接顶上,最终把连接池和MySQL的连接数全部撑爆。
2.3 排查命令与监控维度
我把这套排查命令整理成一个速查表,贴出来方便你直接抄:
| 排查目标 | 命令/监控项 | 关键指标解读 |
|---|---|---|
| 全局连接趋势 | SHOW GLOBAL STATUS LIKE 'Threads_connected'; | 持续高位运行说明连接回收异常 |
| 正在执行数 | SHOW GLOBAL STATUS LIKE 'Threads_running'; | 超过50说明有SQL拥堵或高并发 |
| 连接明细 | SELECT id, user, host, db, command, time, state, info FROM information_schema.processlist; | 重点关注Time大、Command异常的连接 |
| 锁等待 | SELECT * FROM information_schema.innodb_trx; | 查看未提交的长事务 |
| 慢查询 | SHOW GLOBAL STATUS LIKE 'Slow_queries';或开启慢查询日志 | 统计慢查询数量变化 |
| 连接来源分布 | SELECT host, COUNT(*) FROM information_schema.processlist GROUP BY host; | 判断连接是否集中在特定应用实例 |
这几个命令执行完,基本能锁定问题的大方向。但要注意,SHOW PROCESSLIST在连接数特别多的时候输出量很大,建议直接查information_schema.processlist并用WHERE time > 10这样的条件过滤掉短连接,只看长时间存在的连接。
另外提一句,如果生产环境开了Performance Schema,还可以从events_statements_current和events_transactions_current表里拿到更精确的SQL执行情况和事务状态。这些表在连接数多的时候查询会有点慢,但胜在信息全,能直接看到哪个连接正在跑什么SQL、事务开启了多久。排查时我是两个方案并行:快速定位用processlist,深挖根因用Performance Schema。
3. 案例实操:一个真实的连接池爆满事件
3.1 故障现象与现场信息采集
那次故障发生在某个交易系统的生产环境,时间大概是下午两点出头。第一波报警来自应用层监控,错误信息清一色是HikariPool-1 - Connection is not available, request timed out after 30000ms。这是个非常经典的HikariCP报错,意思是请求连接等了30秒还没等到,直接超时。紧接着MySQL侧的监控也报警了,Threads_connected从正常的300左右一路冲到1500,接近max_connections = 2000的上限。
我第一步做的事是把当时能抓到的现场信息全部记录下来,包括:应用日志里报错的时间点和频率、MySQLprocesslist的快照、innodb_trx表的事务快照、系统监控里的CPU和磁盘IO数据。为什么第一步是采集信息而不是直接动手?因为连接池爆满是一个动态发展的过程,如果先执行了一些"急救"操作,比如重启应用或者kill连接,现场就被破坏了,后面再想定位根因就难了。
现场的processlist快照显示了一个很有意思的现象:大量连接的Command是Query,Info字段展示的SQL都是同一张业务表t_order的SELECT语句,而且Time都在10秒以上。这就基本排除了连接泄漏的可能,因为如果是连接泄漏,Command应该是Sleep而不会是Query。真正的问题是这些SELECT语句本身跑得太慢,单个查询10秒以上,导致连接周转不开。
3.2 慢SQL定位与执行计划分析
顺着t_order这张表去查,先看表结构。t_order有约3000万行数据,查询条件是WHERE merchant_id = ? AND order_status = ? ORDER BY create_time DESC LIMIT 10。从业务角度说,这个查询无非就是"查某个商家的某状态下最近10笔订单",非常常见的需求。但看执行计划,问题立刻暴露:type = ALL,全表扫描,rows预估670万。再看表索引,虽然建了idx_merchant_id单列索引,但选择性很差——单个merchant_id下有数百万条订单,MySQL优化器认为走索引回表还不如直接全表扫。
这里有个关键细节:为什么之前没有暴露问题?因为这个系统的商家维度数据量是逐渐涨起来的。三个月前merchant_id下可能就几十万条数据,全表扫描虽然慢但还在可接受范围。随着业务增长,单商家订单量突破百万级后,全表扫描的代价急剧上升,执行时间从几百毫秒恶化到十几秒。这就是典型的"数据量增长引发的性能悬崖",不是代码写错了,是数据量跨过了某个临界点。
在这个案例里,光靠加索引解决不了根本问题,因为merchant_id加order_status的组合索引虽能缩小扫描范围,但ORDER BY create_time DESC还需要额外排序。最优解是建立一个(merchant_id, order_status, create_time)的联合索引,这样MySQL可以直接按索引顺序取出前10条记录,连排序都省了。这个索引建好之后,查询时间从12秒降到30毫秒左右,连接池的压力立刻缓解。
整个操作过程:先SHOW INDEX FROM t_order看现有索引,确认没有合适的联合索引;然后用EXPLAIN SELECT ...验证全表扫描的判断;接着ALTER TABLE t_order ADD INDEX idx_merchant_status_time (merchant_id, order_status, create_time);最后再用EXPLAIN确认type变成ref,rows降到几百。注意线上大表加索引要评估锁表时间,MySQL 8.0支持INPLACE算法,一般不会阻塞读写,但最好还是选业务低峰期操作。
3.3 连接数参数调整与临时扩容
加索引是治本的手段,但索引建立需要时间,慢SQL的影响还在持续。不能干等着索引建完再恢复业务,所以同时做了几项"急救"操作。第一,临时调大连接池上限,把HikariCP的maximumPoolSize从200调到500。这是个饮鸩止渴的手段,但能争取到喘息时间。第二,在MySQL端临时把max_connections从2000调到3000,避免出现Too many connections的硬错误。第三,联系业务方对慢查询接口做了限流,降低新请求涌入的速度。
这里要解释一下为什么不建议一上来就重启应用。连接池爆满的时候,重启应用确实能清空连接池状态,看似解决了问题,但如果没有解决根因,重启之后连接池很快又会被打满。而且在高并发场景下重启应用会造成服务中断,影响面更大。正确的做法是"先止血,再治病":先通过参数调整和限流让系统别继续恶化,然后立刻定位根因并修复。
还有个细节容易被忽略:连接池的参数不是越大越好。maximumPoolSize = 500意味着应用可以同时持有500个数据库连接,如果MySQL的max_connections只有1000,而你有5个应用实例,每个实例500个连接,加起来2500个,直接就把MySQL压垮了。连接池上限要根据MySQL的承载能力和应用实例数量一起计算,我一般建议:MySQLmax_connections除以应用实例数,再留一个30%的余量,就是单实例连接池上限的合理值。比如MySQL设3000,有4个应用实例,每个实例的连接池上限建议在500左右(500×4=2000,余量1000给其他管理工具和突发流量)。
4. 常见根因深度分析与解决方案
4.1 连接泄漏:最隐蔽的元凶
连接泄漏是连接池爆满问题里最让人头疼的一类,因为它的特征和慢SQL完全不同,排查思路也要换一套。连接泄漏的表现是:应用日志里没有明显的慢SQL报错,MySQL端的Threads_connected缓慢但持续上升,直到某一天突然打满。processlist里能看到大量Sleep状态的连接,Time都是几百上千秒。
连接泄漏的常见产生原因有几个。最典型的是代码里获取连接后,在异常分支中忘记释放。比如这样一段代码:
public void doSomething() { Connection conn = null; try { conn = dataSource.getConnection(); // 执行SQL } catch (Exception e) { // 处理业务异常,但conn没有close log.error("error", e); } finally { // 开发写了finally,但注释掉了close // if (conn != null) conn.close(); } }一旦某个请求走上异常分支,连接就不会归还给连接池。虽然Java 7引入了try-with-resources语法能自动释放连接,但很多老项目还在用传统写法,一个不起眼的疏忽就成了定时炸弹。
另一个常见泄漏场景是连接池和事务框架混用时,事务管理器的连接释放逻辑被绕过。Spring的@Transactional方法在正常返回时会把连接归还连接池,但如果方法内部捕获了异常且没有重新抛出,事务管理器感知不到异常,也就不会触发回滚和释放。这种问题光看连接池日志很难发现,得结合APM工具的调用链和事务追踪才能定位到具体方法。
排查连接泄漏的标准办法是开启连接池的泄漏检测。HikariCP里对应两个参数:leakDetectionThreshold默认是0(不开启),设成60000(毫秒),表示连接被持有超过60秒就触发泄漏日志。Druid连接池更直接,有个removeAbandoned参数,设为true后会自动回收超过removeAbandonedTimeout(默认300秒)未归还的连接,并打印当时的调用栈到日志里。开了这个参数之后,通常一两周内就能从日志里揪出泄漏点。
4.2 长事务与锁等待:拖垮系统的连锁反应
长事务的问题在于它不只是占着一个连接,还会把其他连接的活也堵住。一个事务的流程是:BEGIN开启事务,执行若干SQL,COMMIT或ROLLBACK结束事务。事务期间持有的行锁不会释放,其他事务想改同一行就得等。等到什么时候?等到持有锁的事务提交或回滚。如果事务里夹杂了外部接口调用、消息发送、复杂的计算任务,事务的持续时间可能从几毫秒拉长到十几秒甚至几十秒,期间所有需要操作同一行数据的连接全部排队。
我处理过一个典型场景:一个转账接口,事务里除了更新账户余额,还调用了短信服务商接口,单次调用平均耗时800毫秒。这个接口平时问题不大,因为调用量不高。但某次短信服务商升级导致接口超时,默认超时时间还是30秒,所有转账请求都在事务里干等着短信返回,连接池瞬间被打满。事后复盘时做了两处修改:一是把短信调用移出事务,事务里只保留必要的数据库操作;二是给外部调用设置更合理的超时时间,快速失败总比长时间占用资源强。
排查长事务用information_schema.innodb_trx就能看到实时未提交的事务列表,关键是看trx_started字段,确认事务已经开了多久。关联trx_mysql_thread_id和processlist.id,就能定位到具体连接正在执行什么SQL。配套的还可以看performance_schema.events_statements_history,查事务最新执行过的语句。
解决长事务的方向是"缩小事务边界"。事务里只放必须原子化的写操作,读操作放到事务外,外部调用绝对不进事务。如果业务上确实需要保证一致性,可以考虑异步化或者分阶段提交,而不是让事务去等待外部系统返回。
4.3 SQL查询性能劣化:指数级放大的压力
慢SQL对连接池的影响不是线性的,而是指数级的。假设单个SQL原本执行20毫秒,连接池200,每秒理论吞吐量是10000。如果这个SQL某天退化到2秒,同样200个连接,每秒吞吐量变成100,直接掉两个数量级。业务流量没有变化,但系统已经扛不住了。
SQL性能劣化的原因五花八门:数据量增长导致执行计划变化、统计信息过时导致优化器选错索引、索引失效(比如隐式类型转换)、深度分页导致扫描代价剧增。有一种情况特别值得注意:MySQL优化器会根据information_schema里的统计信息决定是否走索引,如果表数据被大范围更新过而统计信息没及时更新,优化器可能选了一个很糟糕的执行计划。
解决办法除了常规的加索引、改写SQL、清理数据外,建议重点检查执行计划是否稳定。MySQL 8.0支持EXPLAIN ANALYZE可以直接看到每个执行步骤的实际耗时和扫描行数,比单纯的EXPLAIN更直观。比如我之前排查过一个ORDER BY create_time DESC LIMIT 100的查询,EXPLAIN显示走了filesort,用EXPLAIN ANALYZE一看,排序这一步消耗了8秒,改造成按索引排序后,8秒变0.1秒。
4.4 连接数参数配置不合理与容量规划
这个原因最基础,但翻车概率不低。很多项目的连接池参数是"上线时拍脑袋定的",没有经过压测和容量评估。minimumIdle设太大导致空闲连接过多,白白消耗MySQL资源;maximumPoolSize设太小导致高峰期连接不够用;connectionTimeout设太长导致业务线程长时间阻塞等待,拖垮整个请求线程池。
还有MySQL端的max_connections设置过小,或者wait_timeout和interactive_timeout设置不合理的问题。wait_timeout默认8小时,意思是空闲连接超过8小时才会被MySQL服务端断开,这个值太大了。连接池本身有idleTimeout在管理空闲连接,但MySQL服务端的超时参数最好也配合调小,比如设置成180秒。这样做的好处是,就算连接池出现异常没能及时回收空闲连接,MySQL也能主动断开,不至于把连接数顶爆。
合理参数需要结合压测数据来定。简单估算公式:连接池上限 = (核心线程数 × (1 + 等待因子)),更实用的是直接模拟线上流量做压测。压测时把回放模式打开,观察Threads_running的峰值和99%响应时间,找到吞吐量拐点,再反推连接池参数。一般来说,Threads_running日常均值在两位数以下说明连接池配置合理,如果动不动上百,就要检查是不是SQL本身太慢。
5. 工具选型配置与实战命令速查
5.1 连接池选型与核心参数详解
当前Java领域主流的连接池就是HikariCP和Druid两个。HikariCP以极致的性能著称,Spring Boot 2.x以后默认集成,字节码级别的优化让它在高并发场景下的表现非常出色。Druid的优势在于监控能力,自带Web监控页面,能看到SQL执行统计、连接池状态、慢SQL日志,这些能力对排查连接池问题帮助极大。我个人的建议:新项目直接用HikariCP,追求省心;老项目如果想增强可观测性,可以考虑换Druid或者给HikariCP配独立的监控指标采集。
HikariCP的核心参数配置示例:
spring: datasource: hikari: # 池中最小空闲连接数 minimum-idle: 10 # 池中最大连接数 maximum-pool-size: 200 # 连接最大存活时间,建议小于MySQL wait_timeout max-lifetime: 1800000 # 连接空闲超时,不活跃连接自动回收 idle-timeout: 600000 # 获取连接超时时间(毫秒),过短容易误报,过长拖垮线程 connection-timeout: 30000 # 泄漏检测阈值(毫秒),设为0表示不检测 leak-detection-threshold: 60000这套配置有几个值得展开的逻辑:max-lifetime设30分钟,是因为MySQL默认wait_timeout是8小时,连接池必须在MySQL主动断开之前把连接回收掉,免得拿到一个server端已关闭的"死连接"。leak-detection-threshold设60秒,覆盖绝大多数正常SQL的执行时长,超过这个时间的连接就可能在泄漏。当然,如果系统里确实有合法的大查询超过60秒,这个阈值要放宽。
Druid连接池的话,开启监视和泄漏回收的关键配置如下:
spring: datasource: druid: initial-size: 10 max-active: 200 min-idle: 10 max-wait: 30000 remove-abandoned: true remove-abandoned-timeout: 300 log-abandoned: trueremove-abandoned和log-abandoned这两项特别重要,前者在连接被持有超过300秒后强制回收,后者会把当时完整的调用栈打印到日志。开启后只要能复现问题,基本一击必中,直接把泄漏代码所在行号都找出来。但要注意,这个强回收机制如果有合法的长事务(比如凌晨批处理),可能被误伤,所以建议配合白名单或者只在排查期开启。
5.2 监控告警落地与实战命令速查
连接池问题的发现时机越早,损失越小。建议至少三套监控同时开:应用层的连接池状态监控,数据库层的连接数和线程状态监控,以及慢SQL日志监控。应用层如果用的Spring Boot Actuator,直接暴露/actuator/metrics/hikaricp:active-connections、hikaricp:idle-connections、hikaricp:waiting这些指标,接到Prometheus+Grafana里就能看到连接池水位曲线。数据库层重点盯Threads_connected、Threads_running、Slow_queries三个指标,任何一个突然飙升都是告警信号。
我自己在用的告警规则:HikariCP活跃连接数连续5分钟超过最大池的80%,直接P1告警;Threads_running大于50持续1分钟,通知DBA介入;出现Connection is not available错误,立刻触发紧急告警。这套规则上线后,一次线上故障都没再造成过长时间的业务影响,基本都在刚冒头时就被摁住了。
日常和排查期的MySQL命令也整理一份:
-- 查看当前连接数与运行中SQL数 SHOW GLOBAL STATUS LIKE 'Threads_connected'; SHOW GLOBAL STATUS LIKE 'Threads_running'; -- 查看连接明细(重点关注Time大的连接) SELECT id, user, host, db, command, time, state, LEFT(info, 150) AS info FROM information_schema.processlist WHERE time > 10 ORDER BY time DESC; -- 按应用IP聚合连接数 SELECT host, COUNT(*) AS cnt FROM information_schema.processlist GROUP BY host ORDER BY cnt DESC; -- 查看InnoDB当前未提交事务 SELECT trx_id, trx_state, trx_started, trx_mysql_thread_id, trx_query FROM information_schema.innodb_trx ORDER BY trx_started ASC; -- 查看是否有锁等待 SELECT * FROM sys.innodb_lock_waits; -- 查看全局状态中的慢查询数量 SHOW GLOBAL STATUS LIKE 'Slow_queries'; -- 查看当前实际生效的最大连接数参数 SHOW VARIABLES LIKE 'max_connections'; SHOW VARIABLES LIKE 'wait_timeout';这些命令的执行频率也有讲究:Threads_connected和Threads_running建议做成每10秒自动采集一次;processlist快照平时少查,出问题时赶紧抓一份,抓完立刻存下来,后面复盘要用。我习惯把排查期的processlist快照存成CSV文件,标注好时间点,方便后续做趋势对比。
5.3 压测验证与容量规划方法
参数调整之后不能光靠"感觉"来判断效果,最好的验证方式是压测。用JMeter或者wrk模拟线上流量,逐渐加压,观察三个关键指标:连接池活跃连接数的峰值、Threads_running的峰值、以及99%响应时间。如果连接池活跃连接数轻松打满但CPU和数据库负载不高,说明连接池上限设小了;如果连接池没打满但响应时间已经恶化,说明瓶颈不在连接池,而是SQL本身。
压测数据要换算成容量规划的依据。假设压测时发现:200个连接,QPS 3000,Threads_running均值15,99%响应时间80ms,这就说明当前数据库能在这个连接池配置下支撑3000 QPS。如果业务预期半年后QPS会到5000,那连接池加到250到300个连接就可以,而不是盲目翻倍。多出来的每一个连接都是MySQL的资源开销,能少则少。
高峰期压测还有个技巧:在接近上限的压力下观察连接池拒绝等待的曲线。如果connectionTimeout频繁触发,说明连接池太小或SQL有拖尾;如果连接池一直有富余连接但响应时间还是很高,那就要去查数据库端的锁竞争或者磁盘IO了。连接池只需要那么多连接,数据库的并行能力才是真正的瓶颈。
6. 故障复盘与长期治理机制
6.1 建规立制:从一次故障到一套流程
单次故障解决了不代表问题彻底结束,我的习惯是每次都要拉一个复盘,把所有细节沉淀下来。复盘文档的内容包括:故障发生的时间线和告警记录、排查过程(每一步操作和依据)、根因分析、临时止血措施、长期修复方案、以及预防同类问题的改进清单。
这个改进清单里通常包含几个方向。代码层面:建立连接池获取与归还的规范,强调try-with-resources的正确用法,禁止在事务里做外部RPC调用,禁止捕获异常后不抛出导致事务状态异常;SQL层面:建立慢SQL评审机制,任何新SQL上线前必须走EXPLAIN,超过阈值的必须优化;容量层面:半年度做一次压测和数据量增长评估,及时调整连接池和MySQL参数。
针对连接池问题,我还额外加了一条:每季度做一次连接池泄漏检测开关的巡检,确认leakDetectionThreshold是开启的,日志里没有周期性的泄漏告警。很多人的这个配置项设完就忘了,等到真出问题时才发现配置被注释掉了。
6.2 长期监控体系与团队协作机制
长期治理离不开监控体系的支撑。三层监控架构优先级明确:第一层是快速感知,连接池活跃数、线程等待数、MySQL连接数、慢查询数,这些基础指标必须全量覆盖,告警阈值要精确到能区分"波动"和"故障";第二层是快速定位,APM调用链、processlist快照、事务列表要能随时拉取,出问题时10分钟内能展示出连接在哪个服务、哪段SQL、等了多久;第三层是根因追溯,慢查询日志、错误日志、连接池的泄漏日志要集中存储,支持按时间点回溯原文。
团队协作上,连接池爆满不是DBA一个人的事。应用开发负责排查代码逻辑和连接池配置,DBA负责排查数据库状态和参数优化,运维负责系统资源排查和网络链路确认。建议故障演练里加上连接池爆满这个场景,让相关角色事先跑一遍流程,不然真出故障时容易手忙脚乱,该看监控的去看日志,该查代码的去重启数据库,一团乱。
另外一个小技巧,处理这类问题时的所有操作命令和输出,我习惯保存到一个工作笔记里。线上问题处理完,这份笔记就是复盘的第一手材料。好多时候当时觉得"显而易见的判断",过两周回头看,会发现当初差点走错方向,正是因为某个关键信息当时没注意到。有了完整记录,后来人处理类似问题能少走很多弯路。
7. 实战心得总结
连接池爆满这类问题,归根结底是"资源分配与释放"的失衡。要么分配不够,要么释放不掉,要么使用过程太慢导致周转失灵。排查的思路万变不离其宗:先看连接数的高低和状态分布,再顺着异常连接定位到具体的SQL或事务,最后回到代码和参数层面找到真正的病根。
我个人在实际操作中另一个体会是,连接池爆满的故障现场往往很嘈杂,告警一大堆,各种声音都在喊"快处理"。越是这样越要冷静,先花两分钟看一下整体趋势,判断问题是脉冲式的还是持续性的,再决定是从容排查还是紧急止血。大部分连接池问题不是真的"突然挂了",而是日志和监控早就给了信号,只是没人在意那些缓慢上升的曲线。
最后再分享一个小技巧:处理完连接池问题后,记得检查一下连接池的minimumIdle和maximumPoolSize差值。如果两者太接近,连接的弹性伸缩能力就消失了,低峰期也占着一堆无用连接;差值太大又会导致高峰期创建连接的延迟。一个合理的区间是minimumIdle设为maximumPoolSize的20%到30%,既能保证突发流量的资源供给,又不会白占太多空闲连接。这个经验是我在一次低峰期MySQL内存飙升的排查中总结出来的,当时也是连接池参数的问题,不过那是另一个故事了。