记一次连接池泄漏导致的线上事故排查与修复
事故背景
周一上午九点多,监控系统开始疯狂报警。我们的核心服务响应时间从平均50ms飙升到2000ms以上,错误率从0.1%上升到15%,数据库CPU使用率接近100%。更糟糕的是,这种情况呈现持续恶化的趋势。
初步排查
第一步:检查基础指标
我立即登录到监控系统查看各项指标:
- 服务QPS:与平日持平,无明显增长
- 容器指标:CPU使用率90%,内存使用率70%
- 数据库:活跃连接数达到最大限制(100),大量连接处于"Sleep"状态
- 线程池:工作线程全部处于忙碌状态,队列堆积
第二步:分析错误日志
查看错误日志发现大量以下类型的错误:
[ERROR] [2025-03-15 15:23:45] [http-nio-8080-exec-12] c.z.hikari.pool.HikariPool - Timeout after 30000ms waiting for connection.
这表明应用无法从连接池获取到数据库连接,触发了获取连接的超时。
深入分析
连接池行为分析
我们使用的是HikariCP连接池,配置如下:
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.leak-detection-threshold=60000
正常情况下,连接数应该稳定在5-10个左右,但现在达到了最大值20且持续不下。
连接泄漏检测
幸运的是我们配置了leak-detection-threshold
,查看日志发现大量连接泄漏警告:
[WARN] [2025-03-15 15:23:45] [HikariPool-1 housekeeper] c.z.hikari.pool.ProxyLeakTask - Connection leak detection triggered for connection ...
这表明有数据库连接被获取后没有被正确关闭。
代码审查
根据日志中的堆栈信息,我们定位到几个可疑的业务方法。最终发现一个复杂的业务逻辑中存在连接未关闭的情况:
public void processBatchOrders(List<Order> orders) {Connection conn = dataSource.getConnection(); // 手动获取连接try {for (Order order : orders) {// 复杂业务逻辑if (someCondition) {return; // 这里直接返回,导致连接未关闭!!!}// 更多业务逻辑}} finally {// 缺少conn.close()}
}
问题根源
根本原因是开发人员在手动管理数据库连接时:
- 在复杂业务逻辑中多处提前返回,但未确保连接关闭
- 没有使用try-with-resources语法
- 没有在finally块中正确释放资源
每次调用这个方法都会泄漏一个连接,随着请求量增加,最终耗尽了连接池的所有连接。
解决方案
短期修复
- 紧急重启服务,释放所有泄漏的连接
- 添加临时监控,跟踪连接获取/释放比例
长期修复
- 代码修复:重构代码,使用try-with-resources确保连接关闭
public void processBatchOrders(List<Order> orders) {try (Connection conn = dataSource.getConnection()) {for (Order order : orders) {// 业务逻辑if (someCondition) {return; // 现在可以安全返回}}}
}
-
最佳实践:
- 避免手动管理连接,尽量使用Spring的
@Transactional
- 如果必须手动管理,使用模板方法模式封装资源管理
- 避免手动管理连接,尽量使用Spring的
-
防御性措施:
- 降低泄漏检测阈值到30秒:
leak-detection-threshold=30000
- 添加连接池监控指标到Prometheus
- 在CI流程中添加静态分析工具检测资源泄漏
- 降低泄漏检测阈值到30秒:
-
测试增强:
- 添加集成测试验证连接不泄漏
- 使用单元测试模拟验证资源清理
经验教训
-
连接池使用原则:
- 获取和释放连接必须成对出现
- 释放操作必须放在finally块中
- 更推荐使用框架管理连接
-
监控的重要性:
- 连接池指标应该纳入核心监控
- 设置合理的告警阈值(如连接使用率>80%)
-
代码审查重点:
- 特别注意手动资源管理的代码
- 检查所有可能的代码路径是否都正确释放资源
-
技术债务的危害:
- 这个隐患其实已经存在数月
- 只有在高负载时才会显现
- 技术债务终将偿还,而且总是在最不方便的时候
后续改进
事故后我们实施了以下改进:
- 建立了连接池使用规范,禁止随意手动获取连接
- 在全系统范围扫描类似问题,修复了3处潜在泄漏点
- 增加了连接池指标的仪表盘和告警
- 在开发者培训中加入资源管理专题
这次事故让我深刻理解了连接池泄漏的危害性——它像内存泄漏一样具有累积效应,最终导致系统不可用。同时也让我认识到防御性编程和全面监控的重要性。希望这次经验分享能帮助其他开发者避免类似的陷阱。