3006 字
约 10 分钟
21
S11-A10 · 连接池容量决策:池大小与真实并发
2024-12-31
2026-11-09

把 maximumPoolSize 从 10 调到 50,接口 P99 反而更高,数据库 CPU 也上去了。这不是调参失误,而是方向错了:连接池不是"越多越快的车道",而是一组被数据库侧容量约束的许可证。许可证发多了,排队从应用侧挪到数据库侧,而后者带着锁、上下文切换和内存压力。

只讨论一件事:给定一个数据库和一个并发模型,池该多大,设错的账单长什么样。


1. 四个数字,各自对应一个真实约束

参数(HikariCP 口径) 含义 真正的约束来自 设大的后果
maximumPoolSize 同时存在的物理连接上限 数据库核数、内存、max_connections 数据库侧排队与上下文切换
minimumIdle 保活空闲连接数 峰值与均值的差距、建连耗时 长期占用数据库会话与内存
connectionTimeout 从池取连接的等待上限 请求 SLA 与线程池排队预算 等待时间吃掉整条请求的超时预算
idleTimeout / maxLifetime 空闲回收与最长存活 中间件空闲断连时间 拿到被对端关闭的死连接

maxLifetime 最常被忽略:它必须短于数据库、代理、LB 中最小的空闲断连时间,否则池里会有"看着有效、已被对端掐掉"的连接,表现为低峰期随机 Communications link failure。


2. 天花板在数据库侧,不在应用侧

约束 观察方式 决定什么
max_connections SHOW VARIABLES LIKE 'max_connections' 所有实例池之和的硬上限
每连接内存 按版本与配置核对,量级为数百 KB 到数 MB 连接数直接吃内存,与 buffer pool 争抢
CPU 核数 nproc、实例规格 同时可执行的活动连接数 ≈ 核数的个位倍数
磁盘 IOPS / 延迟 慢查询、iostat 长事务会占住连接不放
锁等待 sys.innodb_lock_waits 连接被占着不用,池被无效占用

结论直白:上限由数据库的活动并发能力决定,不由应用想开多少决定。起点可参考 核数 × 2 + 有效磁盘数 这一档(PostgreSQL 社区常用口径);MySQL 侧更保守,写路径锁竞争更重。具体数字按实例规格与读写比核对。


3. 为什么池越大越慢

三个机制同时起作用:上下文切换(活动连接超过核数后缓存命中率下降);锁竞争(更多事务挤同一批热点行,行锁与 latch 冲突上升,持锁时间变长);数据库侧排队(应用侧不排了,数据库内部队列更长,单查询变慢又反过来延长占用)。

有效吞吐 ≈ min(应用侧可并发, 数据库侧可并发)
数据库侧可并发 ≈ 核数 × (1 / (1 - 等待占比))     // 等待占比越高,可并发越低

越过"数据库侧可并发"之后,新增连接只抬高响应时间与 P99,吞吐不变或下降。


4. 用排队论口径算"够用就好"

目标是让数据库接近饱和但不排队,不是把连接开满。粗算用 Little 定律:

需要的并发连接数 ≈ 目标 QPS × 单次查询平均持有连接时间(秒)
例:目标 200 QPS,平均 SQL 耗时 15ms  → 200 × 0.015 ≈ 3 条并发
    再乘安全系数 2~3(应对长尾与抖动) → 池 8~10 条即可
场景 单请求平均持有时间 200 QPS 所需并发 建议池大小
主键点查 1–3 ms 1 5–10
普通列表查询 10–20 ms 2–4 10–15
聚合/排序报表 100 ms 以上 20+ 拆到只读池
长事务(批量更新) 秒级 极大 单独池 + 严格上限

这里的"持有时间"是从取连接到归还的全长,含应用侧处理,不只是 SQL 执行时间。


5. 连接的生命周期成本

环节 典型量级 什么时候值得关注
TCP 建连 同机房亚毫秒到毫秒 跨机房时抬到毫秒级
认证/权限校验 毫秒级,可缓存 每次新建都做全量认证时明显
TLS 握手 数毫秒到数十毫秒 强制 TLS 且未启用会话复用
会话初始化 每条连接一次 有 initSql、时区与字符集设置时
语句预热 首次执行才有 计划缓存未命中

频繁建连是把上述成本按请求次数摊销。minimumIdle 设 0 看着省资源,中等并发下会持续建连;设太大则长期占用会话。折中是让它接近低谷期实际并发。


6. 连接泄漏:三种成因与两种检测

# 泄漏检测:借出超过阈值未归还就打印持有栈,仅适合排障期开启
spring:
  datasource:
    hikari:
      leak-detection-threshold: 20000    # 毫秒
      maximum-pool-size: 20
      minimum-idle: 5
      connection-timeout: 3000
      max-lifetime: 1200000             # 必须短于中间件的空闲断连
      keepalive-time: 300000
成因 表现 检测方式
未关闭(漏 close) 池占用单调上升直至耗尽 泄漏检测栈;SHOW PROCESSLIST 长时间 Sleep
事务未结束 连接处于 Sleep 但仍持有事务与锁 查 information_schema.innodb_trx 的 trx_started
异步跨线程 在 A 线程取、B 线程还,或线程池切换后丢失上下文 线程栈里连接对象被非请求线程持有

