结论先行:连接池大小调成 CPU 核数的 10 倍、甚至 50 倍,往往不是变快,而是把抖动放大。下面是一次真实的 P99 抖动排查记录。
【现象】 一个订单查询接口,QPS 稳定在 800 左右,P50 一直 12ms,但 P99 间歇性飙到 2.4s,每天下午 3 点前后高发。CPU、GC、慢日志都看过,没找到明显异常。
【排查路径】
- 先看 HikariCP 的 pending/active 指标。发现 P99 飙高时段,activeConnections 长期贴顶在 200(配的就是 200),且 wait 队列里有线程在等连接,平均等待 1.8s。
- 结论:瓶颈不在数据库本身,而在「等连接」。数据库侧 active 会话其实只有 60 左右,大量连接被池子占着但没真正在跑 SQL——这是典型的「连接被持有时间过长」。
- 定位持有源头:一段事务里先查了 3 张表,中间穿插了一次远程调用(约 800ms)。连接在整个事务期间一直被占着,远程调用期间连接其实是空占的。
【改法(按收益排序)】
- 把远程调用挪出事务。事务只包 DB 写操作,远程调用前置或后置。这一条做完,连接平均持有时间从 1.1s 降到 300ms,P99 直接回到 15ms 量级。
- 把连接池从 200 降到 60。公式参考:connections ≈ (DB 核心数 × 2) + 磁盘数。我们 8 核 SSD,算下来 17 个理论值,留余量取 60。降到 60 后吞吐没掉,抖动反而更稳——因为并发不再把数据库的连接/内存/锁资源打满。
- 加一个「获取连接超时」从默认 30s 降到 2s,配合熔断快速失败,避免线程被无限挂住。
【几个反直觉的点】
- 池子越大,单次查询越快,但系统整体 P99 越差。因为大池子会放大数据库侧的锁争用与内存压力,形成正反馈抖动。
- 「连接空闲但没归还」比「没连接可用」更隐蔽。检查点:有没有把连接持有在事务外、有没有在循环里反复 getConnection 却不 close。
- 压测 QPS 达标 ≠ 生产可用。生产有长尾事务、有网络抖动,必须看 P99/P999 和 wait 队列深度。
【一句话】 连接池大小不是性能参数,是「上游并发能压到下游多少」的阀门。阀门调大,下游先崩。先降持有时间,再谈池子大小。
【附:我们最终的参数】 maximumPoolSize=60、minimumIdle=10、connectionTimeout=2000ms、maxLifetime=1800000ms、leakDetectionThreshold=30000ms(开了泄漏检测,上线后抓到过 2 处未归还)。