现象或背景
在高并发的 deadline 共享场景下,我们常常会遇到数据库连接池的性能瓶颈。例如,一个订单查询服务在凌晨时段出现严重的性能退化,187 个请求各卡了 30 秒,P99 从 12ms 飙到 30 018ms,翻了 2500 倍。
原因分析
连接池假死的根本原因是 TCP Keepalive 默认 7200s 的探测周期远慢于中间 NAT / 负载均衡器的 300s idle timeout。NAT 已经把连接表项清了,但 TCP 两端还在「以为连接是好的」,导致下一次复用时出现 30 秒的挂起。
此外,连接池的 connectionTimeout 与 idleTimeout 设置不合理,容易导致资源浪费和性能下降。
具体做法
- 准入拦截:在
getConnection()之后、executeQuery()之前进行准入检查,确保请求在合理的时间范围内执行。
long remain = deadlineMs - System.currentTimeMillis();
long admit = sqlMeta.getp99() + 5;
if (remain < admit) {
pool.closeQuietly(conn);
throw new BudgetExhaustedException("admit_reject remain=" + remain);
}
- 双条件降级:根据剩余预算和 pending 数量做出决策,避免资源浪费。
等连接超时时:
if (pending < N && remain > socketTimeout + 20ms) → 再排一次
else if (pending >= N) → 直接快速失败 + 降级缓存
else → 快速失败 + 降级缓存
- 直方图观测:拆分「等连接耗时」和「执行耗时」两个直方图,便于问题定位。
// 拿连接阶段
long t0 = now();
Connection c = pool.getConnection();
observe("db.acquire_ms", now() - t0);
// 执行阶段
long t1 = now();
try (PreparedStatement ps = c.prepareStatement(sql)) { rs = ps.executeQuery(); }
observe("db.execute_ms", now() - t1);
踩过的坑
- socketTimeout 设 2000ms 不是安全值,它必须远小于 deadline,不是接近 deadline。
- orTimeout / CompletableFuture 逻辑超时 ≠ 资源释放,线程仍占着连接直到 socketTimeout 自然结束。
- 扩池只推迟问题不解决:DB 慢时并发越大 DB 越慢,正反馈。
结论与可复用清单
- 准入拦截:避免半执行问题,提升资源利用率。
- 双条件降级:根据剩余预算和 pending 数量做出决策,避免资源浪费。
- 直方图观测:拆分「等连接耗时」和「执行耗时」,便于问题定位。
这些做法在实际生产环境中验证有效,帮助我们解决了多个复杂的性能问题。
0赞
0收藏
0评论
2浏览