☰
MySQL QPS/TPS计算全解析:从计数器到监控脚本
2026/10/22 6:00:56 网站建设 项目流程

做压测或者容量评估的时候,经常会被问到这么一个问题:“这台 MySQL 到底能撑多少 QPS/TPS?”压测工具里能看到它的报告,但换成生产环境,没有按钮能直接吐出一个指标。MySQL 本身也不像某些中间件那样带一个现成的“每秒请求数”仪表盘,能看到的只有一堆从启动开始累加的计数器。所以实操里我们都用同一套办法:抓两次计数器快照,做差再除以间隔秒数,还原出这段时间的平均速率。思路不复杂,但网上流传的写法各说各话,有人用 Queries,有人用 Questions,TPS 的定义更是千奇百怪。这篇文章会从计数器的底层含义讲起,把我实测踩过坑之后稳定在用的采集脚本、公式、以及各种容易失真的场景一次说清楚,适合要自己搭监控、写巡检脚本或者整理压测报告的开发者、DBA 和运维同学参考。

1. 先把计数器口径搞清楚:QPS/TPS 不是 MySQL 直接给出的数字

MySQL 确实没有原生的“每秒查询数”指标。SHOW GLOBAL STATUS输出的都是累计值,比如Queries从 MySQL 启动开始到现在一共执行了多少条语句。我们要做的,就是对这个累计值做差分。正因为如此,选哪一个计数器作为分子,直接决定了算出来的数字代表什么业务含义。

1.1 QPS 的两种取数口径:Queries 和 Questions

上网搜 QPS 计算方式,能看到两种主流写法,区别就在这条语句上。

SHOW GLOBAL STATUS LIKE 'Queries'; SHOW GLOBAL STATUS LIKE 'Questions';

Queries统计的是服务器执行的语句总数,范围非常广,包括存储过程内部执行的每条语句,还包括 Prepared Statement 每次 execute 时的内部重复执行。Questions的含义更接近“客户端发到服务器端的查询数”,存储过程内部的语句不计入,Prepared Statement 的非首次执行也可能不重复累计。

两者差多少?做个简单实验就知道了。在会话里连续执行三次下面这条语句:

PREPARE stmt FROM 'SELECT 1'; EXECUTE stmt; EXECUTE stmt; EXECUTE stmt; DEALLOCATE PREPARE stmt;

执行前后分别记录Queries和Questions,你会发现Queries的增量通常比Questions多。既然口径不同,计算 QPS 之前必须先约定:你的业务是大量普通查询为主,还是大量存储过程、预编译语句?前者用Questions更贴近应用侧体验,后者用Queries更贴近数据库实际工作量。

我个人的经验是,对外汇报容量数据时优先用Questions,因为它不容易被内部语句放大;排查数据库自身压力时再看Queries的差值。关键不是选哪一个“更正确”,而是整篇报告、监控系统里只保留同一个口径,不要今天用 Queries、明天用 Questions,否则曲线图自己都解释不了。

1.2 TPS 的“事务”到底按什么算

TPS 的常见公式是:

TPS = (Com_commit + Com_rollback) / 间隔秒数

Com_commit统计成功提交的事务数,Com_rollback统计被回滚的事务数。显式事务里BEGIN到COMMIT算一次提交,不管这个事务里塞了 10 条 UPDATE 还是 100 条 DELETE。所以这个口径表达的是“事务提交次数/秒”,不是“每秒写入的 SQL 条数”。

不少资料还会给出另一种算法,用Handler_commit和Handler_rollback替换上面的两个计数器。Handler_*是存储引擎层的提交次数,范围更细,开启自动提交时每一条 DML 都会让Handler_commit增加,而普通 SELECT 因为不走提交事务,一般不会累计。实测下来,用Handler_commit算出来的 TPS 数值会比Com_commit大不少,尤其是在大量单条 INSERT 写入的场景。两种公式我都跑过对比,最终固定用Com_commit + Com_rollback作为主口径,原因有两个:

  • Com_commit在 MySQL 5.x 到 8.x 各个版本里含义相对稳定,资料也多,团队之间对齐成本低。
  • 它更接近业务视角的“事务次数”,而不是存储引擎内部动作。

想评估事务的“大小”,可以再算一个比例:

平均事务语句数 = (Com_insert + Com_update + Com_delete) / (Com_commit + Com_rollback)

如果这个值长时间在 1 附近,说明绝大多数事务是一条 SQL 自动提交;如果经常大于 5,说明业务在跑批量写操作,一个事务里面塞了很多语句。这时候光看 TPS 数字会严重低估数据库实际承受的写入压力,务必关联这个比例一起看。

