26分钟就断连?一次 MySQL “幽灵掉线” 的深度破局之路

44 阅读4分钟

问题现象:系统在空闲一段时间后,首次访问数据库时报错 Communications link failure,错误日志显示“Last packet received was 1,594,079 milliseconds ago(约26分钟)”。即使配置了连接池验证,问题依然反复出现。

本文将完整复盘该问题的排查思路、踩过的坑、最终解决方案,并详解 Druid 连接池关键参数配置,帮助大家避免重蹈覆辙。


 一、问题初现:不只是“超时”那么简单

最初,我们看到典型的 MySQL 通信异常:

Caused by: com.mysql.cj.exceptions.CJCommunicationsException: Communications link failure
The last packet successfully received from the server was 1,594,079 milliseconds ago.

直觉告诉我们:数据库连接被断开了
于是我们检查了 MySQL 的 wait_timeout 和 interactive_timeout,发现均为 7200 秒(2小时)——远大于 26 分钟。

结论:不是 MySQL 主动断连,而是中间网络设备(如云厂商 SLB、NAT 网关、防火墙)在空闲约 15–30 分钟后切断了 TCP 连接


二、第一次尝试:配置连接池验证

我们立即为 Druid 连接池添加了基础防护:

test-on-borrow: true
validation-query: SELECT 1 FROM DUAL
min-evictable-idle-time-millis: 300000  # 5分钟
test-while-idle: true

预期效果:每次借出连接前先验证,确保连接有效。

结果:问题依旧!更诡异的是,日志中开始频繁出现:

ERROR druid.sql.Statement - execute error. SELECT 1 FROM DUAL

这说明:Druid 确实在验证连接,但验证本身失败了——连接已被网络中断。


三、深入排查:为什么验证失败还会报业务错误?

通过分析 Druid 源码和日志,我们发现:

  • • 虽然 test-on-borrow=true 能检测到坏连接,
  • • 但默认情况下,验证失败会直接抛出异常,不会自动重试!

这意味着:用户请求会直接失败,体验极差。

关键突破:启用 Druid 的 异常分类器(Exception Sorter) ,让连接池能识别“可恢复的网络异常”,并自动重试获取新连接。


四、最终解决方案:三重防护体系

1、启用 MySQL 异常分类器(核心!)

在 connection-properties 中显式指定:

connection-properties:
  druid.exceptionSorter: com.alibaba.druid.pool.vendor.MySqlExceptionSorter

✅ 作用:当 SELECT 1 验证失败时,Druid 会识别这是“网络断连”而非 SQL 错误,自动 discard 坏连接,并重试获取新连接,对业务透明。


2、合理设置空闲回收时间

根据网络设备超时(约 26 分钟),我们将空闲时间设为 10–15 分钟,留足安全余量:

min-evictable-idle-time-millis: 600000   # 10分钟
max-evictable-idle-time-millis: 900000   # 15分钟
time-between-eviction-runs-millis: 30000 # 每30秒检查一次

✅ 作用:在连接被网络中断前,主动回收,大幅降低验证失败概率。


3、关闭生产环境的 removeAbandoned(重要!)

初期我们启用了:

remove-abandoned: true
remove-abandoned-timeout-millis: 180000 # 3分钟

但 Druid 日志警告:

WARN - removeAbandoned is true, not use in production.

原因:它可能误杀真正的长事务(如耗时 5 分钟的报表导出),导致 Communications link failure

✅ 生产环境必须关闭,改用良好编码规范(如 Spring 事务、try-with-resources)防止泄漏。


五、Druid 关键参数详解(生产推荐配置)

参数推荐值说明
test-on-borrowtrue借出时验证,确保连接有效(牺牲微秒级性能,换稳定性)
validation-querySELECT 1轻量验证 SQL,比 SELECT 1 FROM DUAL 更标准
min-evictable-idle-time-millis600000 (10分钟)必须  < 网络设备超时(如阿里云 SLB 默认 15 分钟)
exceptionSorterMySqlExceptionSorter自动识别网络异常,触发重试(解决验证失败仍报错的关键!)
remove-abandonedfalse生产环境禁用,避免误杀长事务
keep-alivetrue启用 Druid 内部保活机制
filtersstat,wall,slf4j开启 SQL 监控、防火墙、统一日志

完整配置示例:

dynamic:
  druid:
    initial-size: 10
    min-idle: 10
    max-active: 40
    max-wait: 30000

    time-between-eviction-runs-millis: 30000
    min-evictable-idle-time-millis: 600000
    max-evictable-idle-time-millis: 900000

    validation-query: SELECT 1
    validation-query-timeout: 3
    test-while-idle: true
    test-on-borrow: true
    test-on-returnfalse

    # 核心:自动处理网络断连
    connection-properties:
      druid.exceptionSorter: com.alibaba.druid.pool.vendor.MySqlExceptionSorter
      druid.stat.mergeSql: true
      druid.stat.slowSqlMillis: 5000

    filters: stat,wall,slf4j
    keep-alive: true
    pool-prepared-statements: true
    max-pool-prepared-statement-per-connection-size: 20

    # 生产环境关闭泄漏回收
    remove-abandoned: false
    log-abandoned: false

六、经验总结

  1. 1. 不要只看 MySQL 超时:云环境、NAT、防火墙才是“隐形杀手”;
  2. 2. test-on-borrow=true 是基础,但不够:必须配合 exceptionSorter 才能自动重试;
  3. 3. 生产环境禁用 removeAbandoned:它治标不治本,还可能引发新问题;
  4. 4. 连接池配置要“适配网络”minEvictableIdleTimeMillis 必须小于中间设备超时;
  5. 5. 长事务要拆分:避免在 @Transactional 方法中调用外部耗时接口。

七、结语

数据库连接断连问题看似简单,实则涉及 网络、连接池、驱动、业务代码 多层协作。只有理解每一层的行为,才能构建真正高可用的系统。

希望本文能帮你少走弯路。如果觉得有用,欢迎转发给团队同学,一起提升系统稳定性!

技术无小事,细节定成败