MySQL线程池优化高并发:连接数、TPS与压测调优
简介:《MySQL新增线程池插件说明》面向DBA、后端开发与性能测试人员,聚焦高并发请求下MySQL线程创建销毁开销大、性能波动等问题,提供线程池插件引入前后的对比测试与稳定性验证思路。压缩包仅含1个PDF文件,大小3.26MB,正文按测试计划、目标、环境、网络拓扑、软硬件配置、测试数据与代码改造、测试工具、单库及四库负载测试、稳定性测试等模块组织,并说明线程池工作原理、安装配置参数及sysbench、JMeter、Performance Schema等工具选型。文档给出可复用的压测策略与执行安排,便于读者对照搭建接近生产的环境,采集QPS、TPS、响应时间和资源利用率数据,判断插件收益与持续高负载下的稳定性。已有446人学习,适合需要落地数据库性能优化与压测方案的中高级技术人员参考。
1. 线程池不是"多开线程":MySQL 高并发下的线程模型为什么必须换
把 MySQL 线程池插件装上之后最反直觉的一点是:服务端线程数没有变多,反而被限制在一个常数附近,单库吞吐却从 131676 TPS 涨到了 250274 TPS。这是一份数据库优化性能压测方案里记录的对比——同一个 48 核实例、同一张 120 万行的表、同一条主键查询,唯一变量是插件开没开。很多人以为数据库优化就是加核、加内存、调大 max_connections,可连接一多,CPU 使用率上去了,TPS 反而掉下来。线程池要解决的就是这个"连接数涨、吞吐不涨"的怪现象:把线程创建销毁和调度排队的成本,从每次请求都要付,改成一次性付清。搞懂它,你才知道高并发下该调的是连接数还是线程数。
2. 一连接一线程:从 pthread_create 到调度器抖动,开销到底在哪
2.1 连接数等于线程数这件事,本身就是一个隐藏上限
MySQL 默认的线程模型叫 one-thread-per-connection,直译就是"一个连接配一个线程"。客户端连上来,服务端在接入层分配一个 THD(Thread Handler Descriptor)对象,然后用 pthread_create 起一个线程,这个线程从头到尾只为这一个连接服务,直到连接断开才销毁。这套模型在小规模下非常优雅:没有队列、没有调度、没有跨连接的状态共享,请求处理路径最短。
问题出在规模上。2000 个连接就是 2000 个线程,每个线程要占一块栈空间(thread_stack 参数控制,常见配置是 256KB 到 1MB),还要在 OS 的调度器里占一个调度实体。线程不是免费的,即使它什么都不干,也躺在 runqueue 里等着被调度,内核仍要维护它的上下文。方案里那台 48 核 755G 内存的机器看着很宽裕,可真正限制你的是 CPU 核数与可运行线程数的比例,跟内存基本没关系。
这里有个常被误解的点:应用侧的连接池只解决了"客户端不频繁建连",它完全没有改变服务端"连接数=线程数"的等式。连接池把 500 次短连接变成了 500 个长连接,服务端依然老老实实起了 500 个线程。换句话说,连接池是客户端的一层缓存,它压根没优化到服务端的线程模型。
2.2 CPU 使用率 69% 里,有多少是真正在跑 SQL
方案里有个反复出现的指标组合值得盯:CPU 平均使用率 69%、I/O wait 很小、90% 响应时间 1ms。乍看很健康,但如果把 CPU 时间拆成 user 和 sys 两部分,高并发下的 sys 占比往往会被拉高——上下文切换、锁获取、内存分配、系统调用,全都算在 sys 里。
真实的损耗来自三个方向。第一是调度延迟:48 核上如果同时有几百个可运行线程,每个线程拿到的 CPU 时间片被切得很碎,一次请求在 CPU 上只跑了几十微秒就被换下,寄存器现场和缓存行全部作废,重新调度回来时 L1/L2 大概率已经冷了。第二是锁竞争:线程多了,全局的 LOCK_thread_count、连接链表、THD 相关结构上的临界区被争抢,原本几十纳秒的操作可能被拉长到微秒级,而且是串行化的。第三是内存分配压力:每个连接线程都要分配自己的网络缓冲区、结果集缓冲,分配器在高并发下也容易成为热点。
这些开销有个共同特征——它们跟业务逻辑无关,纯粹是"为了支撑这么多个线程"而付出的固定成本。请求越短,这部分固定成本占比越高。方案里测的是主键点查,一条 SQL 执行可能只有几十微秒,此时线程调度开销几乎和业务处理开销同量级,这就是为什么短查询场景对线程模型特别敏感。
2.3 用利特尔法则反推:为什么线程池后要 400 个并发才能喂饱
先看方案里四组负载测试的实际读数:
| 场景 | 客户端并发 | 持续时长 | TPS | 90% 响应时间 | CPU | 网络流量 |
|---|---|---|---|---|---|---|
| 线程池前,四库 | 80 | 300s | 129871 | ≤1ms | 69% | 40.125MB |
| 线程池前,单库 | 80 | 300s | 131676 | ≤1ms | 68% | 41MB |
| 线程池后,四库 | 100 | 300s | 171907 | ≤1ms | 69% | 52.25MB |
| 线程池后,单库 | 400 | 300s | 250274 | ≤2ms | 67% | 75.125MB |
这张表最容易被忽略的一列是"客户端并发"。线程池前,80 个并发就能把单库压到 131676 TPS,90% 响应时间 1ms 以内;线程池后,单库要开到 400 个并发才跑到 250274 TPS,而且响应时间从 1ms 变成了 2ms。并发翻了 5 倍,吞吐只涨了 90%,响应时间翻倍——这看起来是"变差了",其实是把两种成本换了个位置。
用利特尔法则(Little's Law)粗略估一下:稳定状态下,系统内并发数 ≈ 吞吐 × 平均响应时间。线程池前,131676 TPS × 1ms 大约对应 132 个在途请求;线程池后,250274 TPS × 2ms 大约对应 500 个在途请求。这个数字说明,线程池把"请求在服务端排队"这件事从线程调度器那里搬到了应用层的任务队列里。排队本身没有消失,只是变得可观测、可限流、可调优。原来排队发生在 OS runqueue 里,你只能看着 CPU 使用率干着急;现在排队发生在插件自己的队列里,你能看到队列长度、能看到哪个 group 堵住了,这是从"黑匣子"到"仪表盘"的升级。
反过来看,如果只看吞吐不看响应时间,很容易得出"线程池提升 90%"的乐观结论,但真正的结论应该是:线程池让服务端能用更少的线程承接更高的并发,代价是单请求的最坏延迟被拉长,尾延迟需要单独盯。
3. 线程池插件怎么工作:thread group、listener 与 worker 的三层分工
3.1 thread group:为什么按 CPU 核数切,而不是搞一个大池子
线程池最核心的结构是 thread group(线程组)。一个 group 本质上是一组 worker 线程,加上一个属于自己的任务队列和一把属于自己的锁。默认情况下,group 的数量与 CPU 核数相关,配置参数 thread_pool_size 控制的正是这些线程组的数量,它并不是"总线程数"。
这个语义混淆是最常见的一类错误:很多人把它当成"池里最多开多少个线程"直接填 50,结果在一台 48 核机器上只开了 50 个组、每组按 1 个活跃 worker 加 oversubscribe 计算,实际可并行处理的请求数远低于预期,队列开始堆积。可靠的用法是让 thread_pool_size 跟核数对齐,由每组内部的 worker 数量去控制实际并发。
分组的意义在于隔离竞争。如果全库只有一个大池子,那么每次有请求入队、每次 worker 取任务,都要抢同一把锁;核数越多、线程越多,这把锁越热,最终退化成串行。拆成 N 个组以后,每个组只服务一部分连接,锁的争用被摊薄到 N 份,扩展性近似线性。这是一种典型的"分片"思路,跟哈希表分段锁、连接池分片是一回事。
连接和 group 的绑定关系一旦确定就不再随机漂移,新连接进来时按某种策略(常见是轮转或者按连接标识取模)分配到一个 group,之后就一直待在这个组里。稳定绑定带来一个好处:同一个连接的请求总是落在同一批 worker 上,CPU 亲和性和缓存局部性都能得到一定的保持。
3.2 listener 与两级队列:为什么老连接要比新连接优先
每个 group 里都有一个 listener 线程,它不负责执行 SQL,只负责从网络事件里捞出可读数据、把任务塞进队列、并在必要时唤醒 worker。这个角色很像一个调度员,它决定了"谁先被处理"。
队列被设计成两级:高优先级队列和普通队列。高优先级队列里放的是"已经在这个 group 里处理过的连接"发来的后续请求,普通队列放的是新分配进这个 group 的连接的第一个请求。worker 取任务时优先清空高优先级队列,只有高优先级队列空了才去碰普通队列。
这么设计的动机很实在:如果只用一个队列,一个刚接入的新连接可能排在几百个老请求后面,网络缓冲区越堆越满,客户端开始重试,重试又生成新连接,雪崩就是这么起来的。给老请求加优先级,等于保证已经在处理的会话不会被新流量饿死,系统在高负载下仍然能维持"部分请求快速完成",而不是"所有请求一起变慢"。这个取舍在压测里的体现就是 90% 响应时间仍然能压在毫秒级——哪怕平均值已经被最坏的那批请求拉高,分位数指标还是好看的。血泪经验是,很多团队做容量评估只看平均 RT,线程池上线后平均 RT 涨了、P99 涨了,但 P50 没怎么动,实际用户体验反而更稳定。
3.3 stall limit 与 worker 唤醒:阻塞的线程怎么不拖垮整组
线程池最容易被忽视的机制是"阻塞检测"。worker 执行的 SQL 可能阻塞——磁盘 I/O、行锁等待、大排序落盘,这些都可能在几毫秒到几秒内不返回。如果一个 group 里所有 worker 都卡在阻塞上,队列再满也没人取,整个组就"死"了。
插件用 listener 做周期巡检,判断这个 group 是否处于"停滞"状态:队列里有任务,但没有 worker 在推进。一旦超过阈值(thread_pool_stall_limit,单位是毫秒),listener 就会唤醒或创建新的 worker 来接管队列。也就是说,线程池里 worker 的上限并不是写死的常数,而是在"允许的过度并发"范围内动态伸缩。
这里有个参数值得单独说:thread_pool_oversubscribe,它定义了每个 group 允许的额外活跃 worker 数量,常见默认值是 3。意味着在一个 group 里,理想状态下只有 1 个 CPU 密集的 worker 在跑,另外最多 3 个可以在阻塞状态中挂着。加起来相当于每组 4 个"名额"。这个数字不是拍脑袋定的,而是经验上的折中——太少容易因为阻塞而饿死队列,太多就退回到频繁上下文切换的老路。
把 oversubscribe 和 group 数乘一下,就能算出这台机器的实际并行处理能力上限。48 核、48 组、每组 4 个名额,理论上限接近 192 个同时推进的请求。但绝大多数点查在 CPU 上只跑几十微秒,压根到不了阻塞的程度,所以真实吞吐远高于这个数字,靠的是 worker 快速轮转而不是并发堆积。
下面这段配置是把上面这些机制串起来的最小集合:
[mysqld] # 启用线程池调度,替代默认的一连接一线程 thread_handling = pool-of-threads # 线程组数量,建议与 CPU 核数对齐,不是"总线程数" thread_pool_size = 48 # worker 停滞判定阈值,单位毫秒;设太小会频繁创建线程,设太大会掩盖阻塞 thread_pool_stall_limit = 500 # 每个组允许的额外活跃 worker 数,控制阻塞容忍度 thread_pool_oversubscribe = 3 # 空闲 worker 的回收时间,单位秒,避免低峰期线程空占资源 thread_pool_idle_timeout = 60 # 每个组允许创建的 worker 上限,防止异常场景下线程爆炸 thread_pool_max_threads = 100000参数之间是有联动关系的。thread_pool_size 决定横向分片数,thread_pool_oversubscribe 决定纵向容忍度,thread_pool_stall_limit 决定"什么时候认为纵向不够用"。三者配合不好就会出现两种极端:组数太少,锁竞争没解决;stall_limit 太小,短查询高峰时频繁创建线程,等于把已经付清的成本又还回去了。
3.4 状态观测:线程池跑起来之后看什么
线程池上线后不能只看 TPS,还要看组内的实际状态。多数实现的插件会暴露一组线程池相关的状态变量,可以直接查:
-- 查看线程池整体状态,变量名以实际插件文档为准 SHOW STATUS LIKE 'Threadpool%'; -- 关注项: -- Threadpool_threads 当前组内 worker 总数 -- Threadpool_idle_threads 空闲 worker 数 -- Threadpool_connections 当前绑定的连接数 -- Threadpool_stall_count 累计判定为停滞的次数 -- Threadpool_queue_length 当前排队任务数判断逻辑很直接。如果 Threadpool_idle_threads 长期接近 0,说明 worker 不够用,要么加组,要么调 oversubscribe。如果 Threadpool_stall_count 持续增长,说明大量请求卡在阻塞上,得去查慢 SQL、锁等待或者磁盘 I/O,这时加线程只会让问题扩散得更快。如果 queue_length 一直高位不降,而线程数也上不去,看看是不是 thread_pool_max_threads 触顶了。
4. 压测方案本身的设计:四段验证怎么把结论做扎实
4.1 控制变量:为什么这份方案的数据可信
一套性能压测最怕的不是数字难看,而是数字不可比。方案里做了几件事把变量压到最少:软硬件配置固定(数据库机与 4 台发压机都是 48 核 755G 内存)、基础数据量固定(20w、30w、120w、1w 四张表)、压测脚本按现网最大请求体改造、参数化数据量按并发和时长准备、把日志级别从默认调整到 error 以减少写日志带来的额外 I/O。
日志级别这一项特别容易被忽略。默认日志级别下,每条 SQL 都可能触发格式化、写缓冲、刷盘,压测打到十万级 TPS 时,日志本身就是一个稳定的性能黑洞,而且它会随并发线性增长,把线程模型带来的差异完全淹没。把级别降到 error,等于先把这条干扰项剪掉,剩下的差异才归因得到线程池。
压测工具用的是 JMeter,通过命令行在负载机上跑:
# 非 GUI 模式压测,结果写进 jtl 文件,避免图形界面的资源开销影响发压能力 ./jmeter -n -t query_ability_attr.jmx -l logs/run_01.jtl # 压测结束后把 jtl 转成 HTML 报告,看聚合报告、响应时间趋势、TPS 曲线 ./jmeter -g logs/run_01.jtl -o report_01/这三步的关键是"-n"——图形界面本身要吃掉一两个核,在 5 台负载机规模下还好,单机压测时它会让发压能力提前见顶,导致你误判服务端的容量上限。jtl 文件是原始数据,报告是二次加工,排查异常时一定要回到 jtl 里看具体请求的分布,而不是只信 HTML 页面上那个平均值。
监控侧用的是 zabbix、JvisualVM 加 Linux 命令,采样窗口从压测开始前 5 分钟就启动,这样才能拿到基线,而不是从第一条请求开始才有数据。这一点很多团队会漏:不记录基线,后面看到 CPU 稳定在 67%,你没法判断这 67% 里有多少是数据库自己贡献的,多少是机器上别的进程。方案里后面那条备注就踩在这个点上——数据库机上的其他应用常年占着 3% 的 CPU,所有读数都要把这也算进去。
4.2 阶梯加压:40 一路加到失败,找的是拐点不是峰值
方案的加压方式是标准的阶梯式:从 40 个并发流起步,稳定运行 5 分钟后加 40,再稳 5 分钟,如此往复。每一档都检查五个门槛——CPU 和内存平均使用率不高于 70%、I/O wait 不高于 30%、网络带宽不超过 70%、业务成功率不低于 99.99%、90% 响应时间在 100ms 以内。任何一项破了就停,定位问题。
这套判据的价值在于它不是找"最高 TPS",而是找"在健康水位下的最高 TPS"。很多人压测只盯着 TPS 曲线的顶点,可顶点往往出现在资源已经被打满、响应时间开始抬头的区间,那个数字在生产上不能用。70% 这条线是有讲究的:留 30% 余量给突发流量、给合并操作、给备份任务,也留给内核自己的调度周转。I/O wait 卡在 30% 同理,再往上说明存储层已经在排队,这时候 TPS 再涨也是靠堆积换来的。
找到拐点之后还有一步"复压":按最大能力的请求量再稳跑 5 分钟,确认这个容量是可重复的,而不是某一瞬间的抖动。这一步其实是在排除偶然因素,比如刚好赶上一次 checkpoint 结束、刚好页缓存全热。
4.3 四库和单库的差异,暴露了瓶颈到底在哪一层
把四组负载测试放在一起看,会看到一个很明显的非对称:四库场景下,线程池把 TPS 从 129871 提到 171907,涨了约 32%;单库场景下,从 131676 提到 250274,涨了约 90%。同一个插件、同一套硬件,为什么差距这么大?
答案在瓶颈位置上。四库场景下,压力被分散到四个实例上,单个实例分到的请求量少,线程数没到临界点,线程池能省的调度开销有限;同时 4 台发压机的网络出口和 JMeter 自身的处理能力开始成为新的天花板,网络流量只到 52.25MB 就能看出端倪——服务端还没跑满,发压端先顶住了。单库场景正好相反,所有请求砸在一个实例上,线程调度成为最紧的那根弦,线程池一上,弦松了,TPS 直接冲上 250274。
这个差异说明一件很实用的事:做线程池收益评估,要在"真正有瓶颈"的场景下测。如果拿四库的数据去说服决策,很可能得出"提升 30%"的保守结论;如果拿单库的数据,又会过于乐观。合理的做法是两个都测,然后看你的生产拓扑更接近哪一种。
4.4 12 小时稳定性:为什么 52 线程只跑出 95000 TPS
稳定性测试的设定跟负载测试完全不同:52 个并发持续 12 小时,TPS 稳定在 95000 左右,90% 响应时间 ≤1ms,成功率 100%,CPU 使用率 60%,网络流量 27.5625MB。这组数字比峰值低了一大截,有人会问是不是没压到位。
这正是稳定性测试该有的样子。它验证的不是"能不能跑到极限",而是"长时间运行会不会劣化"。判断劣化的依据是三件事:TPS 曲线有没有随时间下滑、成功率有没有出现零星失败、内存和线程数有没有单调增长。如果 12 小时内 TPS 曲线始终保持水平,说明线程池没有泄漏,worker 的创建回收是闭环的;如果有缓慢下滑,通常是内存碎片、连接状态累积或者某些资源的引用计数没释放。
52 这个并发值也是刻意选的,方案里写的是"不低于最优容量的稳定业务流"——它不是峰值,而是留了余量的可持续水位。用峰值跑 12 小时,测出来的不是稳定性,是耐久性,两者目的不一样。
4.5 那条"32 组线程"的补充数据,讲的是组数不是越多越好
方案末尾有一条容易被跳过的备注:改成 32 个组、单库、150 并发(5 台发压机各从 10 逐渐加到 50)时,TPS 达到 240000,CPU 使用率 67%;继续加并发,TPS 和 CPU 的涨幅都不明显;而并发数瞬间冲到 225、250 时,CPU 使用率反而只有 60%。
三个现象各自的解释不一样。TPS 到 240000 后不再涨,是因为 32 组在 48 核机器上已经出现了明显的核间不均衡——有的组里 worker 排着队,有的组闲着,总体上 CPU 还有余量但任务分不到那些核上。CPU 在并发瞬间冲高时反而降到 60%,是因为瞬时大量请求到来时,线程创建和队列入队这些操作本身要抢锁,加上请求还没真正开始执行,CPU 采样窗口里看到的更多是等待而不是计算。
这条数据最有价值的结论是:thread_pool_size 不是越大越好,也不是越小越好,它要跟核数匹配。48 核配 32 组,平均每组 1.5 核,天然存在负载不均;配 48 组,一核一组,分摊最均匀。超过核数以后,多出来的组不会增加并行度,只会增加调度实体和锁的数量。
5. 避坑:线程池上线前最容易栽的几个跟头
5.1 参数语义类:名字看着一样,含义完全不是一回事
把 thread_pool_size 当成线程总数填。现象是配置里写了 100,监控上看到的 worker 总数远大于 100,或者反过来,队列一直堆积但线程数上不去。根因是这个参数在多数实现里表示线程组的数量,而不是线程数量,真正的单组并发由 oversubscribe 和其他参数共同决定。改法是先确认插件版本里这个参数的确切语义,再按 CPU 核数设组数,用状态变量验证实际 worker 数。
把 stall_limit 理解成"worker 最长等待时间"。现象是把它设得很小(比如几十毫秒),压测时线程数剧烈波动,CPU 的 sys 占比明显升高。根因在于它其实是"判定组内停滞的阈值",一旦超过就触发新线程创建,设太小等于让插件不停地建线程销毁线程,把省下来的成本又花出去了。改法是从几百毫秒起步,观察 Threadpool_stall_count 的增长速率,只有在阻塞确实频繁时才下调。
忽略 thread_pool_max_threads 这个兜底上限。现象是某次慢查询集中爆发后,进程线程数暴增,内存跟着涨。根因是阻塞检测不断创建新 worker,而上限设得过大或者干脆用的是默认大值。改法是按"核数 × oversubscribe × 安全系数"估一个合理上限,宁可让队列堆积触发告警,也不要让线程无限膨胀。
5.2 场景适配类:线程池救不了长查询
在线事务里混着大查询,上线后尾延迟恶化。现象是 P50 没变、P99 从几十毫秒跳到几百毫秒,偶尔出现超时。根因是线程池的前提假设是"单个请求在 CPU 上执行时间短",一旦有请求跑几秒钟,它占着的 worker 名额就一直不释放,同组其他请求只能排队等 stall_limit 触发扩容,扩容又带来新的竞争。改法是把长查询拆到独立实例或者只读副本上,主库的线程池只服务短事务。
把线程池当成解决连接数过多的办法。现象是开了线程池,连接数依然往上涨,最终撞上 max_connections。根因是线程池限制的是服务端线程数,不是连接数,每个连接依然要占 THD、要占缓冲区。改法是在应用侧做连接池上限控制,服务端把 max_connections 当作硬性保护而不是容量目标。
短连接场景下收益远低于预期。现象是业务方大量使用短连接,每次请求建连断连,线程池上线后 TPS 提升很小。根因是连接建立本身的开销(认证、TLS 握手、THD 初始化)没有被线程池覆盖,线程池只优化了请求处理阶段。改法是先在客户端引入连接复用,再评估线程池收益,否则测出来的差异会被建连成本稀释掉。
5.3 压测方法类:发压端先撑不住,测出来的是自己
压测曲线在某个并发点突然变平,误判为服务端容量上限。现象是并发加到某个值后 TPS 不再增长,响应时间也不涨,看起来像是服务端到了极限。根因很可能是发压机自己跑满了,JMeter 的线程模型和 GC 都会吃 CPU,5 台机器也可能不够。改法是同时监控发压机的 CPU 和网络出口,如果发压机先到 70% 以上,先加机器再谈结论。
只看平均响应时间,忽略分位数。现象是报告里平均 RT 只有 2ms,看起来很美,上线后用户投诉变多。根因是线程池把延迟分布拉长了,平均值被大量快速请求压住,长尾被掩盖。改法是把 90%、99% 分位数和最大值一起列进验收标准,方案里定的"90% 响应时间 ≤100ms"就是往这个方向靠。
测试数据量偏小,缓存全命中。现象是压测 TPS 特别高,生产上完全复现不了。根因是测试表只有几十万行,整张表都在 buffer pool 里,压根没有磁盘读。改法是按生产数据量级准备,或者至少让工作集明显超过内存容量的合理比例,方案里 120 万行这个量级就是有意为之。
6. 什么时候不该开线程池:一次容量决策的取舍清单
线程池的收益来自"请求短、并发高、线程调度成为瓶颈"这三个条件同时成立。三个条件缺一个,收益就会打折,缺两个甚至可能为负。纯长事务场景,比如批处理、大报表、全表扫描,worker 一旦阻塞就是几秒到几分钟,线程池的扩容机制会不断造新线程,最后既不省资源也不提吞吐,还不如让每个连接自己扛着。低并发场景同样没必要,几十个连接的情况下 one-thread-per-connection 的路径最短、延迟最低,加一层队列只是徒增一次入队出队。连接数长期稳定的场景(比如只有几个后台任务在跑)也不值得折腾。
判断该不该开,我一般看三个信号。第一,压测中 CPU 使用率上去了但 TPS 不涨,且 user 时间占比低于 sys,说明时间花在调度和锁上。第二,连接数随业务增长线性上升,但 CPU 核数是固定的,线程数与核数的比值已经超过一个数量级。第三,短查询占比高,单条 SQL 执行时间在亚毫秒到几毫秒之间。三个信号同时出现,线程池的收益基本能兑现;只出现一个,先去做别的优化。
验证方法也很具体:同一套硬件、同一份数据、同一个脚本,只切换 thread_handling,跑两轮阶梯加压,把 TPS、90% 分位数、CPU 的 user/sys 拆分、上下文切换次数(vmstat 的 cs 列)一起拉出来对比。如果 TPS 涨了但 cs 没降,说明瓶颈不在调度上,线程池只是碰巧撞对了一部分;如果 cs 明显下降、TPS 同步上升,那才是真正的机制生效。这个对比做完,参数怎么调心里就有数了——组数向核数看齐,stall_limit 从几百毫秒起步,oversubscribe 保持默认观察 stall_count,再按实际阻塞情况微调。
后面这套动作我养成了一个习惯:任何线程模型相关的改动,上线前都要在同样的并发下把 cs 和 sys 占比各测一遍,不看这两个数就下结论,等于闭着眼睛调参。那份压测方案里 12 小时稳定性只跑 52 并发、CPU 稳在 60% 的做法,我后来也一直沿用到自己的容量验收里——峰值决定你能接多少,稳定水位决定你能撑多久,两件事必须分开测。希望帮到你。
本文还有配套的精品资源,点击获取