2. show global status 采样看起来简单,但四个细节最容易出错

知道取哪些计数器之后,很多人兴冲冲写了个脚本:抓一次状态、睡几秒、再抓一次,相减出结果。逻辑确实没错,但实操里我见过不少算出来的数据没法看的情况,基本都是下面这四类问题。

2.1 计数器匹配别用模糊匹配

SHOW GLOBAL STATUS输出有 300 多行,里面有很多字段名带近似前缀。比如 grepQueries,很可能会同时命中Queries、Questions、Com_queries等。直接用管道 grep 再取数字,算出来的是什么自己都说不清。

建议一次性把整张状态表拉下来,用 awk 以行首做精确匹配,比如:

awk '/^Queries[[:space:]]/{print $2}' /tmp/status_a.txt awk '/^Com_commit[[:space:]]/{print $2}' /tmp/status_b.txt

SHOW GLOBAL STATUS的字段名和值之间是制表符分隔,匹配时把行首锚定和空白符匹配都加上,能有效避开相似字段名的干扰。写成脚本时更推荐把两次快照都读进关联数组,统一处理,避免每条指标单独做一次文本匹配。

2.2 采样间隔太短,曲线会抖到没法看

计数器是累加值,不是瞬时速率。理论上任何间隔都可以算平均速率,但间隔越短,慢 SQL 的边界效应越明显。

举个例子:一条 UPDATE 执行了 3 秒,在它执行期间,这条语句的计数是在执行的瞬间加进去的,还是在结束的瞬间加进去的?不同版本、不同场景下表现不完全一样。如果采样间隔是 1 秒,刚好跨过这条语句完成节点,这一秒的 QPS 可能虚高;如果恰好没跨过,又可能完全没统计到。我多次实测,1 秒间隔下 QPS 曲线毛刺很大,抖动幅度超过 40% 都不稀奇;把间隔拉到 5 秒以上,曲线明显平滑,数据更有参考意义。

日常巡检建议固定 5~15 秒一个窗口,取平均值。只有做问题定位、需要输出秒级曲线的时候,才临时把间隔收到 1 秒,而且一定只看趋势,不要拿单秒数值下结论。

2.3 实例重启后差值会变成负数

MySQL 实例重启,所有全局状态计数器清零。如果你的采集脚本跨过了重启点,第一次快照很大,第二次快照很小,差值就成了负数。没有防护的脚本会输出一个完全没意义的“负 QPS”,有些监控系统甚至会因此触发误报。

脚本里必须加防御逻辑:

DIFF=$((VALUE_B - VALUE_A)) if [ $DIFF -lt 0 ]; then DIFF=0 fi

同理,主从切换场景里,原来采集的实例变成了新主库,计数器同样会重置。负数归零只能保证数值不异常,更好的办法是采集脚本同时记录实例的 Server UUID 或启动时间,发现节点变化时丢弃这一轮数据。

2.4 权限和账号安全细节

执行SHOW GLOBAL STATUS本身不需要超级权限,但建议单独建一个监控账号,最小权限即可:

CREATE USER 'monitor'@'localhost' IDENTIFIED BY '一个足够复杂的密码'; GRANT PROCESS, SHOW DATABASES ON *.* TO 'monitor'@'localhost';

PROCESS权限可以让监控账号看到线程状态,SHOW DATABASES是很多监控项目实际在用的最小数据集。不要在命令行参数里直接写密码,避免密码进 shell 历史;用配置文件或者客户端的登录路径功能更稳妥。这个问题在老手眼里是常识,但新手照着网上的文章抄命令,经常把 root 密码直接带上,这里提醒一下。

3. 一个可以直接抄的 QPS/TPS 统计脚本

下面这个脚本是我自己生产环境一直在用的精简版本,逻辑是抓两次SHOW GLOBAL STATUS快照,做差后输出 QPS、TPS 以及各类语句的每秒执行次数。注释写得很详细,可以直接放到定时任务里跑,也可以临时跑一下看当前负载。

3.1 脚本主体与输出样例

