☰
MySQL生产环境压测:用sysbench建立性能基线
2026/10/21 7:38:02 网站建设 项目流程

这几年经手的 MySQL 实例越多,我越觉得“上线前先压一压”是绕不开的环节。生产环境下的 MySQL,日常监控面板可能一片绿,但 CPU 被打满时到底还能扛多少读多写少的请求?连接数从 300 涨到 600 之后,是延迟变高还是直接拒绝服务?这些问题不主动打一轮负载,很难提前说清楚。sysbench 压测就是回答这些问题最直接的做法。这篇接着“生产环境下的 MySQL”这个话头往下聊,重点放在 sysbench:怎么准备场景、怎么跑、怎么读结果、生产环境里有哪些压测的坑。适合正在做数据库巡检、性能调优的 DBA 和后端同学参考。

1. 为什么生产环境要先跑一轮 sysbench 压测

1.1 sysbench 为什么值得一直用

sysbench 是一款老牌基准测试工具,最早是给 Linux 文件系统做基准测试的,后来加入了数据库压测模块,尤其是 MySQL 的 OLTP 场景支持得最完善。很多人问过我用不用换更新的压测工具,我的回答一直是:先把手上的 sysbench 用明白。

理由很朴素。第一,它足够稳定,同一套脚本在不同时间、不同机器上跑出来的结果可对比性强,这是做性能基线的核心前提。第二,它自带一组 Lua 脚本,覆盖了点查、读写混合、更新索引、插入、删除这些常见访问模型,绝大多数业务场景不用自己写脚本。第三,它的资源开销很低,压测机上一两核就能啃下来,不会因为压测工具本身变成瓶颈。

sysbench 的价值不在于“压力越猛越好”,而在于它能用可控、可重复、可量化的负载,把 MySQL 在特定硬件配置下的吞吐上限和延迟特征打出来。这个上限就是所谓的基线。没有基线,后面改参数、换硬件、调 SQL,都成了凭空猜。

1.2 压测的本质是建立基线,不是跑个最高分

见过不少同学压测喜欢挑战极限,线程一路上到 512、1024,然后拿那个最高的 TPS 截图发出来。说实话,这种结果在生产环境里参考价值不大。生产环境不会给你一个固定并发数,也不会有压测这种整齐划一的访问模型。压测的真正目的是建立“可比较的基准线”。

我习惯把每次压测做成一次可复现的实验:固定压测机、固定被测实例、固定数据集大小、固定脚本和并发数,唯一变化的,是我想验证的那个变量,比如 MySQL 参数、磁盘类型、实例规格。这样一来,前后两次压测的差异就能归因到一个变量上,而不是被环境噪声干扰。

举个例子说明。有一次我为一个项目做了内存参数调整,把innodb_buffer_pool_size从 8G 调到 16G。如果只看业务监控,几乎看不出变化,因为平时并发没到那个份上。但用同一套 sysbench 脚本、同样的 8 张表 1000 万行数据、64 个线程跑 5 分钟,前后 TPS 差了接近 40%,P99 延迟也从 85ms 降到 52ms。没有这套压测基线,这个配置升级的效果就无法量化。

还有个容易被忽略的用途:容量规划。当业务方问“我们这台实例还能撑多久”时,最好的回答不是拍脑袋,而是拿出上一次压测的基线和当前业务峰值做对比。如果业务峰值 TPS 是 2000,而压测基线在 CPU 打到 70% 时是 6000,那余量就还有三倍;如果基线在 CPU 100% 时也只有 2500,那说明离极限不远了,扩容要提上日程。

1.3 跟其他压测工具怎么选

MySQL 生态里也不是只有 sysbench 一个选择。简单列一下我用过或者评估过的工具,方便大家按场景选。

工具适用场景主要特点
mysqlslap快速探测连接、并发简单查询MySQL 自带,部署零成本,但只能做简单负载,无法模拟复杂事务
tpcc-mysql模拟交易型 OLTP 业务更接近真实业务模型,但部署繁琐,数据生成慢,结果解释成本高
sysbench基准测试、性能回归、配置对比轻量、自带 Lua 场景、结果稳定,适合做长期基线

