把连接池从 4 调到 128,吞吐掉了 60%

把连接池从 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. 真的不够用了,解决方案是让查询变快(索引、缓存、减少往返),不是加连接。

最后

连接池是一个限流器,而不是一个加速器。

它的真正作用是保护数据库------通过限制并发,让数据库工作在它效率最高的区间。

而「调大连接池」这个动作,等于主动关掉了这个保护。

你没有让系统变快,你只是把队列搬到了一个报警器照不到的地方。

相关推荐
智驭未来掌门人1 小时前
雷达架构梳理:那些容易混淆的边界与分层
后端
琳达爱分享1 小时前
四种限流算法实测对比:固定窗口、滑动窗口、漏桶、令牌桶(附 Redis+Lua 实现)
后端·架构
用户9888321871741 小时前
别把一次性任务写成常驻 Worker:从 .NET 托管服务的生命周期说起
后端
韩振方1 小时前
Kubernetes StatefulSet 让每个副本找回自己的存储
后端
yunwei371 小时前
eBPF 示例:使用 CUPTI 构建 GPU 火焰图分析器
linux·后端·性能优化
网腾无限1 小时前
多智能体编排的长事务翻车实录:重试风暴、重复扣款与补偿失效怎么治
后端
Lyy1 小时前
DevOps平台 — 第十六篇:动态的设计与实现
后端·devops
136096757231 小时前
微信校验文件不用文件
后端
福大大架构师每日一题1 小时前
2026-10-20:统计二叉树中支配节点的数量。用go语言,给定一棵完全二叉树的根节点 root,树中每个节点都有一个值。 对于一个节点 x,考察由 x 本身
后端