#!/bin/bash # 用法: ./qps_tps.sh [间隔秒数] 默认 5 秒 INTERVAL="${1:-5}" MYSQL="mysql --login-path=monitor -N -e" $MYSQL "SHOW GLOBAL STATUS" > /tmp/mysql_status_1.txt sleep "$INTERVAL" $MYSQL "SHOW GLOBAL STATUS" > /tmp/mysql_status_2.txt awk -v interval="$INTERVAL" ' BEGIN { while ((getline line < "/tmp/mysql_status_1.txt") > 0) { split(line, a, "\t"); val1[a[1]] = a[2] + 0; } close("/tmp/mysql_status_1.txt"); } { name = $1; value = $2 + 0; if (name in val1) { diff = value - val1[name]; if (diff < 0) diff = 0; val2[name] = diff; } } END { qps = (val2["Queries"] > val2["Questions"] ? val2["Queries"] : val2["Questions"]) / interval; tps = (val2["Com_commit"] + val2["Com_rollback"]) / interval; select_per_s = val2["Com_select"] / interval; insert_per_s = val2["Com_insert"] / interval; update_per_s = val2["Com_update"] / interval; delete_per_s = val2["Com_delete"] / interval; printf "QPS=%.2f TPS=%.2f select=%.2f insert=%.2f update=%.2f delete=%.2f\n", qps, tps, select_per_s, insert_per_s, update_per_s, delete_per_s; } ' /tmp/mysql_status_2.txt

输出类似这样:

QPS=1258.40 TPS=113.20 select=912.80 insert=280.60 update=52.40 delete=20.60

脚本里 QPS 取了Queries和Questions的较大值,实际使用可以按第 1 节讲的口径自己改。不同团队可能有各自约定,这不重要,重要的是脚本和报表口径保持一致。

3.2 能不能用 performance_schema 替代状态表

不少新版本提供了性能模式表,有人问能不能直接从里面查“当前速率”。理论上可以查类似语句事件汇总表,按事件类型做差分;实际用下来有两点麻烦:

  • 性能模式的语句汇总表默认开启的采集事件有限,长时间运行后还会遇到内存回收、计数重置的问题,数据缺失或跳变的情况比状态表更常见。
  • 状态表是跨版本最稳定的接口,几乎所有 MySQL 分支都支持,脚本拿过去就能跑。

所以我更建议状态表作为默认方案。需要细粒度定位是哪类 SQL 消耗了 QPS 时,再用性能模式相应表做辅助分析,两者是互补关系。

3.3 压测场景和生产采集的配合方式

压测工具一般都会给出每秒请求数、每秒事务数,很多同学以为这个数字就是数据库的承受能力。我习惯在压测的同时也跑上面的脚本,把客户端统计和服务端计数器对齐。两者通常对不上,而对不上的原因恰恰是最有价值的信息。

常见差异包括:

  • 客户端每秒请求 2000,服务端只算出 800,说明大量请求在连接池、网络层就被拦截了,根本没到达数据库。
  • 客户端报错率很低,但服务端 TPS 高,说明一次业务请求对应多次数据库事务,业务侧逻辑对数据库的压力被低估了。
  • 两端都能对上,但 QPS 只有几百,说明压测本身没有把并发拉起来,瓶颈压根不在数据库。

压测不是只看数据库达到的峰值,还要能解释“两边数字为什么不一样”。保留两套视角,查问题会快很多。

4. 那些让 TPS 哗然失真的场景:自动提交、从库与长事务

QPS/TPS 计算的公式本身没难度,难点是计算结果能不能反映真实业务状态。下面三种场景我都在线上遇到过,属于标准公式容易误判的高频坑。

4.1 自动提交让 TPS 看起来接近 QPS,实际是假象

很多业务系统根本没有显式事务,连接串里 autocommit 默认开启,每条 INSERT 都是独立事务、独立提交。这时候Com_commit的累加速度几乎等于每秒执行的写语句数量。比如业务 QPS 5000,其中 4000 条是 INSERT,那 TPS 算出来也是 4000 左右。

这个 4000 能叫“每秒事务数”吗?从数据库计数角度,确实提交了 4000 次;但从业务容量角度,它表达的只是“每秒自动提交了 4000 条单条语句”,并不能证明业务系统每秒能完成 4000 个完整业务事务。一个业务下单动作,可能拆成 10 条 SQL,每条自动提交,那 4000 TPS 实际只支撑了 400 个下单流程。

排查方法:看Com_begin的增长速度。如果Com_commit远大于Com_begin,说明大量事务是隐式自动提交。这时候衡量业务真实吞吐,要靠应用日志里的业务事务计数,数据库侧这个 TPS 只能作为参考基准。

4.2 从库的 QPS 会虚高,别把它当业务负载

从库在执行主库同步过来的日志时,修改数据的语句同样会累计到Queries、Questions、Com_insert等计数器里。主库一旦有大批量写入任务,从库的 QPS 会突然飙升,看起来比主库还忙。这时候如果用同样的阈值去告警,从库的“假 QPS 高峰”很容易触发误报。

