关键词: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% 是网络 / 中间设备