我的建议是:常规的压测基线、配置对比、新硬件评估,直接用 sysbench;如果业务是典型的订单交易系统,想模拟更贴近真实的事务组合,再考虑 tpcc-mysql,但也要有心理准备,单是准备数据就可能要跑一个晚上。

2. 压测前的地基:环境、数据、账号

2.1 压测机与被压实例的部署原则

很多人压测结果不准,不是 MySQL 的问题,而是压测机先扛不住了。sysbench 跑 128 线程时,压测机自身的 CPU、网卡、内存都会吃紧,尤其是网卡软中断和进程调度。如果压测机本身只有 2 核,那这里的 128 线程调度本身就要消耗大量 CPU,TPS 根本起不来,结果全被压测机的瓶颈稀释了。

我的部署经验是:压测机单独一台,至少和被压实例同规格的 CPU 核数,优先走内网万兆或至少千兆双网卡绑定。如果两者放在同一台物理机上,资源互相抢占,测出来的数据毫无意义。被压实例的 CPU、内存、磁盘要尽量保持“干净状态”,不要同时跑备份、大查询、大批量导入。

另外,sysbench 线程数并不是越多越好。线程数超过实例 CPU 核数后,上下文切换成本会快速上升,TPS 增长放缓甚至下降。压测时我一般先用 CPU 核数的一半起步,然后按 16、32、64、128 这样阶梯增长,直到 TPS 不再明显上升,就说明当前配置已经触顶。

2.2 sysbench 的几种安装方式

sysbench 1.0 是当前的主流版本,各种发行版仓库里基本都有。CentOS 系统上:

yum install epel-release yum install sysbench sysbench --version

Ubuntu / Debian 上更简单:

apt-get install sysbench sysbench --version

如果仓库里的版本太老,或者想用最新版,可以源码编译,源码在官方仓库可以直接拉取。编译依赖需要 autoreconf、libtool、mysql-devel 等,过程不复杂,但对大多数人来说没必要,用系统包管理装好的 1.0 版本足够完成生产压测了。

装完后要确认 Lua 脚本目录在哪,通常 CentOS 上是/usr/share/sysbench/,里面有oltp_read_write.lua、oltp_read_only.lua、oltp_write_only.lua、oltp_update_index.lua等脚本。后面所有命令都要引用这些脚本路径。

2.3 造数据用什么参数,为什么是这个量级

压测之前必须有一份与业务形态接近的数据。sysbench 的 prepare 阶段会建表并插入随机数据,最常用的是先建一个测试库和账号:

CREATE DATABASE IF NOT EXISTS sbtest; CREATE USER 'sbtest'@'%' IDENTIFIED BY 'sbtest123'; GRANT ALL PRIVILEGES ON sbtest.* TO 'sbtest'@'%';

然后执行 prepare:

sysbench /usr/share/sysbench/oltp_read_write.lua \ --mysql-host=127.0.0.1 --mysql-port=3306 \ --mysql-user=sbtest --mysql-password=sbtest123 \ --tables=8 --table-size=1000000 --threads=4 prepare

这里我特别想解释两个参数:--tables=8和--table-size=1000000。8 张表可以模拟多表访问,避免只压单表带来的锁粒度过于宽松的问题;每张表 100 万行,算上随机内容列和索引,单表差不多 200 到 300MB,总数据量约 2G 多。这个量级在大多数实例的 buffer pool 都能装下,跑出来的读场景会偏乐观,适合测 CPU 和内存层面;如果要测磁盘 IO,需要把数据量做大,做到 buffer pool 的 1.5 到 2 倍以上。经验值是:数据量等于缓冲池的 1.5 到 2 倍,会迫使一部分读落到磁盘,结果更能反映真实业务的换页行为。

