ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

MySQL线程池插件高并发性能压测方案与结果对比

MySQL线程池插件高并发性能压测方案与结果对比

简介:面向 MySQL 数据库管理员、运维与后端开发者的性能优化测试文档,围绕高并发请求下线程频繁创建销毁带来的资源开销与性能瓶颈,给出线程池插件的工作原理说明、安装配置思路以及一套完整的压测方案。压缩包内共 1 个 PDF 文件,约 3.26MB,内容按正式测试报告体例编排:从概述、术语与测试依据,到测试计划、测试目标、测试环境(含网络拓扑、应用系统架构、风险点分析与软硬件配置),再到测试数据准备、代码改造与工具选型,章节层次清晰,便于按需检索。正文重点展开新增线程池前后的四库负载、单库负载与长时间稳定性测试,配合 QPS、TPS、响应时间等指标对比与结果分析,并给出人力安排与时间进度预估,可据此复现压测流程、定位瓶颈并评估线程池上线后的实际收益。已有 446 人学习下载,适合需要落地高并发数据库优化的中高级技术人员参考。

1. 高并发下数据库先扛不住,问题往往不在 SQL 本身

现网数据库一到高并发就出现响应抖动,第一反应通常是去查慢查询日志、加索引、拆表。但真到压测环节会发现,SQL 执行计划已经很干净,瓶颈卡在连接数一上去、线程创建和销毁的开销就把 CPU 吃掉了。MySQL 新增线程池插件,解决的正是这个层面的事:用预先创建、循环复用的线程组去承接请求,而不是每个连接都拉一个新线程。

这套方案适合两类人:一类是要交一份完整的数据库性能压测方案、毕设或课设的同学,需要一套从测试目标、环境配置、用例设计到结果对比的完整骨架;另一类是从业者,手里有 MySQL 实例要做线程池上线前的效果验证,想要一份能直接改参数就用的测试方案。下面按需求、设计、实现、避坑、验收的顺序,把这份方案拆开讲透。

2. 方案总体设计:从测试目标到用例矩阵的四层结构

一份能落地的性能测试方案,不是一上来就贴 JMeter 脚本。它得先把「验证什么」定死,再往下推环境、数据、用例。原始文档的结构其实很清晰:术语定义、测试依据、测试计划、测试目标、测试环境、测试准备、测试类型、结果记录、风险问题。我一般会把它归成四层,每层解决一个交付问题。

2.1 指标层:把「性能好」翻译成可判定的数字

方案里最关键的一张表是性能测试目标表,它决定了后面所有用例的通过与否。原始方案给出的判定线是:CPU、内存平均使用率 ≤70%,I/O wait ≤30%,网络带宽不超过 70%,90% 响应时间 ≤100ms,业务成功率 ≥99.99%。这几条不是拍脑袋,是给压测过程设的「刹车线」——任何一条破了就停止加压,转去定位问题,而不是继续往上怼并发。

指标判定阈值观测手段超标后的动作
CPU 平均使用率≤70%zabbix、top停止加压,查热点 SQL 与线程状态
内存平均使用率≤70%zabbix、free排查连接堆积与缓冲池配置
I/O wait≤30%iostat、vmstat确认是否刷脏页或全表扫描
网络带宽≤70%网卡流量监控核对请求体大小与结果集大小
90% 响应时间≤100msJMeter 聚合报告定位慢请求分布
业务成功率≥99.99%JMeter 错误率抓错误样本与数据库错误日志

这里有个容易被忽略的点:阈值要和被测业务的 SQL 形态匹配。方案里被测语句是按主键加属性编码查ability_attr表,属于典型的点查,所以 90% 响应时间定在 100ms 是合理甚至偏宽松的;如果换成范围扫描或聚合统计,同一条线就会误判成「性能不达标」。定指标时最好把被测 SQL 原文一并写进方案,别只写接口名。

2.2 环境层:单实例配置与压测机配比

原始方案的硬件配置是 MySQL 单实例 48 核 755G 内存,四台负载机同样是 48 核 755G。这个配比透露了一个设计意图:压测机不能成为瓶颈。数据库侧 48 核意味着线程池的线程组数量有充足的调度空间,而四台压测机是为了在需要 400 并发时还能保持发压端不吃满。