识别同步流量和真实业务流量的几个信号:

  • 看复制状态里 Seconds_Behind_Source,从库正在追日志时,延迟往往同步上涨。
  • 看写入类指标上涨的同时,Com_select是否平稳。从库通常读多写少,如果 INSERT/UPDATE/DELETE 突然猛增,大概率是同步流,不是应用在从库上写。
  • 从库告警建议关掉写入类指标,只保留Com_select、Threads_running、复制延迟等读路径指标。

4.3 长事务和锁等待会掩盖真实压力

一个事务执行了 30 秒,期间它持有的锁让其他会话排队。从计数器的角度,30 秒内它只在结束时提交了 1 次,TPS 算出来可能只有 0.03。与此同时被阻塞的会话不断重试,Com_rollback蹭蹭往上涨,TPS 可能看起来还有点高。合在一起看,数据似乎显示“系统挺忙”,但又说不清忙在哪。

更典型的场景是锁等待把大量事务拖死。假设某个时刻有 50 个事务同时等一把锁,正常情况它们每秒能提交 100 次;现在因锁等待全都卡住,每秒只提交 1 次,TPS 直线下降。单独看 TPS 可能会得出“数据库变闲了”的错误结论,实际是业务全面阻塞。

所以算 TPS 的时候,一定要配上辅助指标:

  • Threads_running:当前正在执行的线程数。该值长时间高于 CPU 核心数,大概率有排队。
  • Com_rollback与Com_commit的比值:回滚占比高,说明冲突或超时严重。
  • 锁等待类指标:看 InnoDB 层的锁相关状态,能直接暴露阻塞源头。

这几个指标并排看,比单看 TPS 可靠得多。

5. 落地建议:QPS/TPS 如何用到容量规划和故障告警上

算出来只是第一步,让数值真正指导容量决策、预警线上故障,才算把这个工作做完。这里把我在实际使用中的经验总结一下。

5.1 不要只看一个数字,要学会组合指标

单个 QPS 5000 说明不了系统健不健康。同样的 5000 QPS,16 核服务器可能在摸鱼,2 核小机器可能已经 CPU 打满。还需要看平均响应时间、缓存命中率、当前活跃线程数。

我习惯用一个粗糙但实用的指标做横向对比:“单位 QPS 消耗的活跃线程数”。

每 QPS 线程消耗 = Threads_running / QPS

这个值越大,说明每个查询平均消耗的并发资源越高。如果长期超过 0.1,就要提高警惕,说明很多查询在排队,系统可能已经逼近瓶颈。这个公式不是精确理论,但非常适合日巡检、周容量评估时快速抓异常。

5.2 峰值采样比平均采样更有决策价值

脚本默认算的是窗口平均速率。容量评估如果只看平均值,会严重低估业务高峰期的压力。今天的业务中午 12 点 QPS 峰值 8000,但全天平均只有 1500,按平均值规划硬件必然出问题。

实操建议:用 crontab 每分钟跑一次 5 秒窗口的采样,结果追加到日志,事后取每分钟最大值。比如:

* * * * * /opt/scripts/qps_tps.sh 5 >> /var/log/mysql_qps.log 2>&1

日志按awk -F'='解析后取最大值,就能得到峰值分布。有条件的话,把每次采样的平均值、最大值、采样时间一起落库,长期留存再画趋势,容量预判的底气会足很多。

5.3 告警阈值尽量不要对 QPS/TPS 设绝对值

绝对值的 QPS 报警很难设定:不同业务模型差异太大,数据库版本和硬件也不同,同一个阈值换套配置就失效。我自己用得比较顺的思路是组合信号触发:

  • QPS 突然比历史基线上涨 3 倍以上,且Threads_running同步上涨,视为真实流量高峰,触发告警。
  • QPS 没有明显上涨,但Threads_running上涨,优先怀疑慢 SQL 或锁等待,告警去看执行计划。
  • TPS 快速下跌而 QPS 基本平稳,说明写事务被阻塞或者大量回滚,优先排查锁和主从状态。

这些规则守住的是“异常变化”,而不是“绝对数值”,在业务波峰波谷明显的场景里误报率低得多。性能指标本质上是一块仪表盘,不是保险丝,组合起来看才能判断发动机是正常轰鸣还是已经拉缸。

最后分享一个实际踩过的坑。我第一次做压测时,直接在压测工具里看总事务数,以为那就是数据库的真实 TPS,后来和服务端SHOW GLOBAL STATUS算出来的值对不上,反复排查才发现客户端统计包含了网络层重试的请求,而服务端计数器统计的是真实到达且完成的事务,两条链路的口径本来就不同。此后我做任何压测都保留两套视角:客户端视角记录业务吞吐,服务端计数器视角记录数据库实际执行量,两者差值本身就是排查链路问题的重要线索。这个习惯建议你也试试,能避掉不少想当然的坑。

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

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

立即咨询