prepare 阶段的线程数不用太高,我一般用 4 到 8,太高反而因为插入随机内容消耗 CPU 导致整体变慢。如果数据量特别大,比如要生成 10 亿行,记得提前估算磁盘空间,sbtest 库的数据文件加上索引,不会比业务库小太多。

2.4 生产环境压测的靶机选择:克隆节点与从库

生产环境不可能随便扛压,尤其主库背后还有一堆从库在同步,一条压测命令下去,binlog 量可能瞬间暴涨,把复制链路打崩。所以靶机的选择非常关键。

首选方案是克隆一个独立实例。MySQL 8.0 自带的 clone 插件可以从主库在线克隆一份全量数据到新实例,这个实例是完全独立的,自己维护 buffer pool,压测产生的负载不会影响源头实例。跑完直接销毁克隆实例,干净利落。如果版本还不支持 clone,可以考虑先用物理备份工具恢复一台临时实例,恢复完成后在这个临时实例上做压测。

如果实在只能用从库压测,我建议只压读场景,也就是oltp_read_only。写场景会在从库上产生额外的事务,这些事务不会进入中继日志,也不会再同步回主库,但从库本身的数据会多出压测写入的行。如果后续这个从库要顶上来当主库,这些压测数据就会变成脏数据,必须清理。

还有一种情况是业务允许短时停写,那可以在低峰期直接停掉应用改流量,用主库本机压测一轮,压完再切回。这个操作风险很高,我只有在扩容评估和参数大改动时才敢这么做,而且一定提前准备好回滚方案。

3. 压测场景拆解:别只套一条默认命令

3.1 读懂 sysbench 的基础参数

sysbench 的参数不算多,但每个都直接影响结果含义。我把最常用的整理成一张表。

参数作用建议值
--threads并发线程数从 CPU 核数一半起步,按 16/32/64/128 递增
--time测试时长(秒)至少 300 秒,短了没有稳态参考性
--events总请求数,0 表示不限制一般配--time使用,设 0 更灵活
--rate每秒事务数上限生产环境限流压测时使用,平时不设
--report-interval周期输出统计5 秒比较合适,能看到趋势
--percentile延迟分位设 99,重点看 P99
--rand-type随机分布类型uniform / zipf,热点场景用 zipf

有个细节要注意:--time和--events同时设置时,谁先达到就算谁结束。我一般用--time=300且不设 events,保证测试时间可控。--report-interval=5是每 5 秒输出一条中间统计,压测结束后能看到整个过程是稳定上升还是出现了明显波动,这对判断资源瓶颈很有帮助。

3.2 读、写、混合读写场景设计

很多人的压测只有一条命令:oltp_read_write。但生产环境里的业务模型差异很大,读多写少的服务、写多读少的管道、密集点查的网关,它们的瓶颈往往在不同的地方。我建议按下面这张表拆场景。

Lua 脚本访问模型典型业务
oltp_read_only纯 SELECT 查询读多写少的查询服务、报表查询
oltp_write_only纯写入事务日志采集、批量写入服务
oltp_read_write混合读写事务大多数互联网 OLTP 应用
oltp_point_select主键点查高频单行查询
oltp_update_index索引列 UPDATE热点数据更新
oltp_deleteDELETE 操作定时清理/过期数据删除

跑顺序也有讲究。我一般先跑oltp_read_write拿到整体水位,再按业务占比跑细分场景,比如读多写少就补一个oltp_read_only,看从库能冗余多少读流量。单一场景的作用是定位瓶颈:如果纯写场景 TPS 远低于混合场景,说明写入路径或刷脏能力有问题;如果纯读场景延迟高,可能要考虑 buffer pool 是否够大。

3.3 锁与热点:这几个 lua 脚本别忽略

默认的oltp_read_write数据访问是均匀随机的,也就是说每个线程大概均匀地打在不同的行上。但真实业务的访问往往高度集中,比如秒杀、爆款商品、热点账号,大量请求都打在极少数数据行上。这种访问模式对 MySQL 的锁机制是很大的考验。