方案里对环境的描述还有几处必须写进文档的细节:网络拓扑暂缺、应用架构暂缺、风险点分析暂缺。这三项在正式交付里不能空着,通常会补成「压测机与数据库同机房直连,排除跨网段干扰」「应用层只保留数据库访问链路,去掉缓存与消息队列,避免干扰源」「风险点是单实例压测结论不能直接外推到主从架构」。测试环境不模拟生产,结论就没有迁移价值,这一点在答辩时几乎必被追问。

2.3 数据层:基础数据量与参数化策略

方案给的基础数据量是client_app20 万、app_ability30 万、ability_attr120 万、conf_attr1 万。这个量级的设计逻辑是让索引树有足够深度、Buffer Pool 不能全量缓存,从而压出真实的磁盘与内存交互,而不是全部命中内存跑出一个虚高的 TPS。

参数化方面,被测 SQL 里用了${__property(pkid)}和${__property(attrCode)}两个属性变量,说明主键和属性编码都是从外部属性文件读入的。我一般会这样组织参数文件,保证每次请求打到不同行,避免同一行被反复命中缓存:

# 生成参数化数据:主键与属性编码笛卡尔组合,写入 jmeter 属性文件 python3 gen_params.py --rows 200000 --out params.csv # params.csv 前几行形如 # pkid,attrCode # 100001,ATTR_0001 # 100002,ATTR_0002

生成脚本要保证主键落在表实际存在的区间内,否则大量请求查空,成功率看着是 100%,实际什么都没压到。这一点在结果分析阶段特别容易翻车:TPS 高得离谱,一查全是空结果直接返回。

2.4 用例层:四种组合覆盖线程池前后差异

方案的核心设计是把测试拆成 2×2 的矩阵,再加一组稳定性与一组线程组数对比:

  • 线程池前 + 四库负载
  • 线程池前 + 单库负载
  • 线程池后 + 四库负载
  • 线程池后 + 单库负载
  • 线程池后 + 12 小时稳定性
  • 线程池后 + 不同线程组数对比

「四库」和「单库」的区分是为了看线程池在多实例分摊压力时的表现差异;「前后对比」是方案的主线,没有基线数据,线程池的效果就无从谈起。这个矩阵看似简单,但它把「改造有没有用」和「改造后在什么条件下有用」两个问题同时回答了,比只跑一组前后对比要有说服力得多。

3. 线程池插件配置与压测脚本落地

设计定完之后,落到具体动作就三件事:把线程池参数配对、把 JMeter 脚本跑通、把结果采集链路打通。这三件事任何一环出错,后面的数据都白跑。

3.1 线程池参数:三个配置项决定调度行为

线程池的核心参数不多,但每一个都会改变压测结果的形状。配置通常写在my.cnf的[mysqld]段:

[mysqld] # 启用线程池调度模式,替代默认的一连接一线程 thread_handling = pool-of-threads # 线程组数量,决定并行调度粒度,通常不超过 CPU 核数 thread_pool_size = 32 # 线程停顿上限(毫秒),超过后调度器会认为该组被阻塞并补充线程 thread_pool_stall_limit = 500

thread_pool_size是最需要反复试的参数。原始方案在 48 核机器上做过 32 个线程组的对比测试,结论是 150 并发时 TPS 到 24 万、CPU 67%,继续加并发 TPS 和 CPU 涨幅都不明显;并发瞬间冲到 225、250 时 CPU 反而回落到 60%。这个现象说明线程组数接近饱和后,多出来的连接只是在排队,不产生额外吞吐。所以thread_pool_size不是越大越好,一般从 CPU 核数的二分之一到相等之间取值做对比,找出拐点。

thread_pool_stall_limit影响的是阻塞判定。值太小,短事务会被误判为卡住,调度器频繁补线程,等于把线程池退化成线程风暴;值太大,真卡住的组迟迟不补充,排队延迟上来。点查场景 500ms 是常见起点,长事务场景要往上调。

提示:修改线程池参数必须重启实例才生效,压测前先确认参数已加载,别拿默认配置跑完一整轮才发现没生效。

3.2 JMeter 脚本:命令行跑压测与生成 HTML 报告

方案给的操作路径很实用:先在本地图形界面调脚本,再把整个目录传到负载机,用非 GUI 模式跑。图形界面本身吃资源,几百并发下用它跑出来的数据不可信。

