把连接池从 4 调到 128,吞吐掉了 60%
一个常见的操作:接口慢了,先把数据库连接池调大。
模拟一下。后端数据库有 4 个有效并行处理单元,200 个客户端不停发请求:
erlang
池大小 吞吐 (req/s) 平均延迟 P99 延迟 吞吐相对最佳
-------------------------------------------------------------
1 100 1994.5 ms 2331.2 ms 24.9%
2 200 998.7 ms 1168.9 ms 49.7%
4 403 495.9ms 582.9 ms 100.0% ← 最佳
6 391 511.0ms 613.0 ms 97.1%
8 382 522.7ms 628.1 ms 94.9%
16 348 573.7ms 758.6 ms 86.4%
32 299 667.5ms 1058.2 ms 74.3%
64 233 856.2ms 1845.1 ms 57.9%
128 160 1244.3ms 4121.4 ms 39.8%
吞吐在池大小 = 4 时达到顶点,之后一路下跌。
池子从 4 调到 128:
yaml
吞吐 403 → 160 掉了 60%
P99 583 → 4121 涨了 7.1 倍
你把池子调大 32 倍,换来的是吞吐腰斩和 P99 涨 7 倍。
为什么会这样
因为连接池不创造处理能力,它只决定排队排在哪里。
数据库的真实并行能力是固定的------由 CPU 核数、磁盘 IOPS、锁的粒度决定。给它 128 个并发查询,它不会变快 4 倍,它只会:
- 时间片轮转:每个查询跑一会儿就被切走,总时长被拉长
- 上下文切换:128 个进程/线程来回换,CPU 缓存反复失效
- 锁竞争加剧:buffer pool 的 mutex、WAL 的写锁、行锁的等待队列全都变长
- 内存压力:每个连接都有自己的排序缓冲、临时表空间
池子小的时候,多余的请求排在池外;池子大的时候,它们排在数据库里。
队列从一个你能看见、能控制、能快速失败的地方,挪到了一个你看不见的地方。
这正是前几天那篇排队论文章的另一面:利用率接近 1 时延迟会爆炸------而把连接池调大,恰恰是在强行把数据库推到那个区间。
Little's Law 验算
markdown
池大小 吞吐/s 平均延迟(s) 吞吐 × 延迟 数据库内并发
----------------------------------------------------------
1 100 1.9945 199.6 1.0
4 403 0.4959 199.7 4.0
16 348 0.5737 199.7 16.0
24 322 0.6201 199.6 24.0
「吞吐 × 延迟」这一列恒等于 200 ------ 正好是客户端数量。
这是 Little's Law(在途数 = 到达率 × 逗留时间)的直接体现:200 个客户端各自同步等待,系统里永远有 200 个请求在途,这个数和池子大小无关。
池子大小唯一决定的,是这 200 个请求里有多少挤在数据库内部(最后一列)。
池 = 4 → 4 个在库里受苦,196 个在池外安静排队
池 = 128 → 128 个在库里互相踩踏
吞吐是 200 / 延迟。既然在途数固定,想提高吞吐唯一的办法就是降低延迟------而把并发推高恰恰是在抬高延迟。
最佳池大小是多少
markdown
后端并行单元数 最佳池大小 此时吞吐 经验公式 2N+1
----------------------------------------------------------
2 2 199 5
4 4 399 9
8 8 800 17
16 16 1594 33
32 32 3179 65
在我的模型里,最佳值精确等于后端的并行单元数。
但真实数据库比这复杂------查询不是纯 CPU 的,它有磁盘等待。一个连接在等 IO 时,CPU 是空的,这时候多一个连接确实能填上。所以实践中的经验公式会多留一些:
scss
连接数 = (核心数 × 2) + 有效磁盘数
这是 PostgreSQL 社区的建议,HikariCP 的文档也在引用它。注意它的量级:一台 8 核 + SSD 的数据库,推荐值是 17 左右,不是 200。
HikariCP 的 wiki 里有个被反复引用的案例:某个系统 9000 个线程、2000 个连接的池,压力测试响应时间约 100 ms;把池子降到 96 之后,响应时间降到 2 ms ------ 吞吐还涨了。
为什么大家总想调大
因为症状会骗人。
接口超时 → 看日志发现 "connection pool timeout" → 结论:池子不够用 → 调大 → 短期内错误消失(因为不再有人拿不到连接了)→ 但延迟变得更糟,只是从「快速失败」变成了「大家一起慢」。
"获取连接超时" 不是池子太小的证据,它是下游已经处理不过来的证据。
调大池子相当于把煤气报警器拆掉------报警声是没了。
实践清单
1. 池子大小从「后端核数 × 2 + 磁盘数」起步,然后压测验证。几十就够了,不要上百。
2. 压测要看吞吐曲线的拐点,不是看错误率。 错误率随池子增大单调下降,但吞吐是先升后降的------拐点才是你要的值。
3. 设置合理的 connectionTimeout,并让它快速失败。 拿不到连接就立刻返回 503,比排队 30 秒后超时好得多(排队会把延迟传导给上游,引发连锁)。
4. 应用实例数 × 每实例池大小 = 数据库看到的总连接数。 20 个实例 × 池 50 = 1000 个连接,数据库会被打爆。扩容应用实例时,池大小要同步调小。
5. 分离连接池。 在线查询、后台任务、报表走不同的池,防止一个慢任务把所有连接占满。
6. 真的不够用了,解决方案是让查询变快(索引、缓存、减少往返),不是加连接。
最后
连接池是一个限流器,而不是一个加速器。
它的真正作用是保护数据库------通过限制并发,让数据库工作在它效率最高的区间。
而「调大连接池」这个动作,等于主动关掉了这个保护。
你没有让系统变快,你只是把队列搬到了一个报警器照不到的地方。