sysbench 里可以用--rand-type=zipf来模拟热点访问。Zipf 分布会让一部分数据被访问得特别频繁,自然产生行锁竞争和间隙锁竞争。我跑过很多次对比,uniform 分布下 P99 延迟可能只有 30ms,切到 zipf 分布后直接飙到 200ms 以上。这个差距说明锁等待是延迟的主要来源,也提醒我们:如果业务明确存在热点数据,压测结果要预留足够的余量。

另外,oltp_update_index、oltp_update_nonindex、oltp_delete这几个场景也要跑。更新索引列会触发二级索引维护,比更新普通列的开销高很多;DELETE 会涉及记录删除和可能的碎片回收。把它们单拎出来跑,可以看到不同写操作对系统影响的差异,后续再做参数优化时更有针对性。

3.4 一套可复用的压测脚本模板

下面这是我常用的压测模板,把线程数、场景都做成循环,结果落盘,方便后续比对。

#!/bin/bash HOST=127.0.0.1 PORT=3306 USER=sbtest PASS=sbtest123 LUA_DIR=/usr/share/sysbench RESULT_DIR=~/sysbench_results mkdir -p $RESULT_DIR for THREADS in 16 32 64 128; do for SCENE in oltp_read_only oltp_read_write oltp_write_only; do echo "===== $SCENE threads=$THREADS =====" sysbench $LUA_DIR/$SCENE.lua \ --mysql-host=$HOST --mysql-port=$PORT \ --mysql-user=$USER --mysql-password=$PASS \ --tables=8 --table-size=1000000 \ --threads=$THREADS --time=300 --report-interval=5 \ --percentile=99 --max-requests=0 \ run | tee $RESULT_DIR/${SCENE}_${THREADS}_$(date +%Y%m%d%H%M).log done done

跑完之后不用翻完整日志,直接提取关键行:

grep -E "transactions:|queries:|95th percentile" *threads*.log

这样一轮压测的 TPS、QPS、P99 都在眼前。我一般会把结果目录按日期归档,目录名带上实例标识和压测目的,比如20250115_pg01_buffer16g,后面找起来非常方便。

4. 结果怎么看:从 TPS 到延迟分布

4.1 sysbench 输出的每一项含义

压测结束后 sysbench 会输出一段统计信息,第一次看的人容易懵。我拆一段典型的输出:

SQL statistics: queries performed: read: 1878578 write: 530000 other: 236000 total: 2644578 transactions: 118000 (393.33 per sec.) queries: 2644578 (8814.26 per sec.) ignored errors: 0 (0.00 per sec.) reconnects: 0 (0.00 per sec.) Latency (ms): min: 1.08 avg: 40.12 max: 520.11 95th percentile: 120.05 sum: 4734168.22

重点看四类数据。第一是transactions后面的每秒事务数,也就是 TPS,这是吞吐指标。第二是queries后面的每秒 SQL 请求数,也就是 QPS。第三是Latency块里的 avg、max、95th percentile,代表延迟水平。第四是ignored errors和reconnects,正常压测应该都是 0,一旦大于 0,先不要看性能数据,要查错误原因,常见的是锁超时、连接被断开、权限问题。

有个容易误读的地方:transactions是事务数,每个事务里可能包含多条 SQL 语句。比如oltp_read_write的一个事务会包含几个 SELECT、一个 UPDATE、一个 DELETE 或 INSERT,所以 QPS 通常是 TPS 的七八倍。这个组合是固定的,所以横向对比时用 TPS 或 QPS 哪个都行,关键是每次压测用同一个场景,不要混着比。

4.2 P95/P99 比平均值更能说明问题

平均值是最会骗人的指标。一次压测里,99% 的请求可能都在 10ms 内完成,但 1% 的请求因为刷脏、锁等待、网络抖动跑到 3 秒,平均一下就被平均到了 45ms。业务方的体感却主要由那 1% 的慢请求决定,用户不会因为“平均延迟不错”就原谅一次转圈。