# 进入 jmeter 的 bin 目录,非 GUI 模式执行脚本 cd /opt/apache-jmeter-3.3/bin # -n 非 GUI 模式,-t 指定脚本,-l 指定结果文件 ./jmeter -n -t ability_attr_query.jmx -l logs/run_400.jtl # 由结果文件生成 HTML 报告目录 ./jmeter -g logs/run_400.jtl -o report_400

第一条命令里-l指定的.jtl是原始采样记录,每一笔请求的响应时间、状态码都在里面,后面排查错误样本要靠它。第二条命令的-o目录必须是空目录,否则会直接报错退出,这个坑第一次用基本都会踩。生成的report_400/index.html打开就是聚合报告、响应时间趋势、TPS 曲线,方案里那些截图就是从这来的。

阶梯加压在脚本里通过线程组的 Ramp-up 和循环次数控制,对应方案里「先 40 个请求流跑 5 分钟,再加 40 个」的节奏。实际操作中我会把阶梯写进线程组的调度器,让加压速率稳定,而不是手动一轮轮改数字。

3.3 监控与采集:资源数据要和压测数据对齐时间轴

JMeter 只告诉你请求侧发生了什么,CPU、内存、I/O wait 得从数据库侧拿。方案用的是 zabbix 加 JvisualVM 加 Linux 命令的组合,其中 zabbix 负责长期趋势,top、iostat、vmstat负责现场取证。

# 每 2 秒采样一次,记录 CPU、内存、IO 与上下文切换 vmstat 2 300 > vmstat_before.log # 单独看磁盘 IO 等待与每秒读写 iostat -x 2 300 > iostat_before.log

关键在于时间对齐:压测开始时间和监控采样开始时间要记录到同一份记录里,否则事后对不上,就会出现「TPS 明明涨了但 CPU 曲线看着没动」的错觉。我通常会在压测脚本启动前 5 分钟就开始采样,和方案里写的一致,这样能拿到干净的基线区间。

3.4 稳定性测试:12 小时不是熬时间,是看趋势

稳定性用例的设计是 52 线程持续 12 小时,判定标准是 TPS 不低于负载测试得到的最优值、成功率不低于 99.99%、资源不越线。这里真正要看的是曲线斜率:内存是否缓慢爬升(线程或连接泄漏)、TPS 是否随时间衰减(锁竞争累积或统计信息过期)、I/O wait 是否阶段性抬升(刷脏页节奏变化)。

12 小时跑完只看一个汇总数字是没有意义的。我的做法是每小时切一段,算这一小时的 TPS 均值、P99 响应时间和 CPU 均值,做成一张小时级对比表。只要出现某小时后指标系统性下滑,就说明存在随时间累积的问题,这时候再去看线程池的队列长度和SHOW PROCESSLIST的历史采样。

4. 结果对比:线程池前后四组数据的解读方法

方案第 9 章给了完整的实测数据,这部分是整份文档最有参考价值的地方,因为它同时提供了不加线程池和加线程池的基线。把数据摆在一起看,才能理解线程池到底改变了什么。

场景并发线程持续时长TPS90% 响应时间成功率CPU 使用率
线程池前,四库80300s129871≤1ms100%69%
线程池前,单库80300s131676≤1ms100%68%
线程池后,四库100300s171907≤1ms100%69%
线程池后,单库400300s250274≤2ms100%67%
线程池后,四库稳定性5212h约 95000≤1ms100%60%

4.1 吞吐提升与并发承受力的分离观察

四库场景下,线程池前 80 并发 TPS 约 13 万,线程池后 100 并发 TPS 约 17.2 万,提升约 32%,CPU 基本持平在 69%。这说明同样资源下,线程池把更多 CPU 用在了处理请求而不是线程调度上。

单库场景的差距更值得注意:线程池前 80 并发约 13.2 万,线程池后直接拉到 400 并发、TPS 约 25 万,90% 响应时间从 ≤1ms 变成 ≤2ms,CPU 还降了一点到 67%。这组数据的含义是线程池把并发承受能力放大了五倍,代价只是响应时间翻了一倍但仍然在 2ms 量级。写方案时如果不把这个「并发上限提升」和「吞吐提升」分开讲,读者会误以为线程池只是简单提速。

4.2 稳定性数据的解读陷阱

稳定性测试 52 线程跑 12 小时,TPS 约 9.5 万,比负载测试的最优值低不少。这不是回退,而是稳定性测试选的是「能长期跑住」的容量,不是峰值容量。方案里还有一个补充说明:服务器上其它应用一直占着 3% 的 CPU,这 3% 在做资源核算时要扣掉,否则会高估数据库自身的消耗。

