适用场景:
- 学校 / 政务系统
- 应用部署在内网服务器
- Oracle 数据库部署在云上,通过跨网络访问
- 低并发(寒假、夜间)时偶发接口卡死、16 分钟无响应
一、问题现象(我们遇到的真实情况)
- 应用服务进程正常,没有崩溃
- 访问 Oracle 的接口长时间无响应(10~20 分钟)
- 最终连接池耗尽,系统“假死”
- 同一系统访问 内网 DM 数据库完全正常
- 重启应用后短时间内恢复
- 问题多发生在 寒假 / 夜间低流量时段
二、核心结论(一句话定性)
这是“内网系统跨网络访问云 Oracle,在低流量、长空闲场景下产生 TCP 半开 / 黑洞连接,导致 JDBC 读操作无限等待”的典型工程问题。
- 不是业务代码 Bug
- 不是 Oracle 性能问题
- 不是并发太高
- 是 网络拓扑 + 长连接机制 + 缺少读超时 共同导致
三、为什么只有 Oracle 出问题,DM 没问题
1. 网络路径不同
-
DM:
- 应用服务器 → 内网 → 内网 DM
- 无跨网、无 NAT、无云边界
-
Oracle:
- 应用服务器 → 政务出口 → 防火墙 / NAT → 云网络 → 云 Oracle
2. 结论
- 内网数据库:连接要么活,要么立刻断
- 跨网数据库:可能出现“看起来还在,实际上已不可用”的连接
四、问题的本质:什么是“僵尸连接”
1. 连接的真实状态
- Java / 连接池认为:连接还活着
- 操作系统认为:TCP 仍是 ESTABLISHED
- 中间网络设备:已不再可靠转发业务数据
- Oracle 可能根本收不到请求
这种状态称为:
TCP 半开 / 黑洞连接(Zombie Connection)
2. 最危险的点
- JDBC 默认
read()是 无限等待 - 线程被卡住
- 连接无法归还连接池
- 多个请求后导致整个系统假死
五、为什么低流量(寒假)更容易出问题
- 连接长期空闲(idle)
- 防火墙 / NAT 会对长时间无流量的 TCP 会话进行“静默老化”
- 不发送 FIN / RST
- 下一次使用该连接时才暴露问题
人少不是原因,长时间不用才是触发条件
六、已经采取过的方案:TCP keepalive(有效但不充分)
1. 内核参数配置
net.ipv4.tcp_keepalive_time = 300
net.ipv4.tcp_keepalive_intvl = 60
net.ipv4.tcp_keepalive_probes = 5
2. 作用
- 约 10 分钟内清理“真正已经断掉的 TCP 连接”
- 对网络断连、对端消失等问题有效
3. 局限性
- 只能检测 TCP 层是否活着
- 无法发现“TCP 活着,但 Oracle 应用层不返回数据”的情况
七、为什么必须设置 JDBC 读超时(关键结论)
1. JDBC ReadTimeout 的含义
限制一次数据库读操作最多等待多久
- 不是 SQL 总执行时间
- 而是:多久内必须收到 Oracle 返回的任何数据
2. 不设置的后果
- 网络黑洞 / Oracle 无响应时
- 线程会无限等待(可达 10~20 分钟)
- 连接池逐步被拖死
3. 工程铁律
任何跨网络访问的数据库连接,必须设置读超时,否则一定会在某个时间点出事故。
八、最终采用的解决方案(推荐方案)
1. JDBC URL 设置读超时(必须)
jdbc:oracle:thin:@//host:1521/orcl?oracle.jdbc.ReadTimeout=30000
含义:
- 30 秒内无任何返回 → 本次请求失败
- 连接被回收并重建
- 系统不会被拖死
2. 调整连接池参数(低流量友好)
hikari:
max-pool-size: 20
min-idle: 2
max-lifetime: 840000 # 14 分钟
原则:
- 少养连接
- 快速淘汰老连接
- 避免僵尸连接长期存在
3. TCP keepalive 作为底层补充(保留)
- 清理真正的死连接
- 作为网络层兜底
- 不能替代 JDBC ReadTimeout
九、30 秒失败意味着什么(重要认知)
-
只会在以下场景发生:
- 网络异常
- Oracle 无响应
- SQL 被锁死
-
后果:
- 当前请求失败
- 连接释放
- 系统整体仍可用
允许请求失败,不允许系统假死
十、最终总结
本问题的根因是:
内网系统跨网络访问云 Oracle,在低流量场景下产生僵尸连接,JDBC 默认无限读等待导致线程和连接池被拖死。
通过 TCP keepalive + JDBC ReadTimeout + 连接池参数治理,可有效规避该类问题。