一次 16 分钟 Oracle 连接“卡死”问题的完整复盘(HikariCP + Oracle)

5 阅读4分钟

关键词:HikariCP、Oracle、僵尸连接、低峰期、16 分钟超时、TCP 半死连接


一、问题现象

线上系统在低峰期偶发出现如下问题:

  • 业务线程获取数据库连接卡死
  • 无明显 CPU / 内存异常
  • 日志中最终出现 约 16 分钟后 的连接或 IO 超时异常
  • 高峰期基本不出现,低峰期频繁出现
  • 重启服务后恢复

连接池使用 HikariCP + Oracle,连接池配置看似完整:

  • minIdle
  • keepalive-time
  • validation
  • max-lifetime

但问题依旧存在。


二、最初的困惑:为什么是“16 分钟”?

16 分钟并不是 HikariCP 的任何默认值,也不是 JVM 的超时配置。

关键结论

16 分钟 ≠ 连接池超时

16 分钟 = 网络 / 中间设备对“半死 TCP 连接”的清理周期

这类问题在 政务网、专线、老防火墙、四层设备 中非常常见。


三、核心原因:TCP 还活着,Oracle JDBC 已经死了

这是整个问题的根因

1️⃣ 什么是“半死连接(Zombie Connection)”?

  • 防火墙 / 中间设备:

    • 丢弃应用层数据包
    • 但不返回 RST / FIN
  • TCP keepalive 仍能收到 ACK

  • Linux / JVM 认为连接是“活的”

  • Oracle JDBC 的 socket 读/写永久阻塞

结果:

TCP 看起来是活的
但 SQL 永远没有响应


2️⃣ 为什么只在低峰期出现?

因为:

  • 低峰期连接长期 idle
  • 更容易被中间设备当作“无效连接”
  • 高峰期连接频繁使用,不容易进入“半死状态”

四、为什么【原来的配置】挡不住?

❌ 1️⃣ keepalive-time 并不能保证“连接可用”

keepalive-time: 30000

很多人误以为:

keepalive 能保证连接是好的

这是错误的

事实是:

  • Hikari 的 keepalive:

    • 只是周期性执行一个 SQL
  • 如果:

    • JDBC driver
    • 或 socket 写操作
    • 已经进入阻塞状态

👉 keepalive 线程本身也会被卡住

它不是“抢救机制” ,只是“保活尝试”。


❌ 2️⃣ validation-timeout 也挡不住“已被借出的连接”

validation-timeout: 2000
connection-test-query: SELECT 1 FROM DUAL

验证只发生在:

  • 连接创建时
  • 或归还连接时

问题连接是:

被借出 → 执行业务 SQL → 卡死

👉 validation 根本没机会生效。


❌ 3️⃣ max-lifetime = 14 分钟 过长(这是致命点)

max-lifetime: 840000 # 14 分钟

在低峰期:

  • minIdle 连接长期空闲
  • 防火墙在 10~15 分钟内 kill 掉连接
  • Hikari 还没来得及重建
  • 业务线程刚好拿到这个连接

👉 直接命中僵尸连接


❌ 4️⃣ 没有 JDBC 层的“强制读超时”

最致命的问题是:

原配置中,没有 JDBC Socket ReadTimeout

结果是:

Socket.read() = 无限等待

这正是 16 分钟卡死 的根本原因。


五、为什么【新增配置】可以有效?

✅ 1️⃣ oracle.jdbc.ReadTimeout —— 终极兜底

data-source-properties:
  oracle.jdbc.ReadTimeout: 60000

这是真正解决问题的关键配置

  • 不管 TCP 是否还活着
  • 不管防火墙是否 ACK
  • 只要读超 60s
  • JDBC 强制抛异常
  • Hikari 立即丢弃连接并重建

👉 从“卡死 16 分钟”变成“1 分钟快速失败”


✅ 2️⃣ max-lifetime 明显小于中间件 kill 时间

max-lifetime: 480000 # 8 分钟

作用:

  • 在连接可能被中间设备 kill 之前
  • 主动销毁并重建连接
  • 避免 minIdle 连接长期处于“危险区间”

✅ 3️⃣ OS 层 TCP keepalive(你已经具备)

tcp_keepalive_time = 300
tcp_keepalive_intvl = 60
tcp_keepalive_probes = 5

作用:

  • 兜底清理完全失联的 TCP
  • 防止连接永久挂住

它不是主因,但它是底座


六、最终生效配置(核心片段)

hikari:
  max-pool-size: 20
  min-idle: 4
  connection-timeout: 30000
  idle-timeout: 300000
  max-lifetime: 480000
  keepalive-time: 30000
  validation-timeout: 2000
  connection-test-query: SELECT 1 FROM DUAL
  data-source-properties:
    oracle.jdbc.ReadTimeout: 60000
    oracle.net.CONNECT_TIMEOUT: 30000
    oracle.net.keepAlive: true

七、总结一句话(掘金结尾用)

这不是 Hikari 的 bug,也不是 Oracle 的锅

而是“TCP 半死连接 + JDBC 默认无限等待”的经典组合坑

解决它,必须:

  • JDBC 层强制 ReadTimeout
  • maxLifetime 主动避开中间件 kill 周期
  • keepalive + validation 作为辅助

否则迟早会在低峰期踩雷。


八、给后来人的建议

  • ❌ 不要迷信 keepalive
  • ❌ 不要把 maxLifetime 设得太长
  • ✅ JDBC ReadTimeout 一定要配
  • ✅ 低峰期问题,90% 是网络 / 中间设备