上周五凌晨2点37分,我正睡得迷迷糊糊时被一阵急促的报警短信惊醒。监控系统显示生产环境的MySQL数据库连接数突然飙升到上限,导致新请求全部阻塞。打开电脑查看详细指标时,连接池使用率曲线像坐了火箭一样垂直上升,最终稳定在100%的位置不再回落。
这种情况在流量高峰时段偶尔会出现,但这次发生在凌晨低峰期就显得极不正常。通过SHOW PROCESSLIST命令查看,发现有大量Sleep状态的连接堆积,更奇怪的是这些连接都来自同一个应用服务节点。这让我立即意识到:这不是简单的流量激增问题,而是典型的连接泄漏。
正常的连接池使用应该呈现锯齿状波动曲线:请求到来时获取连接,完成业务后立即释放。而泄漏场景下,连接被获取后就像破洞的水桶一样不断流失,最终导致资源耗尽。通过以下特征可以快速判断泄漏类型:
sql复制-- 查看当前连接详情
SHOW FULL PROCESSLIST;
-- 统计各状态连接数
SELECT COMMAND, COUNT(*)
FROM information_schema.PROCESSLIST
GROUP BY COMMAND;
-- 检查最大连接数配置
SHOW VARIABLES LIKE 'max_connections';
-- 查看连接存活时间(单位:秒)
SELECT TIME, COUNT(*)
FROM (
SELECT TIMESTAMPDIFF(SECOND, TIME, NOW()) AS TIME
FROM information_schema.PROCESSLIST
) t
GROUP BY TIME;
通过审计日志发现,异常连接都指向订单服务的某个实例。进一步检查该节点的线程堆栈,发现大量连接被OrderQueryService的查询方法持有。以下是关键的排查步骤:
bash复制watch com.zaxxer.hikari.HikariDataSource getConnection '{params,returnObj}'
watch com.mysql.jdbc.ConnectionImpl close '{params,returnObj,throwExp}'
发现存在大量连接获取后没有触发close调用
通过BTrace追踪连接生命周期:
java复制@OnMethod(clazz="com.mysql.jdbc.ConnectionImpl", method="close")
public static void traceClose() {
println(strcat("Connection closed at: ", timeMillis()));
}
最终定位到一段使用MyBatis的代码:
java复制@Transactional
public List<Order> queryOrders(Long userId) {
SqlSession session = sqlSessionFactory.openSession();
try {
return session.selectList("queryOrders", userId);
} finally {
// 致命错误:在@Transactional方法内又创建了独立Session
session.close();
}
}
这里存在两个严重问题:
properties复制# HikariCP配置
spring.datasource.hikari.maximum-pool-size=200 → 300
spring.datasource.hikari.leak-detection-threshold=60000
bash复制# 保留现有连接处理完请求
kill -15 <pid>
sql复制SET GLOBAL wait_timeout = 300;
SET GLOBAL interactive_timeout = 300;
java复制public List<Order> queryOrders(Long userId) {
// 移除手动session管理
return orderMapper.queryOrders(userId);
}
java复制@Bean
public HikariPoolMXBean hikariPoolMXBean() {
return hikariDataSource.getHikariPoolMXBean();
}
properties复制spring.datasource.hikari.max-lifetime=1800000
spring.datasource.hikari.idle-timeout=600000
spring.datasource.hikari.connection-timeout=30000
yaml复制- alert: MySQLConnectionPoolCritical
expr: avg_over_time(mysql_global_status_threads_connected[1m]) / mysql_global_variables_max_connections > 0.8
for: 5m
labels:
severity: critical
annotations:
summary: "MySQL连接池使用率超过80%"
使用JMeter模拟以下场景:
xml复制<ThreadGroup>
<LoopController loops="1000" />
<JDBCDataSource configElement="MySQLPool"/>
<JDBCRequest queryType="Select" query="SELECT sleep(1)"/>
</ThreadGroup>
验证指标:
properties复制# 计算公式
最大连接数 = (核心数 * 2) + 有效磁盘数
# 推荐配置
spring.datasource.hikari.maximum-pool-size=10
spring.datasource.hikari.minimum-idle=5
java复制// 在启动时添加泄漏检测
HikariConfig config = new HikariConfig();
config.setLeakDetectionThreshold(30000);
这次事故让我深刻体会到,数据库连接就像酒店房间钥匙——每次使用后必须及时归还前台,否则很快就会无房可住。现在我们在CI流程中增加了连接泄漏检测环节,任何修改数据库访问的代码都必须通过以下测试:
java复制@Test
public void testConnectionLeak() throws Exception {
for (int i = 0; i < 100; i++) {
yourMethodUnderTest();
assertThat(dataSource.getHikariPoolMXBean().getActiveConnections())
.isLessThan(5);
}
}