第三种在 CompletableFuture、@Async、响应式混用里最常见。连接是线程绑定资源,跨线程必须显式交接,TransactionSynchronizationManager 的绑定不会跟着线程池走。


7. connectionTimeout:等待还是拒绝

取值倾向 语义 用户看到什么 适用
很短(几百 ms) 快速失败 部分请求直接失败,错误率上升但延迟可控 有降级路径、可重试的读请求
中等(2–3 s) 有限等待 队列内请求变慢,超时后失败 常规 OLTP
很长(10 s 以上) 无限接近排队 请求堆积、线程池被打满、雪崩 基本不该出现

它必须与线程池队列长度、请求总超时一起算:总超时 3 s 时池等待最多给 1–1.5 s,其余留给 SQL 与网络。不给预算等于把超时判定交给下游,错误会变成"调用方超时、服务端无日志"。


8. 读写分离与池拆分

# 主库只给写与强一致读;只读池单独限流,避免报表把写路径的连接吃光
spring:
  datasource:
    write:
      hikari: { maximum-pool-size: 15, connection-timeout: 3000 }
    read:
      hikari: { maximum-pool-size: 30, connection-timeout: 2000, read-only: true }

拆分依据是持有时间分布是否可分:报表持有连接比点查长一个数量级,混在一个池里会以极低 QPS 占满池。拆开后各自按自己的持有时间算容量。代价是多一份配置、两套监控,且"读走只读池"必须显式声明。


9. 池与线程池的配比

池 / 线程池 不匹配的现象 判断方向
线程池大、连接池小 大量线程阻塞在 getConnection 线程栈里 HikariPool.getConnection 占比高
线程池小、连接池大 连接空闲,并发上不去 池指标 active 远低于 max
两者都大 数据库被打满,应用侧看起来很忙 数据库侧等待事件与 CPU 同时高

方向是线程池 ≥ 连接池,但差值别太大:线程远超连接只会多一层无意义排队。同步阻塞模型取连接池的 1.5–3 倍,异步模型按事件循环数单独算。


10. 实测:压测 + 数据库侧等待事件

# 1) 阶梯压测,每档稳定后再取数
wrk -t4 -c200 -d60s --latency http://TARGET/api/items

# 2) 同期在数据库侧看"谁在等"
mysql -e "SELECT event_name, count_star, sum_timer_wait/1e12 AS sec
          FROM performance_schema.events_waits_summary_global_by_event_name
          ORDER BY sum_timer_wait DESC LIMIT 10;"

# 3) 连接状态分布
mysql -e "SHOW PROCESSLIST;" | awk '{print $6}' | sort | uniq -c

判据:池加大后行锁/IO 等待上升而 QPS 不涨,说明已越过数据库侧可并发;若应用侧 getConnection 等待仍是主要耗时,则池偏小。只看应用侧指标得不出结论,必须两边对齐时间轴。


11. 多实例部署的总量核算

总连接数 = 实例数 × 单实例池大小
必须满足:总连接数 + 运维/备份/监控连接 < max_connections × 0.8
例:6 实例 × 池 20 = 120,max_connections = 200 → 余量不足,需下调池或扩库

弹性伸缩要把最大副本数代入。"扩容后数据库连不上"的根因多是池按固定副本数设定,一伸缩就击穿 max_connections。对策:池加随副本数收缩的动态上限,或用代理层(ProxySQL)做连接复用。


12. 排错表

现象 最可能原因 核对
池调大后 P99 上升、QPS 不变 越过数据库侧可并发 数据库等待事件、CPU
随机 Communications link failure maxLifetime 长于中间件空闲断连 比对 LB/代理的空闲超时
池满但数据库很闲 连接被跨线程持有或事务未提交 泄漏检测栈、innodb_trx
启动后第一批请求慢 minimumIdle 过小,集中建连 建连耗时与首请求线程栈
高峰期大量 Connection is not available connectionTimeout 太短 + 池偏小 池等待耗时分布
主库连接被报表吃光 读写未拆池 按 SQL 类型统计持有时间

13. 边界:这些场景不该按上面的口径配池

场景 判断 理由
单实例、低并发(个位数 QPS) 池 5–10 即可,不必精算 排队论在小数上无意义
纯短连接 HTTP 优化建连成本而非池 池的收益来自复用
Serverless / 按请求拉起 池反成负担 空闲连接占会话、冷启动要建连
批处理独占时段 单独池 + 小上限 长事务毁掉 OLTP 的池
已有代理做连接复用 应用侧池可显著缩小 复用发生在代理层
读多写少且读走缓存 池按落到库的请求算 命中率决定真实并发

池大小是数据库容量的函数,不是应用野心的函数。输入只有三个:真实并发、单请求持有时间、数据库侧可并发能力。


14. 自测

  1. maximumPoolSize 调大之后吞吐不变、P99 上升,说明越过了哪条线?
  2. maxLifetime 应该和哪个外部超时比较,设错的症状是什么?
  3. 用 Little 定律估算池大小时,"持有时间"从哪一刻算到哪一刻?
  4. 池满而数据库空闲,最可能是哪两类原因?
  5. connectionTimeout 与请求总超时的关系该怎么分配?
  6. 弹性伸缩场景下,池总量该按当前副本数还是最大副本数核算?
S11-A10 · 连接池容量决策:池大小与真实并发
https://www.clxhxhhr.top/posts/6189/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。