周一下午,正在写新功能,运营群里突然炸了锅:"后台打不开了!""页面一直转圈!""我的订单没反应!"

我赶紧打开监控面板,一眼就看到了异常:数据库连接池的使用率100%,所有请求都在排队等待连接。

这是一次典型的连接池耗尽事故。从爆发到恢复,花了将近一个小时。记录一下整个过程,希望以后不再踩同样的坑。

事故现象

刚开始只是后台某些页面变慢,大家以为又是网络波动。但不到五分钟,前台也开始卡了,用户反馈页面加载不出来,下不了单。

看监控数据:

  • 数据库连接数:飙到上限,100%占用
  • 活跃连接:大部分被阻塞,在等待获取连接
  • 应用日志:大量"连接池已满,等待超时"的错误
  • 数据库本身:CPU正常,慢查询不多

也就是说,数据库本身没问题,是应用层把连接都占住了,新的请求拿不到连接。

排查过程

第一步,先看是哪些请求占用了连接。

用数据库的processlist查了一下,发现大量的连接都卡在一条SQL上:

SELECT * FROM orders WHERE user_id = ? ORDER BY id DESC LIMIT 10

这条SQL按理说很简单,有索引,不该慢。但它在processlist里占了将近一半的连接,全部处于"Waiting for table metadata lock"状态。

元数据锁(metadata lock)!问题找到了方向。

元数据锁是什么?简单说,MySQL在执行DDL(比如ALTER TABLE)时,会获取表级元数据锁。在这个锁释放之前,所有访问这张表的查询都会被阻塞。

而我的连接池里的连接,一旦有请求被阻塞,连接就被占着不放。阻塞的请求越来越多,连接池很快就被占满了,其他正常的请求也拿不到连接。

根因:一条没跑完的DDL

查了一圈,终于找到了罪魁祸首:上午有个同事(对,就是运营要加个字段)在orders表上执行了一个ALTER TABLE,加一个字段。

按理说,加字段很快,几秒钟就完事。但问题是,当时正好有一个长事务开着,一直没有提交。ALTER TABLE在等这个事务结束,获取元数据锁。

而那个长事务,因为某个业务逻辑卡住了,一直没提交。DDL等锁,查询被DDL阻塞,连接池被占满,雪崩就来了。

紧急恢复

定位到根因后,处理就比较直接了:

第一步:杀掉阻塞DDL的长事务

找到那个长事务的连接ID,KILL掉。DDL马上拿到了元数据锁,执行完成,释放锁。被阻塞的查询立刻恢复。

第二步:重启应用(连接池重建)

虽然锁释放了,但连接池里还有大量积压的请求和异常状态。重启应用,让连接池重新初始化,清掉所有卡住的连接。

第三步:观察恢复情况

重启后,连接池使用率从100%逐步降下来,页面恢复响应,订单可以正常提交。整个恢复过程大概15分钟。

为什么会出现这种情况

事后复盘,这次事故有三个层面的问题:

1. 长事务

那条业务逻辑为什么会卡住?查了代码,发现是一个老功能,里面有个循环查库的操作,数据量大了以后非常慢,事务开着一直不提交。这是直接的诱因。

2. 无锁迁移(大表DDL)

在MySQL 8.0里,ALTER TABLE默认会用INSTANT算法或者INPLACE算法,但依然需要短暂的元数据锁。如果表很大或者有长事务,这个锁等待就会很久。

对大表加字段,应该用gh-ost或pt-online-schema-change这类在线变更工具,或者在低峰期执行。

3. 连接池没有熔断

连接池满了以后,新的请求应该快速失败(比如返回503),而不是无限等待。我们没有配置"获取连接超时",导致所有请求都堆在连接池上,越积越多。

改进措施

针对这三个问题,我们做了以下改进:

1. 长事务监控

新增了一个监控脚本,每分钟检查一次数据库,发现超过60秒未提交的事务就告警。同时写了一个自动清理脚本,对超过10分钟的事务自动KILL(只针对特定的业务账号)。

2. DDL规范化

制定了一个规则:所有生产环境的大表DDL,必须:

  • 先在测试环境跑一遍,评估耗时
  • 用在线DDL工具(gh-ost)执行
  • 安排在凌晨低峰期执行
  • 执行前检查是否有长事务

3. 连接池配置优化

调整了连接池参数:

  • 设置获取连接超时时间(比如3秒)
  • 设置最大等待队列长度,超出的请求直接拒绝
  • 增加连接池健康检查,自动剔除坏连接

4. 快速降级

加了一个简单的熔断机制:当连接池使用率超过80%时,非核心接口直接返回缓存数据或降级响应,优先保障下单、支付等核心流程。

经验总结

这次事故给我几个深刻的教训:

1. 数据库操作要敬畏

一条简单的ALTER TABLE,在特定条件下能拖垮整个系统。对大表的任何结构变更,都要当成一次"手术"来对待,评估风险、做好准备、安排时间。

2. 连接池不是无限的

连接池是有限资源,一定要设置超时和熔断。宁可让一部分请求快速失败,也不要让所有请求堆在一起雪崩。

3. 长事务是定时炸弹

长事务的危害平时看不出来,但一旦遇到DDL,就会引发连锁反应。一定要有长事务的监控和告警机制。

4. 监控告警要及时

如果告警能更早触发,我们能在5分钟内定位问题,而不是等到全站卡死才发现。连接池使用率、活跃连接数、阻塞查询数,这些指标都应该有告警。

写在最后

恢复之后,我在团队群里发了一条消息,说明了事故原因和处理过程。然后整理了一份《生产环境数据库操作规范》,发给了所有人。

晚上下班的时候,看到那个引发问题的同事还坐在工位上。我走过去说:"没事,复盘完了,下次注意就行。"他松了口气,说:"下次做任何数据库操作,我一定先报备。"

线上事故不可怕,可怕的是同样的坑踩两次。把每一次事故都变成一次学习,系统就会越来越健壮。

毕竟,程序员的成长,就是在一次次的故障中,学会敬畏。