所以我压测时重点看 P95、P99,甚至在某些对延迟敏感的业务里看 P999。sysbench 的--percentile=99就是让输出里带出 99th percentile 数值。判断实例稳定性的一个经验法则是:P99 如果超过平均值的 3 倍以上,基本可以断定系统存在明显的长尾抖动,接下来要去查刷脏节奏、锁等待、网络抖动这几个方向。

还要注意max值。一次 300 秒压测里如果 max 高达数秒,说明系统在某个瞬间被击穿过一次,哪怕只发生一次,也要找出原因。很多时候 max 来自压测刚开始时的连接建立和 buffer pool 预热,所以我之前的建议是先跑 60 秒预热,把冷启动的毛刺排除掉,再看正式结果。

4.3 压测没达到预期时,下一步往哪查

吞吐上不去、延迟又高,原因可能出在 MySQL 自身,也可能出在操作系统、磁盘、网络,甚至压测机。我一般按这个顺序排查。

先用top、vmstat 1看系统整体。如果 CPU 的us很高,说明 MySQL 的计算和事务处理已经占满了 CPU,瓶颈在实例性能本身;如果sy很高,要怀疑上下文切换、锁竞争;如果wa/iowait高,基本可以判断是磁盘刷页或读盘速度跟不上。

再用iostat -x 1看磁盘。SSD 的util到 100% 不一定代表饱和,还要看 IOPS 和await。如果await明显上升,说明请求开始排队。这里要记住,MySQL 的写路径经常是异步刷脏,瞬时 IO 压力不易从引擎内部看到,从 iostat 的写吞吐变化趋势反而更能说明问题。

然后进 MySQL 内部。SHOW ENGINE INNODB STATUS\G的 TRANSACTIONS 部分会列出当前锁等待,如果压测期间大量线程处于LOCK WAIT状态,那就是锁冲突主导了延迟。MySQL 8.0 里还可以查performance_schema.data_lock_waits,能看到具体的锁等待关系,定位到是哪张表哪个索引。

最后看参数。常见的影响压测结果的参数包括innodb_buffer_pool_size、innodb_io_capacity、innodb_flush_log_at_trx_commit、sync_binlog、max_connections。这一组参数基本决定了内存命中率、刷脏能力、日志写入策略和并发上限,压测结果不理想时,优先从这里找原因。

4.4 调参前后对比的“三同”原则

做参数优化后,要验证效果就得再做一轮压测。这时候必须遵守“三同”原则:同压测机、同数据集、同脚本。三个条件里只要变一个,结果就不具备可比性。

尤其是数据集,最容易被忽视。有人压测之前把表 truncate 了,或者重新 prepare 了一份数据,那么纵使其他条件一样,行数、索引分布、随机内容都已经变了,TPS 的差异就说不清是参数变化导致的,还是数据量变化导致的。所以调参前后对比时,我一般会保持同一份数据不做重建,只重启 MySQL 实例加载新参数。

为了避免偶然性,单轮压测结果不能作数。我习惯先跑一轮 60 秒预热,然后连续跑三轮 300 秒正式压测,取 TPS 和 P99 的中位数作为最终结果。如果三轮结果波动超过 5%,说明压测环境不够稳定,需要检查是否有其他任务在抢占资源,而不是急着下结论。

5. 生产环境压测避坑清单

5.1 别把生产主库直接当靶子

对生产主库做写压测的风险很高。一来会瞬间产生大量 binlog,复制链路的压力成倍增长,从库可能追不上;二来压测写入的数据会留在主库,造成数据污染;三来如果压测期间业务流量还在进来,两者的并发叠加,谁都无法保证主库不被打挂。

我见过不止一次有人为了图省事,直接在测试环境的主库上跑写压测,结果那个实例刚好在同步关键业务,压测一开始 CPU 就飙到 100%,业务接口超时告警刷屏,最后还是靠着重启和清数据才恢复过来。这种事能不碰就不碰。