注意:稳定性测试的并发数要低于负载测试的峰值,否则跑不出 12 小时就已经在临界点上抖动了,得到的不是稳定性结论而是崩溃时间。

5. 避坑清单:压测方案最容易翻车的五个点

5.1 参数改了但没重启,跑了一整轮默认配置

现象是压测结果和「线程池前」几乎一样,怎么调并发 TPS 都不动。原因是thread_handling和thread_pool_size属于需要重启生效的参数,只改了配置文件没重启实例。解决办法是压测前先执行查询确认当前生效值,把确认结果截图存档,作为方案附件。

5.2 参数化数据全部命中同一批行

现象是 TPS 异常高、CPU 却很闲。原因是主键参数化时取值范围过窄,或者生成脚本造出来的主键大量不存在,请求直接返回空结果。解决方法是压测前抽样统计参数文件里的主键在表中真实存在的比例,同时对被测 SQL 做一次执行计划确认,确保走的是索引点查而不是全表扫描。

5.3 压测机先到瓶颈,误判为数据库到顶

现象是并发加到一定程度 TPS 不涨、数据库 CPU 也没满。原因是发压端 CPU 或网络先吃满,请求根本发不出去。解决办法是加压过程中同步盯压测机的 CPU 与网卡流量,方案里配四台负载机就是为了给并发留余量;一旦发压端利用率超过 70%,就该增加压测机而不是继续调数据库。

5.4 日志级别没降,磁盘 I/O 被日志吃掉

方案里专门写了一项代码改造:把日志级别从默认调到 error。原因是高并发下每笔请求都打日志,I/O wait 会明显抬升,压出来的瓶颈是日志而不是数据库。解决办法是把这项改造写进测试准备的检查清单,压测前后各确认一次配置。

5.5 只比峰值不比曲线,结论站不住

现象是报告里只有一张汇总表,答辩时被问「12 小时里有没有衰减」答不上来。原因是只记录了最终汇总值,没保留过程数据。解决办法是保留.jtl原始文件,按小时切片做趋势对比,把内存、TPS、响应时间三条曲线一起放上去。

6. 验收与答辩:用一张对照表把结论讲死

这类方案交付时,评审最想看的不是跑了多少轮,而是「线程池到底值不值得上,在什么条件下值得上」。我通常会在答辩材料的第一页放一张前后对照表,只保留四列:场景、并发能力、TPS、资源占用。表下面接一句话结论,比如「单库场景下并发承受能力从 80 提升到 400,TPS 提升约 90%,CPU 占用下降 1 个百分点,代价是 90% 响应时间从 1ms 升至 2ms,仍在阈值内」。

答辩被追问最多的三个问题,提前准备好答案基本就稳了。第一个是「测试环境的结论能不能外推到生产」,答案要落在环境差异上:单实例压测没有覆盖主从复制延迟和连接池中间件,生产上线前需要补一轮带完整链路的验证。第二个是「线程组数为什么定 32」,答案要拿对比数据说话,32 组在 150 并发时已接近饱和,继续加并发 TPS 和 CPU 涨幅不明显,说明再增加线程组收益递减。第三个是「稳定性测试的 TPS 为什么比负载测试低」,答案就是容量选型逻辑:稳定性取的是能长期跑住的容量,不是峰值。

验收标准我一般会写成可勾选的条目,评审逐条确认,避免口头通过:

[ ] 线程池前后各四组负载用例数据完整,含 .jtl 原始文件 [ ] 每组用例记录 CPU、内存、I/O wait、网络带宽四项资源数据 [ ] 稳定性测试覆盖 12 小时,含小时级趋势对比 [ ] 线程组数对比测试至少覆盖两组参数 [ ] 所有超标或异常项在「风险和问题」章节有对应说明 [ ] 参数生效状态、日志级别、数据量三项有截图或记录佐证

最后说个血泪经验。早些年做类似的前后对比压测,我把注意力全放在 TPS 上,资源数据只截了一张top图,结果评审问「I/O wait 在加压过程中有没有抬升」时我拿不出证据,只能重跑一轮。从那以后我每次压测都强制走一遍「压测前确认参数生效、压测中同步采样资源、压测后保留原始文件」这三步,不管工期多紧都不省。希望这套结构能帮到你,照着填自己的数据和参数,就是一份能交得出去的完整方案。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进