线程池和数据库连接池的容量,为什么要一起做预算?
线程池和数据库连接池如果各自按经验配,线上很容易出现“线程还很多、连接先耗尽”或“连接够了、线程先堆死”的错位。把任务耗时结构、数据库连接持有时间、下游慢调用和峰值并发放回同一套容量模型里,预算才不会彼此打架。
线程池和数据库连接池,很多团队习惯分开配。线程池按 CPU 核数、任务类型和历史经验估,连接池按数据库连接上限、机器数量和框架默认值估,单看每一边都像有道理。
可一上生产,这两套数字经常会把系统推成两种很熟悉的错位:线程池还没用完,数据库连接先耗尽;或者连接看着够,线程已经在前面排起了长队。
然后团队会发现:
- 明明线程数不小,还是拿不到连接
- 明明连接数已经调大,接口 RT 还是继续飙
- 一调线程池就打坏数据库,一调连接池又只是把问题推后
这说明真正的问题不是某一个池子配小了,而是:
线程池和数据库连接池从来不是两个独立容量问题,而是同一条等待链上的两个并发门闸。
- 线程池控制有多少任务能并发进入处理阶段
- 数据库连接池控制有多少任务能并发进入数据库阶段
如果两者的容量模型不统一,线上就很容易出现“一个池子在放行,另一个池子在排队”的错位现象。
这篇文章想讲清楚的,就是为什么线程池和数据库连接池必须一起做预算,以及预算时到底该看哪些量,而不是只靠经验和默认值拍数字。
什么时候该把两个池子放进同一张容量账
只要线程池和连接池已经反复互相牵连,就别再分开估了,直接把任务时长、连接持有时长和峰值并发放进同一套预算里。
下面这组现象更容易帮你判断,是不是已经进入线程池和连接池要一起预算的场景。
| 你现在看到的现象 | 更像什么 | 下一步 |
|---|---|---|
| 线程池和连接池总是一起打架,想统一做并发预算 | 容量治理 / 联合预算 | 本文就讨论这个场景 |
| 线上已经 active 打满、queue 涨、reject 出现 | 更像先处理线程池已经堵住的现场 | 先看 线程池打满以后,应该先查队列、拒绝策略还是慢任务? |
| 获取连接慢、pending 高、连接池先耗尽 | 更像数据库等待链 | 接 数据库连接池打满时,根因通常不是连接数太小 |
| 想按 CPU / IO / 混合任务给线程池定 sizing | 更像先定线程池 sizing 的方法 | 接 线程池参数怎么按 CPU 密集和 IO 密集任务分别定? |
| 你需要判断是 DB 慢,还是应用拿着连接不放 | 更像先分清连接到底慢在库里还是慢在应用里 | 接 连接池等待时间变长时,如何判断是数据库慢还是应用拿着不放? |
下面只看联合预算,不展开排障细节
如果问题不是单看线程池或单看连接池就能解释,而是两个池子的容量模型一直在互相牵扯,下面就只围绕联合预算展开。
它重点讨论的是:
- 怎么把任务时长、连接持有时长和峰值并发放进同一套预算
- 数据库一慢时,线程池为什么会跟着排队
- 哪些反压和隔离做法能避免两个池子互相拖垮
手上如果已经是明显拥塞故障,直接去线程池或连接池那两条排障线;如果你在做长期治理和容量预算,就把这篇当成主线。
一、为什么分开配池子很容易出问题
因为同一个请求在生命周期里,往往会同时占用这两类资源,只是占用时段不完全相同。
一个典型的业务请求可能会经历:
- 占用业务线程或工作线程
- 执行业务逻辑
- 获取数据库连接
- 执行 SQL / 事务
- 继续做本地逻辑或调用其他下游
- 最终释放线程
这里最容易被忽略的一点是:
- 线程持有时间通常长于连接持有时间
- 但连接持有时间一旦被事务边界、慢 SQL、锁等待拉长,就会反过来拖住线程
所以这不是两个平行系统,而是同一批请求在两个资源池之间来回穿越。
分开配的典型问题
1. 线程池过大,连接池过小
结果会变成:
- 很多线程同时推进到数据库阶段
- 大量线程卡在等连接
- 线程数看起来很多,实际在制造等待
2. 连接池过大,线程池过小
结果会变成:
- 数据库本来还能处理更多请求
- 但应用前面的并发门太窄
- 队列先在业务线程池堆起来
3. 两边都拍大
结果通常是:
- 应用可以把更多请求推向数据库
- 一旦慢 SQL、锁等待、回源放大出现,系统会更快进入整体拥堵
所以问题从来不是“哪个数字大一点更安全”,而是:
两个池子能不能在同一条链路里协同工作,而不是互相打架。
二、一个基本认知:线程池决定入口并发,连接池决定数据库并发
这是做联合预算前最值得立住的一句话。
线程池更像第一道门
它决定:
- 当前能有多少任务同时进入业务执行阶段
- 能有多少请求持有工作线程
- 下游慢时,线程会不会先堆住
数据库连接池更像第二道门
它决定:
- 当前最多能有多少请求同时进入数据库阶段
- 数据库并发访问被限制在什么规模
- 数据库慢时,应用是快速排队,还是大量等待扩散
真正的关键不在于两道门一样大,而在于:
- 第一门放行的流量,第二门是不是接得住
- 第二门一旦变慢,第一门会不会继续把任务压进来
这也是为什么联合预算不是简单追求“线程数 >= 连接数”或“连接数 >= 线程数”,而是要回到请求耗时结构本身。
三、联合预算前,拆开请求的时间结构
这一步非常关键,也是很多容量规划文章最容易略过的。
真正影响线程池和连接池关系的,不只是请求数,而是:
- 线程被占多久
- 连接被占多久
- 两者重叠的时间有多长
更应该先回答的几个问题
- 单个请求总耗时里,有多少时间在持有线程
- 单个请求总耗时里,有多少时间在持有数据库连接
- 连接持有时间里,是真正执行 SQL,还是还夹着事务内等待
- 请求会不会在数据库阶段之外,还等待下游 HTTP / RPC / 缓存
一个常见的错觉
很多人会以为:
- 线程池就是算总并发
- 连接池就是算数据库并发
但真实系统里,更值钱的是这两个比值:
- 线程持有时长 / 总请求时长
- 连接持有时长 / 线程持有时长
如果连接只占线程时间的一小段,线程池和连接池就不需要一比一配置。
但如果事务边界很大、连接持有时间很长,连接池就会更接近线程池瓶颈。
四、为什么线程池和连接池最怕“事务边界过大”
这是两者联合预算里最容易被低估的风险点。
典型高危路径
例如一个任务会:
- 拿到线程
- 开事务
- 查库 / 写库
- 再调外部服务
- 再做一些本地计算
- 最后提交事务
- 释放连接和线程
这时会发生什么?
- 线程从头到尾都被持有
- 连接也被事务一路拿着
- 一旦外部服务慢,线程和连接都会一起被拖久
于是线程池和连接池的容量关系会迅速恶化:
- 线程还在忙
- 连接也回不来
- 两个池子会同时逼近上限
所以凡是事务边界过大的系统,线程池和连接池就更不能分开预算。
五、一个更实用的联合预算思路:先找谁先成为瓶颈
联合预算不是先算一个完美公式,而是先判断:
在你们的核心链路里,线程池和连接池到底谁更容易先成为第一瓶颈。
更像线程池先成为瓶颈的场景
- 任务中有较多本地计算
- 数据库阶段相对短
- 下游 HTTP / RPC 等待长,但不一定占连接
- 线程池里混跑多类任务
- 高峰时先见 queue 上升,而连接池还没明显紧张
更像连接池先成为瓶颈的场景
- 数据库访问频繁
- 事务持有时间长
- 慢 SQL、锁等待、长事务多
- 请求线程数不算太高,但获取连接耗时先升
- 高峰时连接池 pending 和超时比线程队列更早抬头
为什么要先判断这个
因为联合预算的目标,不是让两个池子都“不出问题”,而是让系统在逼近边界时:
- 更早暴露在可控的那一层
- 不要在另一层悄悄积压到更难处理
换句话说,你需要决定:
- 是让线程池先做反压
- 还是让连接池先限制数据库并发
这其实是架构选择,不只是参数选择。
六、联合预算时最该一起看的 5 个量
1. 峰值入口并发
先知道高峰时到底会有多少请求同时进来。
如果连这个量都没有,只讨论线程池和连接池大小,本身就已经失去边界。
2. 单请求线程持有时间
也就是:
- 请求从进入线程到释放线程,平均和高分位各多久
这决定线程池一段时间里能周转多少任务。
3. 单请求连接持有时间
也就是:
- 获取连接后到归还连接,平均和高分位各多久
这比单看 SQL RT 更关键,因为它直接决定连接池周转速度。
4. 连接持有时间在请求中的占比
这个量很能说明问题。
- 如果连接只占线程时间很小一部分,说明线程池和连接池可以适当错开规模
- 如果连接持有几乎覆盖整个请求时间,说明两者风险高度耦合
5. 慢链路下的最坏退化时间
这一步经常被忽略,但对稳定性最重要。
要问的是:
- 当数据库 RT 抬高一倍时会怎样
- 当锁等待增加时会怎样
- 当缓存 miss 导致回源时会怎样
- 当下游调用变慢时,事务是否会把连接一起拖住
没有退化场景,就没有真正的容量预算。
七、一个典型错位案例:为什么线程池 80、连接池 20,会把系统拖成排队系统
假设某服务:
- 业务线程池 80
- 数据库连接池 20
- 高峰时很多请求都会查库
- 单请求线程持有 500ms,其中连接持有 350ms
表面看起来:
- 80 个线程不算小
- 20 个连接似乎也合理
但一旦数据库 RT 稍微抬高,比如连接持有从 350ms 变成 900ms,会发生:
- 同一时间只有 20 个请求能进数据库阶段
- 另外 60 个线程要么在等连接,要么在业务路径上长时间拿着线程
- 线程池 active 会很高
- 连接池 pending 也会很高
- 上游请求开始排在两个池子前面
这就是典型的双层排队。
如果这时只扩线程池:
- 更多线程会一起等连接
如果只扩连接池:
- 更多并发会一起压数据库
真正该做的,是先回到:
- 为什么连接持有变长
- 线程是否在做不该同步做的事
- 事务边界是否把连接拿太久
这个例子最能说明:
联合预算的核心不是两个池子各自有多大,而是它们在退化场景里会不会形成双重等待。
八、如何把联合预算落到实际动作上
1. 按任务类型分池
不要让:
- 核心请求线程
- 慢批任务
- 异步补偿
- 导出任务
共用一套线程池再去谈连接池预算。
否则容量模型会先被任务混跑打乱。
2. 再按核心链路做连接池评估
识别:
- 哪些接口会高频查库
- 哪些接口事务边界大
- 哪些接口容易在高峰被回源放大
3. 设计“谁先反压”
这一点特别重要。
你要明确:
- 是让线程池 queue 控制入口并发
- 还是让连接池更早限制数据库并发
- 两边分别到什么程度算高风险
4. 把阈值落到监控和告警
至少要有:
- 线程池 active / queue / reject
- 连接池 active / idle / pending / 获取连接耗时
- 单请求线程持有高分位
- 单请求连接持有高分位
否则预算做完也很难持续验证。
九、联合预算里最容易出现的几个误判
误判 1:线程池按任务类型配,连接池按数据库上限配,各自合理就够了
各自合理,不代表组合后合理。关键看请求生命周期里两者怎么重叠。
误判 2:线程数一定要大于连接数很多才安全
有些场景确实需要,但如果数据库阶段占比高,这只是在制造更多等待线程。
误判 3:连接池尽量大一点,能减少拿连接超时
如果数据库已接近边界,连接池更大往往只是把问题推向数据库内部。
误判 4:压测通过就说明预算没问题
如果压测没覆盖缓存回源、锁等待、下游慢调用和事务放大场景,结论往往过于乐观。
误判 5:联合预算是一次性工作
业务结构、流量模式、热点对象和事务路径变了,线程池和连接池的关系也会变。
十、做联合预算时,现场最常聊到的几个问题
1. 线程池和连接池有推荐比例吗?
没有通用比例。更重要的是请求里线程持有和连接持有的时间结构,以及高峰退化时两者谁先成为瓶颈。
2. 为什么线程池很空,连接池也可能先满?
因为数据库阶段可能已经足够慢,少量线程就能把连接全部占住。线程数不高,不代表连接就够用。
3. 为什么连接池够大,线程池还是会先堆?
因为请求可能主要卡在数据库之外,例如本地计算、下游调用、事务外等待或任务混跑,线程先成为第一门槛。
4. 联合预算最后最应该沉淀成什么?
最应该沉淀成:
- 核心链路的线程 / 连接持有基线
- 高峰和退化场景的风险阈值
- 谁先反压的设计原则
- 对应的监控和告警边界
十一、最后总结:线程池和连接池的容量预算,本质上是在设计“并发在哪一层被限制、在哪一层被放大”
线程池和数据库连接池如果分开看,最后很容易只看到两个数字;但真正决定线上稳不稳的,是它们在同一条请求链里如何相互作用。
更实用的主线是:
先拆清请求里线程和连接各自被持有多久,再判断哪一层更容易先成为瓶颈,最后让线程池、连接池、事务边界和退化场景一起形成统一容量模型。
只要这条主线立住,线程池和连接池就不再是两个各自调参的孤岛,而会真正变成一套服务于稳定性治理的联合并发预算。
联合预算定住后,后面通常分两路继续做
如果你已经把联合预算这件事想清楚了,后面通常会顺着下面几条线继续补证据或补治理动作:
- 线上已经 active 打满、queue 和 reject 一起上来:
线程池打满以后,应该先查队列、拒绝策略还是慢任务? - queue 不长,但 worker 还是推进很慢:
线程池队列不长但任务还是慢,常见瓶颈在哪里? - 获取连接慢、pending 高、连接池先耗尽:
数据库连接池打满时,根因通常不是连接数太小 - 你需要判断 DB 慢还是应用拿着连接不放:
连接池等待时间变长时,如何判断是数据库慢还是应用拿着不放? - 你还在给线程池按 CPU / IO 类型做 sizing:
线程池参数怎么按 CPU 密集和 IO 密集任务分别定?
所属专题
- 稳定性治理与值班体系
如果你准备把治理动作落到值班和保护策略里
真到线上落动作时,我一般会这样推进
- 先把 线程池 -> 连接池 -> 请求耗时结构 -> 退化场景 放进同一套容量账里。
- 还没分清线程池 sizing 的任务类型前提,就继续看 线程池参数怎么按 CPU 密集和 IO 密集任务分别定?。
- 线上已经出现线程池和连接池一起变差,就把 线程池打满以后,应该先查队列、拒绝策略还是慢任务? 和 连接池等待时间变长时,如何判断是数据库慢还是应用拿着不放? 对着一起看。
- 如果接下来要把预算落到接口保护和治理动作,再把 一个接口的超时、重试、熔断参数,应该怎样整体设计? 串起来。