MySQL线程池高并发压测:从TPS到stall_limit
发布时间:2026/11/4 13:35:38 锦皓数字建站

简介面向数据库运维与后端开发人员的MySQL高并发优化资料围绕线程池插件解决频繁创建销毁线程带来的资源开销与性能波动问题给出从原理、安装配置到压测评估的完整思路。压缩包仅含1个PDF约3.26MB以正式性能测试报告形式组织包含测试依据、目标、环境与网络拓扑、软硬件配置、测试数据与代码改造、工具选型以及新增线程池前后四库和单库负载、稳定性测试等章节。已有446人学习。读者可据此了解thread_pool_size、thread_handling等参数如何落地掌握QPS、TPS、响应时间与资源利用率的对比方法并参考人力与时间安排、风险点分析和报告目录快速搭建可复用的MySQL线程池压测方案。1. 线程池不是“多开线程”MySQL 高并发下的一个反直觉结论把线程池插件装上之后同一台 48 核机器上的 TPS 从 13 万涨到 25 万但 CPU 使用率只从 68% 变成 67%——按常理吞吐涨了快一倍CPU 应该跟着往上走可它几乎没动。这个反直觉的数字来自一份 MySQL 高并发压测报告它戳破了一个常见的误解数据库慢不一定是 SQL 写得不好也可能是线程模型在高连接数下先跨了。MySQL 默认的 one-thread-per-connection 模型让每个连接独占一个线程连接一多线程创建销毁、上下文切换、调度器锁竞争就会把 CPU 大量时间耗在“管理线程”而不是“执行 SQL”上。线程池插件做的事不是简单地把线程数量固定住它把连接、请求、工作线程三者重新分层用 thread_pool_size 个线程组去接住并发。搞懂这套机制才能判断自己的库到底该不该开、开了之后该压到什么程度。2. 一请求一线程的崩溃点连接数上涨为什么反而吞吐下降2.1 默认 thread_handling 模型下每个连接都付出什么代价MySQL 的 thread_handling 默认值是 one-thread-per-connection含义很直白每来一个客户端连接数据库创建一个线程专门服务它连接断开线程销毁。这套模型在低并发时简单有效不需要复杂的调度器请求进来就有线程接住响应路径短。但要算一笔账每个线程有栈空间默认在 256KB 到 512KB 量级还有内核调度实体和 pthread 的创建销毁系统调用。连接数从 100 涨到 1000 时线程数量线性上涨内存占用跟着涨。更关键的是 CPU线程之间的上下文切换、mutex 竞争、调度器运行队列的管理都在消耗 CPU 周期而这些周期并不产出 TPS。压测报告里线程池前的数据能侧面说明问题80 线程持续压 300 秒四库场景 TPS 129871CPU 已经到 69%单库场景 TPS 131676CPU 68%。四库和单库的 TPS 几乎没差别说明瓶颈不在数据库实例数量上而在连接接入层——线程模型本身成了天花板。2.2 线程创建销毁的开销为什么在高并发下被放大单次 pthread_create 的开销在微秒级看起来不大。但高并发下它是乘法关系如果每秒有上万次短连接或者连接池频繁重连创建销毁的次数乘以单次成本就会变成可观的 CPU 消耗。压测里如果只盯平均 CPU这部分开销会被摊薄到看不见但它在 TPS 曲线上会表现为“加并发不涨吞吐”。另一个隐性成本是线程栈的缺页。新线程第一次访问自己的栈内存时触发缺页异常内核要分配物理页。线程销毁后这些页不一定立刻回收内存碎片和页表压力会累积。这属于那种“短时压测看不出来、跑久了才暴露”的问题12 小时稳定性测试里如果只盯 TPS 曲线很容易漏掉内存 RSS 缓慢上涨的曲线。数据库优化里常说的“连接数不是越大越好”底层原因就在这里连接数上涨带来的边际收益会被线程管理成本吃掉。2.3 线程池把“连接”和“执行”切开分层模型的核心线程池插件的核心改动是不再让连接绑定线程。连接进来后先进入某个线程组的队列由监听线程负责从 socket 读请求再把请求分派给工作线程执行。工作线程执行完回到池里等下一个请求不销毁。这个分层带来的直接变化是线程数不再等于连接数连接再多活跃线程数也被压在可控范围内。用一句话概括机制上的差别默认模型是“一个连接一个服务员”线程池是“一组服务员轮着接待排队区在组内”。前者并发越高服务员越多后者服务员数量固定靠队列和轮转扛并发。这里有个容易混淆的点应用层连接池管的是复用 TCP 连接数据库线程池管的是复用执行请求的线程两者解决不同层面的问题。应用层连接池开得再大如果数据库侧还是 one-thread-per-connection连接落到数据库上照样一个连接一个线程该有的开销一分不少。3. 线程池插件的结构拆解thread_pool_size 与 stall_limit 到底约束了什么3.1 线程组、监听线程与工作线程的角色划分常见的线程池实现里线程组是基本调度单元。每个组有一个监听线程它不执行 SQL只负责从连接上读请求、把请求放入队列、唤醒工作线程。工作线程从队列取任务执行执行完继续取下一个。这样设计的好处是监听线程始终不会被慢 SQL 阻塞保证新请求能持续被接收。工作线程被阻塞时监听线程可以感知并做补救——补救机制就是 stall_limit。线程组和连接的关系是多对多一个连接在生命周期内可能被多个工作线程服务但同一时刻只有一个线程持有它的执行上下文。这也是为什么线程池对会话状态敏感的语句需要额外小心常见的实现会在会话上下文切换时做保存和恢复。想观察这套机制是否在按预期工作可以查线程池状态变量-- 不同发行版的状态变量名有差异以实际 SHOW STATUS 输出为准 SHOW STATUS LIKE Threadpool%; -- 重点关注三类值当前工作线程数、空闲线程数、被分配的连接数 -- 如果工作线程数长期贴着上限、空闲线程数为 0说明线程组已经跑满这几个值的意义在于把“线程池有没有生效”从感觉变成数字。工作线程数贴上限、队列在涨说明该调 thread_pool_size 或者优化查询工作线程数远低于上限却 TPS 上不去瓶颈大概率不在线程模型要去查锁、网络或存储。3.2 thread_pool_size 该不该等于 CPU 核数配置里最常见的写法是这样[mysqld] thread_handling pool-of-threads thread_pool_size 48 thread_pool_stall_limit 500 thread_pool_oversubscribe 3thread_pool_size 是线程组数量不是一个组里的线程数。每个组同一时刻最多有一个工作线程在跑在常见的 MariaDB/Percona 实现里组内串行执行是默认行为目的是减少锁竞争。这就解释了为什么推荐把它设成 CPU 核数48 核机器配 48 个组理论上最多 48 个工作线程并行每个核一个调度开销最小。设得太大组多了但 CPU 核数不够线程还是要抢核设得太小CPU 用不满。压测报告里有一个细节值得注意线程池后单库 400 线程压测TPS 250274CPU 67%而线程池后四库 100 线程TPS 171907CPU 69%。单库反而比四库 TPS 高很可能是因为单库场景线程组集中在一台实例上组内调度和缓存局部性更好四库场景连接分散每组负载不均监听线程分派成本更高。这也提示一件事thread_pool_size 的取值要结合部署拓扑看不是抄一个 48 就完事。3.3 thread_pool_stall_limit慢查询的“后悔药”还是“遮羞布”stall_limit 的单位是毫秒含义是如果一个工作线程执行一个请求超过这个时间还没返回监听线程认为它卡住了会采取动作——通常是唤醒或创建额外的工作线程避免队列里的请求被一个慢 SQL 堵死。这个参数是个双刃剑设得太小正常的长查询会被误判为卡住线程数被频繁拉起失去线程池的意义设得太大慢查询真的堵住时补救不及时队列积压响应时间飙升。500ms 是个常见的折中值但具体要看业务 90% 响应时间的分布。它和 long_query_time 不是一回事long_query_time 决定一条 SQL 要不要记进慢查询日志stall_limit 决定一条语句卡住多久后线程池要介入。前者是观测后者是调度。从测试数据看线程池后单库 90% 响应时间从 ≤1ms 变成 ≤2ms这个变化很可能和 stall_limit 有关——并发到 400 时部分请求排队等待的时间被计入了响应时间。这不是线程池变慢了而是排队成本从操作系统调度层转移到了应用层队列里变得可观测了。4. 压测方案复现从 JMeter 脚本到指标采集的链路4.1 测试环境的等价性48 核、四台负载机与四个库测试环境是 48 核 755G 内存的 MySQL 实例一台四台同配置负载机。这个配置的关键点是负载机数量和数据库核数的配比——四台 48 核机器打一台 48 核数据库施压侧不会成为瓶颈测出来的才是数据库的上限。基础数据量client_app 20 万app_ability 30 万ability_attr 120 万conf_attr 1 万。ability_attr 是被压的表120 万行在 755G 内存的机器上基本能全缓存所以 I/O wait 一直很低报告里反复出现“io 消耗很小”。注意如果你的生产数据量远大于内存这个测试结论不能直接搬。缓存命中率不同线程池带来的收益结构也会不同读磁盘的比例上来之后瓶颈会从线程模型转到 I/O 子系统。测试结果的关键数据整理成一张表后面分析时会反复用到场景并发TPS90% 响应时间CPU成功率线程池前·四库80129871≤1ms69%100%线程池前·单库80131676≤1ms68%100%线程池后·四库100171907≤1ms69%100%线程池后·单库400250274≤2ms67%100%线程池后·四库稳定性5295000≤1ms60%100%4.2 阶梯加压与停止条件测试步骤里有一个很实在的设计从 40 个请求流起步稳定 5 分钟再加 40每次加完观察指标。停止条件是 CPU、内存平均使用率 ≤70%I/O wait ≤30%网络带宽 ≤70%成功率 ≥99.99%90% 响应时间 ≤100ms任一不满足就停定位问题。这套停止条件把“压到挂”变成了“压到边界”。一味加并发最后只得到一个崩溃点得不到拐点拐点才是容量规划的输入。阶梯加压的另一个细节是每档稳定 5 分钟。5 分钟足够让连接池稳定、缓存进入稳态避免把冷启动的抖动当成性能拐点。压测里最常见的误判就是把“预热阶段 TPS 低”记成“系统容量不足”然后回头去调参数方向从一开始就错了。4.3 JMeter 命令行执行与结果分析# 非 GUI 模式执行-n 表示不启动界面-t 指定脚本-l 指定结果文件 ./jmeter -n -t ability_attr_query.jmx -l logs/run_20220418.jtl # 把 jtl 结果转成 HTML 报告-g 指定输入-o 指定输出目录目录必须不存在或为空 ./jmeter -g logs/run_20220418.jtl -o report_20220418参数含义-n 关闭 GUI避免施压机自身界面消耗资源-t 指定 .jmx 脚本-l 指定结果输出-g 和 -o 用于离线生成报告。注意 -o 指向的目录不能有旧文件否则命令直接报错。报告里的聚合报告、响应时间趋势图、TPS 曲线三张图要一起看只看 TPS 会漏掉响应时间恶化只看响应时间会漏掉吞吐上限。线程池后单库 400 线程TPS 250274 但 90% 响应时间 ≤2ms说明还有余量如果响应时间已经接近 100ms 阈值即使 TPS 更高也不该继续加。4.4 12 小时稳定性窗口看什么稳定性测试用 52 线程压 12 小时TPS 95000 左右CPU 60%成功率 100%。这个测试的目的不是找上限是找“长时间跑会不会崩”。12 小时窗口里要盯的曲线和短时压测不同短时看 TPS长时看内存 RSS 是否单调上涨、线程数是否稳定、连接数是否泄漏、错误率是否随时间抬升。线程池场景下还要多看一条线程组的工作线程数是否反复震荡。震荡说明 stall_limit 设置和业务查询时长不匹配工作线程被频繁拉起又回收稳定性测试期间 CPU 曲线会出现周期性毛刺。稳定性测试报告里那句“10.2.58.5 服务器其它应用一直占用 3% 的 CPU”也提醒一件事压测机的背景负载要记录否则最后分析时说不清那 3% 是谁的。5. 线程池压测避坑五条从现象到根因的排查记录5.1 并发加到 225 反而 CPU 掉到 60%现象线程池后单库测试150 并发时 TPS 240000、CPU 67%并发瞬间拉到 225、250 时CPU 反而降到 60%。原因线程组的工作线程已经跑满新增连接在组队列里排队没有真正进入执行阶段。CPU 掉是因为工作线程在等锁或等队列不是在算。这个现象说明系统已经过了拐点继续加压只会拉长响应时间。解决把并发停在拐点附近用 stall_limit 配合慢查询日志确认是否有长事务占住工作线程必要时增加 thread_pool_size 或拆库分摊。别把“加并发后 CPU 降了”当成好消息。5.2 GUI 模式和命令行模式跑出两个结果现象同一份 JMeter 脚本图形界面跑出来 TPS 比命令行低一截响应时间也更高。原因GUI 模式本身渲染监听器结果施压机 CPU 被吃掉一部分聚合报告等监听器还会在内存里累积样本样本量大时 GC 压力上来。解决正式压测一律用 -n 非 GUI 模式脚本里不加察看结果树结果落 jtl 后用 -g 离线生成报告。压测机的资源是测试预算的一部分不能让它先垮。5.3 四库和单库的 TPS 不能直接比现象线程池前四库 80 线程 TPS 129871单库 80 线程 TPS 131676几乎一样线程池后四库 100 线程 TPS 171907单库 400 线程 TPS 250274单库反而高。原因四库和单库是两个不同的变量连接分布、每库负载、跨库路由成本都不同。直接横向比会把“线程池效果”和“拓扑差异”混在一起。线程池前的接近说明当时瓶颈在接入层线程池后单库高说明单库场景下线程组调度更集中但也意味着单库更容易先到拐点。解决对比测试要固定拓扑只改线程池开关要比较拓扑差异单独设计一组实验别把两个变量塞进一张表。5.4 日志级别没改压测机先扛不住现象压测刚起步数据库 CPU 才 40%施压机 CPU 已经 90%TPS 上不去。原因应用日志级别还是 info 或 debug每次请求写盘磁盘 I/O 和序列化开销把施压机资源吃掉了。测试报告里专门有一条“代码改造将 log.level 改为 error”就是为了排除这个干扰。解决压测前把日志级别降到 error关掉不必要的拦截器和埋点多机分布式施压监控施压机自身的 CPU、网络、磁盘。压测报告里“代码改造”那一节看起来最不起眼但漏掉它后面所有数据都白测。5.5 I/O wait 很低TPS 却上不去现象iostat 显示 I/O wait 只有个位数但 TPS 卡在某个值不动加并发也没用。原因瓶颈不在磁盘。可能是网络带宽打满、连接数到上限、行锁等待或者线程池队列积压。测试报告的停止条件同时约束 CPU、I/O wait、网络带宽三项就是为了避免只盯一个指标误判。解决把网络流量、Threads_running、Innodb_row_lock_waits、线程池队列长度一起拉出来看用 perf 或 pt-pmp 抓线程栈看工作线程到底卡在哪。单指标排查在数据库压测里基本等于盲人摸象。6. 线程池的取舍什么场景该开什么场景开了更糟线程池不是万能药。从测试数据看它在“高并发短查询”场景收益明显线程池后四库 TPS 从 129871 涨到 171907提升约 32%CPU 没变单库场景把并发拉到 400 才压出 250274 的 TPS说明线程池把可承载的连接数上限抬高了。但换个场景结论可能反过来。第一个取舍是查询时长分布。线程池假设请求是短的工作线程快速轮转。如果业务里长查询占比高工作线程会被长时间占住stall_limit 拉起额外线程线程数反而膨胀退化成接近默认模型还多了一层队列开销。这类场景要么优化 SQL要么给长查询单独走连接池不要和短查询混在一个线程组里。第二个取舍是连接数。线程池的价值在连接数远大于 CPU 核数时体现。如果业务连接数本来就只有几十默认模型足够开线程池只是多一层调度。48 核配 48 个组是一个比较标准的起点实际值要按 CPU 核数和查询特征调。第三个取舍是可观测性。线程池把排队从内核挪到了应用层响应时间里多了一段队列等待。如果监控只看数据库内部指标会漏掉这段等待。需要把线程池队列长度、活跃工作线程数、stall 次数纳入监控否则出了问题只能靠猜。验证线程池是否值得开我一般会做三组对照同并发下开和关的 TPS 与 90% 响应时间逐步加压找开池后的拐点12 小时稳定性看内存和线程数是否漂移。三组都过才敢上生产。这份压测文档的价值不在于那几个 TPS 数字而在于它把测试目标、停止条件、结果记录三件事固定成了模板换成自己的库名和并发梯度就能跑。有个教训我记了很久早年做压测只盯着 TPS 曲线往上冲忽略了 90% 响应时间已经在悄悄爬。等发现时线程池的队列已经积压TPS 数字好看但业务侧超时率飙升。从那以后我每次压测都强制把 TPS、响应时间、错误率三条线画在同一张图上任何一条拐头就停。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。