简介:面向数据库运维与性能测试人员的高并发优化实践资料,聚焦MySQL线程池插件在现网性能瓶颈下的落地评估。内容围绕线程池工作原理、安装配置参数(如thread_pool_size、thread_handling)展开,并给出基准测试、压力测试、稳定性测试与负载测试的完整策略,涵盖测试环境搭建、软硬件配置、测试数据准备、代码改造及sysbench等工具选型,同时包含人力安排与时间规划建议。资源包共1个PDF文件,约3.26MB,为一份结构完整的性能测试报告,目录覆盖测试计划、目标、环境拓扑、风险点分析,以及新增线程池前后四库与单库负载测试、稳定性测试等章节,便于读者对照复现测试流程、评估插件带来的吞吐与延迟变化。已有443人学习下载,适合需要制定数据库压测方案或验证线程池收益的工程师参考。
1. 一次压测翻车引出的线程池插件安装
去年帮某公司做一次数据库性能压测,48 核 755G 内存的机器,单库 80 线程跑ability_attr查询,TPS 卡在 13 万上不去,CPU 却只到 68%。当时第一反应是压测机不够,加了负载机、调了 JMeter 参数,结果纹丝不动。后来 DBA 提醒一句:MySQL 默认是 one-thread-per-connection 模型,高并发下线程创建销毁的开销全压在系统调用上,瓶颈根本不在 SQL。这才把 MySQL 线程池插件翻出来重新装了一遍,装完单库 400 线程 TPS 直接到 25 万。这篇笔记就把这套「装插件、配参数、压测验证」的完整过程记下来,适合正在被高并发连接数折磨、想上线程池又怕装坏数据库的开发者照着走。
2. 装前先定版本:线程池插件与 MySQL 的匹配关系
2.1 先搞清楚你手上的 MySQL 是不是「带池」的版本
线程池插件不是所有 MySQL 发行版都自带。社区版官方二进制包默认不带thread_pool插件,企业版和部分 Percona 分支才内置。我一般先跑一条命令确认,别急着下载:
# 登录 MySQL 后执行,看插件目录里有没有线程池相关 so 文件 mysql -uroot -p -e "SHOW VARIABLES LIKE 'plugin_dir';" ls $(mysql -uroot -p -N -e "SELECT @@plugin_dir;") | grep -i thread如果输出里能看到thread_pool.so(Linux)或thread_pool.dll(Windows),说明插件文件已经在位,只差INSTALL PLUGIN。如果什么都没有,就得换带插件的发行包,或者从源码编译时加-DWITH_THREAD_POOL=ON。这一步是整篇笔记的分水岭——很多人卡在INSTALL PLUGIN报Can't open shared library,根因就是版本包里压根没编进去。
版本选择上,我倾向用 5.7.30 以上的 5.7 系列或 8.0.20 以上的 8.0 系列。5.7 早期版本线程池插件在高并发下有已知的 group 调度抖动,8.0 之后thread_pool_algorithm的默认值从 0 改成了 1,行为差异不小。原文压测环境没写具体小版本,但 2022 年 4 月的测试大概率落在 5.7.3x 或 8.0.2x,这两个区间装法一致,参数名也一致。
2.2 环境对照表:装之前把这几项对齐
| 项目 | 压测环境实测值 | 安装前要确认的点 |
|---|---|---|
| CPU | 48 核 | thread_pool_size不超过核数,一般取核数的 1/2 到 1 倍 |
| 内存 | 755G | 线程池本身不吃内存,但thread_pool_size过大反而增加上下文切换 |
| MySQL 版本 | 5.7.3x / 8.0.2x | 确认插件文件存在,SHOW PLUGINS里无 thread_pool |
| 连接模型 | one-thread-per-connection | 装完后thread_handling要改成pool-of-threads |
| 压测工具 | JMeter 3.3 | 负载机 4 台 48 核,脚本用.jmx命令行跑 |
这张表不是摆设。我见过有人在 4 核测试机上把thread_pool_size设成 64,结果线程组之间抢锁比不装还慢。线程池的收益来自「复用 + 分组调度」,核数不够时分组反而成了负担,所以装之前先把 CPU 核数和预期并发量对一遍。
2.3 装插件前必须做的两件事:备份与停写窗口
线程池插件是动态加载的,理论上INSTALL PLUGIN不需要重启,但thread_handling从one-thread-per-connection切到pool-of-threads的瞬间,现有连接的处理方式会变。生产库上我一般选低峰期,先做逻辑备份再动手:
# 备份当前参数快照,出问题能对照回滚 mysql -uroot -p -e "SHOW VARIABLES LIKE 'thread%';" > /tmp/thread_vars_before.txt # 逻辑备份,至少把核心库表结构导一份 mysqldump -uroot -p --single-transaction --routines --triggers your_db > /tmp/your_db_before_pool.sql--single-transaction保证 InnoDB 表在备份期间不锁表,--routines --triggers把存储过程和触发器一起带走,回滚时不用再翻 binlog。备份完把thread_vars_before.txt留着,后面调参调乱了直接对照原始值改回去,比凭记忆靠谱。
3. 安装线程池插件:从 INSTALL PLUGIN 到参数落盘
3.1 动态加载插件并确认状态
插件文件在位的前提下,登录 MySQL 执行安装:
-- 加载线程池插件,注意 so 文件名不带路径,MySQL 会去 plugin_dir 找 INSTALL PLUGIN thread_pool SONAME 'thread_pool.so'; -- 确认插件状态是 ACTIVE SELECT PLUGIN_NAME, PLUGIN_STATUS, PLUGIN_TYPE FROM INFORMATION_SCHEMA.PLUGINS WHERE PLUGIN_NAME = 'thread_pool';SONAME后面只写文件名,不要写绝对路径,MySQL 自己拼plugin_dir。如果报ERROR 1126 (HY000): Can't open shared library,九成是文件名不对或权限问题——plugin_dir目录对mysql用户必须可读可执行。装成功后PLUGIN_STATUS显示ACTIVE,但此时线程模型还没切,只是插件挂上了。
3.2 切换 thread_handling 并设置线程池核心参数
插件加载后,真正生效要改thread_handling。原文给出的配置项是thread_pool_size、thread_pool_stall_limit和thread_handling,我按 48 核环境给一组可抄的配置:
[mysqld] # 线程池分组数,48 核取 24,约为核数一半,兼顾并行与调度开销 thread_pool_size = 24 # 线程等待新任务的毫秒上限,超过则判定为 stall,触发新线程或调度补偿 thread_pool_stall_limit = 500 # 切换连接处理模型为线程池 thread_handling = pool-of-threadsthread_pool_size是分组数不是线程总数,每组内部再维护工作线程,所以 24 组在 48 核上不会把核跑满,留出余量给后台线程和系统调用。thread_pool_stall_limit设 500ms 是原文值,这个参数调小会让调度更激进、短查询响应更快但上下文切换增多,调大则长查询更容易占住分组。我一般先用 500 跑基准,再按 90% 响应时间微调。
改完my.cnf后,动态生效可以不走重启:
SET GLOBAL thread_pool_size = 24; SET GLOBAL thread_pool_stall_limit = 500; SET GLOBAL thread_handling = 'pool-of-threads';SET GLOBAL只影响新连接,已有连接仍走旧模型,所以压测前要断开重连或重启实例。落盘到my.cnf是为了重启后不丢,两边都要做,别只改内存。
3.3 验证线程池是否真的接管了连接
装完别急着压测,先确认线程模型切过去了:
-- 看当前 thread_handling 值 SHOW VARIABLES LIKE 'thread_handling'; -- 看线程池运行状态,group 数量应与 thread_pool_size 一致 SHOW STATUS LIKE 'Threadpool%';Threadpool_group_count应该等于你设的 24,Threadpool_threads是当前工作线程数,空闲时会低于分组数。如果thread_handling还是one-thread-per-connection,说明SET GLOBAL没生效或实例没重连,这时候压测数据全是假的。我习惯再补一条SHOW PROCESSLIST,看连接是否被归到线程池分组里,虽然输出不直接标 group,但连接数暴涨时线程数不跟着线性涨,就是池子在起作用。
4. 压测配置:JMeter 脚本与线程梯度怎么设
4.1 JMeter 命令行跑压测的标准姿势
原文用的是 JMeter 3.3,脚本在 Windows 上编好.jmx再传到 Linux 负载机跑。这套流程我照搬,命令如下:
# 进入 JMeter bin 目录,非 GUI 模式执行脚本,结果写 jtl cd /opt/apache-jmeter-3.3/bin ./jmeter -n -t query_ability_attr.jmx -l logs/pool_after.jtl # 压测结束后生成 HTML 报告到指定目录 ./jmeter -g logs/pool_after.jtl -o report_pool_after-n是非 GUI 模式,压测必须用它,GUI 模式本身吃资源会污染结果。-t指定脚本,-l是结果日志,-g加-o把 jtl 转成 HTML 报告,打开report_pool_after/index.html就能看聚合报告和响应时间趋势。注意-o指定的目录必须不存在或为空,否则 JMeter 直接报错退出,这是新手最常踩的一个小坑。
4.2 线程梯度:从 40 起步,每轮加 40
原文的压测步骤是「40 个请求流稳定 5 分钟,再加 40,以此类推」,这个梯度设计是为了找拐点。JMeter 里对应的是线程组Number of Threads配合Ramp-Up和Duration:
<!-- 线程组关键参数,40 线程、5 分钟持续、循环由调度器控制 --> <ThreadGroup> <stringProp name="ThreadGroup.num_threads">40</stringProp> <stringProp name="ThreadGroup.ramp_time">10</stringProp> <boolProp name="ThreadGroup.scheduler">true</boolProp> <stringProp name="ThreadGroup.duration">300</stringProp> </ThreadGroup>ramp_time设 10 秒让 40 个线程均匀起来,避免瞬间冲击造成假拐点;duration300 秒对应原文的 5 分钟稳定运行。每轮跑完把num_threads加 40,直到 CPU 超 70%、I/O wait 超 30% 或 90% 响应时间破 100ms 就停。这套梯度在装池前后各跑一遍,对比才有意义——原文装池前四库 80 线程 TPS 129871,装池后 100 线程到 171907,单库更是从 131676 冲到 250274,差距主要来自线程复用省下的创建销毁开销。
4.3 监控项对齐:CPU、I/O wait、网络带宽三件套
压测时监控工具原文列了 zabbix、JvisualVM 和 Linux 命令,我实际用最多的是vmstat和sar,轻量且不依赖 agent:
# 每 5 秒采样一次,重点看 us、sy、wa 三列 vmstat 5 60 # 网络带宽按秒统计,对应原文「网络带宽不超过 70%」的判据 sar -n DEV 5 60vmstat的wa列就是 I/O wait,超过 30 说明磁盘成了瓶颈,这时候加线程只会更糟;sy列高说明系统调用多,正是线程池要压下去的部分。sar -n DEV看rxkB/s和txkB/s,换算成带宽占比判断是否触顶。这三项和 JMeter 的 TPS、90% 响应时间放一起看,才能判断是「池子没生效」还是「池子生效了但别的资源先到顶」。
5. 避坑与排查:线程池装完不生效的五种现场
5.1 现象:INSTALL PLUGIN 报 Can't open shared library
原因基本是插件文件不在plugin_dir,或者文件名大小写不对。Linux 下thread_pool.so和Thread_Pool.so是两个文件,SONAME必须和实际文件名完全一致。解决方式是先ls确认文件名,再原样填进SONAME;如果目录里根本没有这个 so,说明版本包不带插件,换包或重编译,别在权限上死磕。
5.2 现象:thread_handling 改了但 SHOW STATUS 里 Threadpool 全是 0
原因是SET GLOBAL只对新连接生效,压测机用的是连接池,旧连接一直没断。解决方式是压测前重启应用连接池或KILL掉旧会话,再确认Threadpool_group_count有值。我一般直接在压测前FLUSH STATUS再重连,避免旧计数干扰判断。
5.3 现象:装池后 TPS 不升反降,CPU 还更高
多半是thread_pool_size设得比核数还大,分组之间抢锁导致上下文切换暴涨。48 核设 24 是原文环境的合理值,4 核机器设 64 就是灾难。解决方式是把thread_pool_size降到核数一半左右,thread_pool_stall_limit从 500 往上试,观察vmstat的cs(上下文切换)列是否回落。
5.4 现象:稳定性测试跑几小时后 TPS 缓慢下滑
原文 12 小时稳定性测试 52 线程维持 95000 TPS,如果出现下滑,先看Threadpool_threads是否持续增长不回收。线程池本身有回收机制,但长事务会占住分组,导致新请求排队。解决方式是排查慢查询,把超过thread_pool_stall_limit的 SQL 揪出来优化,必要时给长查询单独走连接,别和短查询抢同一组。
5.5 现象:JMeter 生成 HTML 报告时报目录非空
-o指定的输出目录必须不存在或为空,重复跑同一目录会直接失败。解决方式是每次报告目录带时间戳,或者跑之前rm -rf清掉。这个坑不影响压测数据,但会让人误以为压测没跑完,白等一轮。
6. 装完怎么验证跑通:三条命令加一张对比表
验证线程池装没装好、有没有效果,我不看单点指标,走一套固定动作。第一步,确认插件和线程模型:
SELECT PLUGIN_NAME, PLUGIN_STATUS FROM INFORMATION_SCHEMA.PLUGINS WHERE PLUGIN_NAME='thread_pool'; SHOW VARIABLES WHERE Variable_name IN ('thread_handling','thread_pool_size','thread_pool_stall_limit'); SHOW STATUS LIKE 'Threadpool%';三条输出要能对上:插件ACTIVE、thread_handling是pool-of-threads、Threadpool_group_count等于设定值。对不上就回到第 5 章排查,别往下压。
第二步,跑一轮固定梯度的压测,装池前后各一次,把关键指标填进对比表:
| 指标 | 装池前(单库 80 线程) | 装池后(单库 400 线程) | 判据 |
|---|---|---|---|
| TPS | 131676 | 250274 | 明显上升 |
| 90% 响应时间 | ≤1ms | ≤2ms | 不破 100ms |
| 成功率 | 100% | 100% | ≥99.99% |
| CPU 使用率 | 68% | 67% | ≤70% |
| I/O wait | 很小 | 很小 | ≤30% |
这张表就是原文压测结论的浓缩版。装池后线程数翻五倍,TPS 接近翻倍,CPU 没涨,说明收益来自线程复用而不是堆资源。如果装池后 TPS 持平甚至下降,先别怀疑插件,回头查thread_pool_size和压测机瓶颈。
第三步,跑一轮短稳定性测试,至少 30 分钟,盯Threadpool_threads是否稳定、vmstat的wa和cs是否漂移。原文 12 小时稳定性测试维持 95000 TPS,日常验证跑 30 分钟能看出趋势就够了,真上生产前再补长跑。
从那以后我每次给数据库装线程池插件,都强制走一遍「插件状态 → 线程模型 → 梯度压测对比 → 短稳定性」这四步,少一步都不敢说装通了。线程池这东西装错不报错,压测数据一好看就容易放过,等生产上连接数一冲才暴露,那时候回滚成本就高了。希望这套流程能帮到你,少走一次我踩过的弯路。
本文还有配套的精品资源,点击获取