如果真的只有生产实例可用,至少要做到这几条:避开业务高峰,用--rate把压测流量控制在一个安全水位;压测前先摘掉一部分只读流量或关闭非核心入口;准备好一键终止脚本,比如用后台执行 sysbench 的 PID,发现告警立刻 kill。但在我心里,这永远是后备方案,首选还是克隆实例。

5.2 压测过程中如何控制对在线业务的影响

生产环境压测,最重要的是可逆和可控。--rate参数可以把每秒事务数限制在一个预设值,比如先压 1000 TPS,观察实例负载,没问题再上调到 2000。这种渐进式压测比一次性打满安全得多。

同时在压测机上用一个timeout包裹住 sysbench 命令,防止脚本或者网络异常导致压测无限跑下去:

timeout 360 sysbench /usr/share/sysbench/oltp_read_write.lua \ --mysql-host=... --mysql-user=sbtest --mysql-password=... \ --tables=8 --table-size=1000000 \ --threads=64 --time=300 --report-interval=5 run

压测期间不要同时安排逻辑备份、大表 DDL、几十万行的批量更新,这些任务和压测抢 IO、抢锁会让结果失真。可以的话,压测窗口在业务低谷期单独预留,比如凌晨 2 点到 4 点。这个窗口里应用侧也尽量不要有定时任务。

5.3 典型问题速查表

压测中经常遇到的问题,我整理成了一张速查表,方便大家直接对照排查。

问题现象可能原因建议操作
prepare 阶段非常慢插入随机数据 CPU 密集,或表索引过多降低线程数,必要时减少 tablesize
压测时报连接不上max_connections太小压测前调大连接上限,压测后恢复
并发提高后 TPS 不增反降实例 CPU 已饱和,或线程上下文切换成本过高确认瓶颈后,降低并发数取峰值为准
P99 远高于 avg 三倍以上刷脏抖动、锁等待、网络波动查 InnoDB 状态与 iostat
结果三轮波动超过 5%压测机负载不稳,或有其他任务抢资源单独压测机,关掉多余任务
写压测后从库延迟上涨主库产生大量 binlog写场景单独做,避开从库同步链路
错误数持续大于 0锁超时、连接中断、权限异常看 errors 内容,配合 MySQL 错误日志定位

5.4 压测结束后的收尾动作

压测完不等于结束,收尾做不好,后面的麻烦事一点不少。

最简单也最重要的一步:清理数据。临时实例直接销毁;如果是独立缓解克隆实例,直接删掉;如果是在既有实例的测试库里压测,就DROP DATABASE sbtest,释放磁盘空间。这个动作别拖着,测试数据一旦混到业务库里,后面很难分辨哪些行是压测造的。

然后恢复压测期间临时调整的参数。常见的比如max_connections、innodb_buffer_pool_size、sync_binlog,如果是压测前调大的,压测后要改回来,并确认实例配置和监控告警全部恢复。别忘了检查主从复制状态,确认没有卡在某个 GTID 上。

最后把压测结果归档。我习惯在结果目录里存一份说明文件,记录压测日期、实例规格、MySQL 版本、关键配置参数、数据集规模和压测结论。这份说明的价值在半年后调优时才会体现出来,有据可查远比记忆靠谱。归档文件不需要多复杂,一个 Markdown 或纯文本说明即可。

写在最后

压测做过很多轮之后,我最大的感触是:sysbench 给人最重要的东西,不是那个让人兴奋的最大 TPS,而是能被反复验证的趋势。配置改好还是改坏,硬件换代有没有效果,业务增长到什么程度需要扩容,都不是靠感觉,而是靠同一套脚本跑出来的前后差异决定的。每次压测前先想清楚要验证什么变量,压测时把环境控制住,压测后把结果归档,长期坚持下来,这套方法会变成生产环境 MySQL 运维里很扎实的底牌。下次遇到性能问题,先别急着改配置,把基线留下来,后面的一切判断都会踏实很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询