MySQL IOPS、QPS、TPS区别与压测容量评估

发布时间:2026/11/3 21:14:58
MySQL IOPS、QPS、TPS区别与压测容量评估

IOPS、QPS、TPS 这三个词,只要跟 MySQL 打过交道,差不多每天都会在监控面板、故障复盘或者扩容评审会上撞见。可一旦有人追问“这台库 QPS 才三千,磁盘 IOPS 怎么就顶到天花板了”,或者“压测报告写着 TPS 一万二,上线后连三分之一都跑不到”,很多人还是会卡壳。说到底,这三个指标量的是三个完全不同的层面:IOPS 数的是存储设备每秒处理多少次读写请求,QPS 数的是数据库每秒执行多少条语句,TPS 数的是每秒成功提交多少个事务。它们的单位看起来都是“每秒多少次”,但计量对象差了整整两个抽象层级,混着用就必然得出错误结论。

这篇内容面向的是已经能写 SQL、但对着监控图表还不太有底气的开发者,也适合需要做容量评估、压测验收、故障定位的运维和 DBA 同学。我会把三个指标的定义边界、彼此之间的换算关系、压测怎么打、线上怎么采、数字对不上时怎么排查,一条条拆开讲清楚,附上可以直接复制执行的命令和参数。看完之后,你应该能独立回答“我这套业务到底需要多少 IOPS、QPS、TPS”这类问题。

1. 三个指标各自在量什么

1.1 IOPS:以“次数”计的存储层吞吐

IOPS 是 Input/Output Operations Per Second 的缩写,字面意思就是每秒完成的输入输出操作次数。注意它计的是“次数”,不是“数据量”,这是理解它的关键。一次 4KB 的随机读算一次 IO,一次 128KB 的顺序读也只算一次 IO。所以同样是 10000 IOPS,块大小 4KB 和 512KB 对应的实际带宽差了 128 倍,脱离块大小谈 IOPS 基本没有意义。

在 MySQL 的语境里,IOPS 通常特指存储设备层面的物理读写能力。一块机械盘在随机读写场景下大概只有 100 到 200 IOPS,SATA 固态盘能到几万,NVMe 盘做到几十万也常见。但数据库不是直接把 SQL 翻译成磁盘请求的,中间隔着 Buffer Pool、redo log、double write、binlog 这一串组件,一条 SQL 落到盘上可能是零次 IO(全命中内存),也可能是十几次 IO(脏页刷新 + 日志刷盘 + 双写)。所以线上看到的 IOPS,本质上是“存储设备被要求做了多少事”,而不是“业务发起了多少请求”。

衡量 IOPS 时还要区分随机和顺序。随机 IO 每次都要寻道或者访问不同的闪存页,代价高;顺序 IO 是连续地址,吞吐能拉满。MySQL 的负载里,主键点查、二级索引回表基本都是随机读,redo log 和 binlog 写入是顺序写,刷脏页则介于两者之间。看监控时如果只盯着一个总的 IOPS 数字,很容易把顺序写的正常开销误判成随机读的性能瓶颈。

1.2 QPS:以“语句”计的数据库吞吐

QPS 是 Queries Per Second,每秒查询数。在 MySQL 里它有两个常见口径,很多人在这里就分不清了。第一个口径来自Questions状态变量,它统计的是服务器收到的、已经执行过的语句总数,包含存储过程和触发器内部执行的语句,也包含客户端发来的预处理语句执行。第二个口径是把各个Com_*变量加起来,比如Com_select、Com_insert、Com_update、Com_delete,这个口径只统计顶层语句,不含内部嵌套调用。

两个口径算出来的数字可能差出不少,尤其是业务里大量使用存储过程的时候。做容量评估时我更倾向用Questions,因为它更接近“数据库实际承担的工作量”;做业务侧 QPS 报表时用Com_select之类的分类统计更直观,能看出读多还是写多。口径必须在团队内统一,不然两拨人拿着不同的 QPS 数字开会,能吵一下午。

还有一点值得强调:QPS 高不等于系统压力大。一次主键点查可能耗时 0.2 毫秒,一次没走索引的全表扫描可能耗时 2 秒,前者把 QPS 顶上去了但机器很闲,后者 QPS 很低但 CPU 和磁盘都已经在冒烟。QPS 是个吞吐量指标,不是性能指标,它必须和响应时间、错误率放在一起看才有意义。

1.3 TPS:以“事务”计的业务吞吐

TPS 是 Transactions Per Second,每秒事务数。事务这个词在 MySQL 里有两层含义。一层是显式事务,就是BEGIN到COMMIT之间的一批操作,要么全成功要么全回滚;另一层是单条语句的隐式事务,autocommit 打开时每条 SQL 自己就是一个事务。日常讨论 TPS 时,业界默认指的是显式事务,也就是业务上真正意义上的“一次完整业务动作”,比如一次下单、一次转账、一次库存扣减。

在状态变量层面,Com_commit加Com_rollback大致对应提交和回滚的事务数,但要注意隐式提交的语句不会计入这两个变量,所以这个算法在 autocommit 场景下会偏低。更严谨的做法是看Handler_commit,或者干脆用压测工具自己报告的事务数。生产环境里,如果业务代码全部是单条 SQL 自动提交,那 TPS 和 QPS 在数值上会接近,但语义完全不同,不能拿来做横向对比。

TPS 的价值在于它把“一个事务里的多条语句”打包成了一个原子单位。一个下单事务可能包含检查库存、扣减库存、写订单主表、写订单明细、更新用户积分一共五条 SQL,TPS 记 1,QPS 记 5。当你想评估数据库能扛多少业务量时,TPS 才是那个能直接换算成订单数的数字。

1.4 三个数为什么不能互相换算

经常有人问:QPS 一万对应的 IOPS 是多少?这问题没有通解,因为它们之间隔着 Buffer Pool 命中率、事务语句数、日志刷盘策略三道变量。同样是 QPS 一万,如果全部命中 Buffer Pool,IOPS 可能接近于零;如果全部穿透到磁盘做随机读,IOPS 可能就是一万往上。同样是 TPS 一千,innodb_flush_log_at_trx_commit设成 1 和设成 2,落盘次数能差一个数量级。

更麻烦的是,这三个指标还受并发模型影响。连接数从 16 加到 64,QPS 可能涨一倍,但 IOPS 未必涨,因为更多请求打在了同一批热数据上,缓存命中率反而更高。反过来,如果并发上去之后热数据被打散,缓存频繁失效,IOPS 会突然抬升而 QPS 原地踏步。这种非线性关系在扩容评估时特别容易踩坑,光看压测报告上的单点数字就下结论,基本等于赌运气。

我的经验是把三者当成一个三角来看:TPS 反映业务价值,QPS 反映语句负载,IOPS 反映物理成本。任何一个数字异常,都要去看另外两个怎么变化,才能判断是业务涨了、代码写歪了,还是硬件到瓶颈了。

2. 从一次 UPDATE 拆开看三层开销

2.1 一条 UPDATE 在存储层留下几次 IO

想搞清楚 TPS 到 IOPS 的换算,最好的办法是拿一条最简单的 UPDATE 从头走一遍。假设执行UPDATE t SET c = c + 1 WHERE id = 100,且这一页不在 Buffer Pool 里、事务隔离级别是可重复读、sync_binlog = 1、innodb_flush_log_at_trx_commit = 1,那么这条语句大致会经历下面这些落盘动作。

首先是读页。根据主键定位到 B+ 树叶子节点所在的数据页,如果不在内存里就要从磁盘读进来,这是一次 16KB 的随机读,计一次读 IO。接着修改内存中的页,把页标记为脏,这个动作本身不产生 IO。

然后是写 redo log。redo 记录先写进 log buffer,事务提交时刷到磁盘,这是顺序写。写入位置在 redo 文件末尾,通常不需要寻道,但需要fsync保证持久化,一次 fsync 在机械盘上是毫秒级,在固态盘上是微秒到百微秒级。

接下来是 double write。为了防止页写到一半掉电导致页损坏,InnoDB 会先把整个页写到 double write buffer 区域再写回真实位置,这意味着一次脏页刷盘实际产生两次写:一次顺序写进共享表空间的双写区,一次随机写进真实的数据文件位置。双写区是顺序的,可以批量合并,真实位置是随机的。

最后是 binlog。开启 binlog 时,事务提交前还要写 binlog 并刷盘,又是一次顺序写加一次 fsync。如果开了组提交,多个事务的 fsync 可以合并成一次,这是高并发下 TPS 能做上去的关键机制。

把这些加起来,一条 UPDATE 在最坏情况下是 1 次随机读加 4 到 5 次写(redo 顺序写、binlog 顺序写、双写区顺序写、数据页随机写,某些配置下还有额外的 fsync)。但脏页不是每改一次就刷一次,实际是由后台线程按 checkpoint 节奏批量刷的,所以稳态下每个事务分摊的写 IO 远小于这个理论值。这就是为什么“一次事务几次 IO”这个问题只能给区间不能给定值。

2.2 用 TPS 反推 IOPS 的估算公式

虽然精确值算不出来,但工程上可以用一个经验公式做粗估,用在选型和容量规划上已经够用了。我常用的形式是:

预估写 IOPS ≈ 目标 TPS × 每事务平均脏页写次数 × 写放大系数

其中每事务平均脏页写次数,可以通过观察Innodb_data_writes的增量除以Com_commit的增量得到,线上跑一天就能测出自己业务的真实值,比任何论文参数都准。写放大系数在固态盘上尤其重要,因为闪存写入前要先擦除,实际物理写入量可能是逻辑写入量的 2 到 3 倍,这个系数通常查盘的规格书能拿到。

举个具体的例子。假设某个订单库目标 TPS 是 2000,观测下来每个事务平均产生 3 次数据页写,盘上写放大按 2 倍算:

  • 逻辑写 IOPS = 2000 × 3 = 6000
  • 物理写 IOPS = 6000 × 2 = 12000

再考虑日志侧。redo 和 binlog 是顺序写,一般不计入随机 IOPS 的瓶颈,但如果sync_binlog = 1,每秒 2000 次 fsync 对盘的延迟也是考验,需要看盘的 4K 随机写 IOPS 是否远大于 12000,通常要求实际能力是估算值的 1.5 到 2 倍,留出峰值和后台任务的空间。

读侧同理。按Innodb_buffer_pool_reads除以Innodb_buffer_pool_read_requests算出缓存未命中率,再乘 QPS 就是读穿透到磁盘的估算值。命中率 99% 时 QPS 一万只有 100 次读 IO,命中率跌到 95% 就是 500 次,差了五倍,这就是缓存命中率必须和 QPS 一起盯的原因。

2.3 磁盘选型时的余量怎么留

选盘的时候不能只看峰值算出来的那个数。我的习惯是按“稳态 IOPS × 1.5”来选盘,再额外确认盘的 99 分位延迟能不能接受。举个例子,估算下来稳态需要 8000 随机写 IOPS,那就按 12000 选,同时确认在这个负载下盘的写延迟不超过 1 毫秒。很多盘标称 IOPS 五十万,那是在队列深度很深的顺序场景测出来的,队列深度 4 的随机写可能只有两三万,拿标称值做规划必然翻车。

还有一类容易被忽略的 IO:备份和批量任务。每天凌晨的全量备份会把数据文件从头读一遍,这个 IO 量可能比白天业务峰值还大;大表加索引、数据归档、DDL 变更同理。规划时要把这些离线任务的 IO 峰值算进去,否则运维窗口一到,线上业务和备份抢盘,两边都跑不动。

最后留一句提醒:云上环境里,IOPS 往往是和存储容量绑定的。买了 500GB 的高性能盘,可能默认只给 3000 基线 IOPS 加突发额度,突发额度用完就掉到基线。压测前一定先确认盘的基线、突发和突发时长三个参数,不然压出来的数字完全没有参考价值。

3. 自己压一遍:sysbench 分别打 IOPS、QPS、TPS

3.1 压测前必须确认的三件事

第一件事是数据量要足够大。如果表数据能全部装进 Buffer Pool,压测测的就是内存速度,IOPS 会低得离谱,TPS 会高得离谱,跟线上完全不是一回事。经验做法是让压测数据总量达到 Buffer Pool 大小的 3 到 5 倍,比如 Buffer Pool 配 8GB,那压测表加起来做到 30GB 左右,这样才有一部分读会穿透到磁盘。

第二件事是关闭干扰项。压测机上不要跑其他服务,MySQL 的slow_query_log、binlog按线上实际配置来,不要为了跑分好看就把双 1 关掉,那测出来的数字没有意义。同时确认压测账号的权限、连接数上限、max_connections都调到能支撑目标并发。

第三件事是预热。冷启动时缓存是空的,第一批请求全在磁盘上跑,数字会非常难看。正确的做法是先跑一轮短时间的压测做预热,等 Buffer Pool 热起来,再开始正式采样。sysbench 的--report-interval设成 5 到 10 秒,观察中间值是否稳定,如果前后波动超过 20%,说明还没进入稳态。

3.2 三套场景的命令与参数

sysbench 自带多种 OLTP 场景,不同场景打出来的数字对应不同指标。数据准备阶段统一执行下面这条:

sysbench --db-driver=mysql --mysql-host=127.0.0.1 --mysql-port=3306 \ --mysql-user=bench --mysql-password='Bench@123' --mysql-db=sbtest \ --tables=10 --table-size=2000000 --threads=8 --time=0 \ oltp_read_write prepare

十条表每张两百万行,整体数据量在几十 GB 量级,配合 16GB 到 32GB 的 Buffer Pool 能压出比较真实的穿透比例。

想压 QPS,用纯点查场景,一条语句一个请求,最接近读多写少的互联网业务:

sysbench --db-driver=mysql --mysql-host=127.0.0.1 --mysql-port=3306 \ --mysql-user=bench --mysql-password='Bench@123' --mysql-db=sbtest \ --tables=10 --table-size=2000000 --threads=64 --time=300 \ --report-interval=10 --rand-type=uniform \ oltp_point_select run

想压 TPS,用读写混合场景,sysbench 报告的transactions per second就是事务级吞吐:

sysbench --db-driver=mysql --mysql-host=127.0.0.1 --mysql-port=3306 \ --mysql-user=bench --mysql-password='Bench@123' --mysql-db=sbtest \ --tables=10 --table-size=2000000 --threads=32 --time=300 \ --report-interval=10 oltp_read_write run

想压 IOPS,用只写场景,同时对磁盘做采集,看iostat的输出:

sysbench --db-driver=mysql --mysql-host=127.0.0.1 --mysql-port=3306 \ --mysql-user=bench --mysql-password='Bench@123' --mysql-db=sbtest \ --tables=10 --table-size=2000000 --threads=32 --time=300 \ --report-interval=10 oltp_write_only run iostat -x -k 5

iostat输出里重点看三列:r/s和w/s是每秒读写的请求数,加起来就是设备 IOPS;await是平均每次 IO 的等待时间,超过 10 毫秒就说明盘开始成为瓶颈;%util表示设备繁忙度,接近 100% 基本可以判定打满。注意%util在 NVMe 上参考价值有限,并发队列下可能一直显示满但实际还有余量,这时候以await和aqu-sz队列长度为准。

还有个轻量工具mysqlslap,适合快速验证某条 SQL 的并发表现,但它不做事务级统计,测出来的是语句级吞吐:

mysqlslap -h127.0.0.1 -P3306 -ubench -p'Bench@123' \ --concurrency=50 --iterations=3 --number-of-queries=10000 \ --query="SELECT c FROM sbtest1 WHERE id=FLOOR(1+RAND()*2000000)" \ --create-schema=sbtest

3.3 采样口径:别把平均值当结论

压测报告最后打印的那个平均值,是最容易误导人的数字。一轮 300 秒的压测,前 60 秒可能在预热,中间 200 秒稳定,最后 40 秒可能因为数据倾斜或者连接抖动而暴跌,平均值把这些全抹平了,看起来还行,实际只有 70% 的攻击区间是正常的。我一般要求看三样东西:--report-interval输出的中间值曲线、95 分位响应时间、以及错误数。sysbench 的通用统计里会打出95th percentile,这个比平均值可信得多。

还有一个经典坑是连接建立开销。sysbench 默认每个事务复用连接,但如果业务侧每次请求都新建连接,实际能扛的 TPS 会低一截。压测时要明确是“长连接模型”还是“短连接模型”,两者数字没法互相参考。短连接场景下连接池配置、wait_timeout、max_connections都会成为瓶颈,这些在压测报告里往往看不出来。

最后是并发梯度的设计。不要只压一个并发数,从 8、16、32、64、128 逐级往上压,画出 QPS 和 TPS 随并发变化的曲线,找到拐点。拐点之前是线性增长区,拐点之后是排队区,真实系统应该长期运行在拐点的 60% 到 70% 位置上。只压一个点,你根本不知道这个点是在爬坡段还是在雪崩前夜。

4. 线上采集:不压测怎么拿到真实数字

4.1 MySQL 侧的状态变量与计算方式

生产环境不能随便压测,那就只能靠状态变量采。核心思路是两次采样取差值再除以时间间隔,直接读累计值然后除以 uptime 得到的那个“历史平均”,在业务有峰谷变化时几乎没有参考意义。我通常写一个脚本每 10 秒采一次,落到监控系统里再看曲线。

关键变量和算法如下:

指标数据来源计算方式
QPSQuestions(当前值 - 上次值) / 间隔秒数
读 QPSCom_select同上
写 QPSCom_insert + Com_update + Com_delete同上
TPSCom_commit + Com_rollback同上,不含隐式提交
物理读 IOPSInnodb_data_reads同上
物理写次数Innodb_data_writes同上
缓存命中率Innodb_buffer_pool_reads/Innodb_buffer_pool_read_requests1 减去比值
日志写入量Innodb_os_log_written差值 / 间隔,换算成 MB/s
SELECT VARIABLE_NAME, VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME IN ( 'Questions','Com_select','Com_insert','Com_update','Com_delete', 'Com_commit','Com_rollback','Innodb_data_reads','Innodb_data_writes', 'Innodb_buffer_pool_reads','Innodb_buffer_pool_read_requests', 'Innodb_os_log_written','Threads_running','Threads_connected' );

这里有个细节值得注意:Innodb_data_reads的单位是“次”,一次读可能读 16KB 也可能读 1MB,Innodb_data_read才是字节数。算 IOPS 用前者,算吞吐带宽用后者,别搞反了。另外performance_schema.global_status和SHOW GLOBAL STATUS返回的是同一份数据,前者可以用 SQL 直接过滤,写脚本更方便。

还有一个隐藏指标是Threads_running,它表示当前正在执行而非等待的线程数。当Threads_running持续高于 CPU 核数时,说明 CPU 已经在排队,此时 QPS 再高也是虚的,响应时间肯定在涨。这个指标比 QPS 更能反映实时压力,我一般在告警规则里都会加上它。

4.2 操作系统侧的 IOPS 与 MySQL 侧的对账

MySQL 说它每秒发起了 5000 次物理读,但iostat显示盘上只有 3000 次读,这中间的差异必须搞清楚,否则排查方向会完全跑偏。常见的原因有三种。

第一种是文件系统缓存。Linux 的 page cache 会缓存一部分读请求,InnoDB 认为的“物理读”对文件系统来说可能只是内存拷贝,没有真正落到设备上。用O_DIRECT打开数据文件时这个差异会小很多,因为绕过了页缓存。这是判断 InnoDB 读 IO 是否真实落盘的依据之一。

第二种是预读。InnoDB 有线性预读和随机预读机制,一次逻辑读可能触发内核或存储层的连续预读,实际读的块比请求的多。反过来,某些云盘的统计口径把一次大 IO 拆成多个小块计,也会让数字对不上。

第三种是其他进程在共享同一块盘。binlog、慢日志、备份文件、临时表空间可能都在同一块盘上,MySQL 的计数器只统计自己发起的,操作系统统计的是全部。所以生产环境强烈建议把数据文件、日志文件、备份目录分盘放,不光是为了对账方便,也是为了避免备份把业务的 IO 抢光。

对账的实际操作很简单,在同一时间窗口同时取两边的增量:

# 采样 1 cat /proc/diskstats | grep -E 'nvme0n1|sda' # 等 60 秒后再采一次,两边数字相减除以 60

/proc/diskstats的第 4 列是读完成次数,第 8 列是写完成次数,这两个加起来就是设备层面的 IOPS,和 MySQL 侧的Innodb_data_reads + Innodb_data_writes做对比,差异在 20% 以内算正常范围,差得太多就要往下查原因。

4.3 用摘要表定位拖后腿的语句

QPS 掉了或者 IOPS 涨了,最终都要落到具体 SQL 上。performance_schema.events_statements_summary_by_digest这张表按 SQL 模板聚合,能看到每条语句的执行次数、总耗时、平均耗时、扫描行数,是定位问题的第一现场。

SELECT DIGEST_TEXT, COUNT_STAR AS exec_count, ROUND(SUM_TIMER_WAIT/1e12, 2) AS total_sec, ROUND(AVG_TIMER_WAIT/1e9, 2) AS avg_ms, SUM_ROWS_EXAMINED AS rows_examined, SUM_ROWS_SENT AS rows_sent FROM performance_schema.events_statements_summary_by_digest ORDER BY SUM_TIMER_WAIT DESC LIMIT 20;

看这张表有两个套路。按总耗时排序,能找出“单次不快但调用量巨大”的语句,这类是 QPS 的主要消耗者;按SUM_ROWS_EXAMINED / SUM_ROWS_SENT的比值排序,能找出扫描行数远大于返回行数的语句,这类是 IOPS 的主要消耗者,通常意味着索引缺失或者索引选择性差。

举个我遇到过的例子。某个查询的调用次数只占总量的 8%,但SUM_TIMER_WAIT占了 60%,SUM_ROWS_EXAMINED是SUM_ROWS_SENT的 4000 倍。这条 SQL 就是一个典型的全表扫描,每次执行都要从盘上拉大量页进来,QPS 看着不高,IOPS 却一直顶着。加了一个联合索引之后,扫描行数从几十万降到个位数,整体 IOPS 直接降了一半。

需要提醒的是这张表是内存结构,默认只保留最近一段时间的统计,实例重启后会清空,而且对大实例有performance_schema_digests_size的上限,超出的语句会被归到DIGEST_TEXT为 NULL 的记录里。做长期分析时,还得配合慢查询日志一起看。

5. QPS 与 TPS 的偏离怎么读

5.1 QPS 高 TPS 低:点查型负载

这两个数字放在一起看,比例关系本身就是信息。如果一个系统 QPS 是两万,TPS 只有八百,比值到了 25 比 1,那基本可以判定是读密集的点查型业务,比如商品详情、用户信息、配置查询。这类负载的特点是单事务语句少、几乎不写、命中率高,瓶颈通常在连接数、网络往返或者 CPU 的解析开销上,磁盘几乎不是问题。

这种负载优化起来的收益点不在数据库内核,而在前面几层。加一层本地缓存能砍掉七成的请求;把多条小查询合并成批量查询能减少往返;精简返回字段能降低网络和序列化开销。如果确实要把 QPS 往上顶,优先看 CPU 使用率和Threads_running,再看连接池的排队情况,硬盘指标基本不用管。

反过来,如果 QPS 是两万,TPS 是一万八,比值接近 1,那说明这个库上一堆单条 SQL 自动提交的写入,事务粒度非常细。这种形态在批量导入、日志落库、计数器更新场景里很常见。它的隐患是每条 SQL 都要独立提交、独立 fsync,在双 1 配置下 fsync 次数会非常多,TPS 上不去的根本原因往往卡在日志刷盘上,而不是磁盘随机写能力。把多条写合并成批量提交,TPS 能有数量级的提升。

5.2 TPS 高 IOPS 低:内存内的小事务

如果 TPS 很高但iostat显示的 IOPS 很平静,说明这批事务基本都在内存里完成了,脏页还没到必须刷盘的临界点。这种状态看起来很美,但它是有时效性的:Buffer Pool 容量有限,脏页比例一旦触到innodb_max_dirty_pages_pct设置的阈值,后台刷脏线程会突然发力,IOPS 会在几分钟内冲上峰值,同时把响应时间拖下去。这个现象叫“checkpoint 抖动”,在写入密集的库上非常典型。

判断方法很简单,盯着Innodb_buffer_pool_pages_dirty和Innodb_buffer_pool_pages_total的比值。比值长期低于 10% 说明刷脏跟得上,接近 30% 甚至更高就要警惕了,下一步就是响应时间飙升。缓解的办法包括调大innodb_io_capacity让刷脏更积极、把 redo log 容量加大拉长 checkpoint 周期、以及在业务低峰期做批量写入。

还有一种情况是事务里全是读,比如一个事务里执行了十条 SELECT。这种事务 TPS 会上去,但 IOPS 未必高,QPS 和 TPS 的比值大约是 10。看到这种形态,首先要怀疑的是业务代码里的事务边界画得过大,把只读查询包进了事务里,白白持有锁和快照。拆分事务边界通常能同时改善锁等待和 TPS。

5.3 容量估算:从业务量倒推三个指标

真正做规划时,思路是反过来的:先有业务量,再推 TPS,再推 QPS,最后推 IOPS。这个链路走通了,采购和扩容才有依据。

第一步算峰值 TPS。假设某业务日订单量 200 万,下单集中在 8 小时工作时段,峰值系数按 3 倍算:

  • 平均 TPS = 200 万 / (8 × 3600) ≈ 70
  • 峰值 TPS = 70 × 3 ≈ 210

下单事务里平均 6 条 SQL,那峰值 QPS 大约是 210 × 6 = 1260,再加上查询类请求,整体 QPS 按 2000 到 3000 规划比较稳妥。这些数字不算高,一台中等配置的机器就能扛,但如果日订单量涨到 2000 万,峰值 TPS 就是 2100,QPS 上万,规划就得认真做了。

第二步推 IOPS。拿峰值 TPS 乘上每事务脏页写次数(假设 3 次),得到 6300 逻辑写 IOPS,再乘写放大 2,得到 12600 物理写 IOPS。加上读穿透和日志写入,整体按 1.5 万到 2 万随机 IOPS 规划,选盘时再留 50% 余量,落到 2 万到 3 万。这个数字在 NVMe 盘上很轻松,在普通 SATA 盘上就得考虑分库或者换盘了。

第三步做校验。拿规划好的数字和压测结果比,压测峰值至少要达到规划峰值的 1.5 倍才算过关。同时要跑一遍降级验证:把从库摘掉、把缓存关掉,看单机能不能顶住规划峰值,顶不住就说明还有单点风险。这一步很多人会跳过,结果真出故障时才发现所谓的“高可用”根本没验证过。

6. 常见问题与排查实录

6.1 压测数字好看线上却拉胯

这大概是最高频的困惑。压测 TPS 打到一万五,上线后业务峰值只有三千就卡得不行,差距从哪来?我梳理下来大致有五个原因,按出现频率排:数据分布不同、SQL 不同、缓存状态不同、连接模型不同、并发模型不同。

数据分布这条最隐蔽。压测用 sysbench 生成的数据是均匀随机的,主键自增、没有热点;真实业务的数据往往高度倾斜,头部 1% 的用户占了 80% 的请求,全打在同一个数据页和同一批索引页上,锁竞争和页竞争瞬间就上来了。解决办法是压测时导入真实数据的脱敏样本,或者至少构造出倾斜分布。

SQL 不同这条很好理解。压测跑的是标准 OLTP 语句,线上跑的是带 JOIN、带子查询、带排序分页的复杂 SQL,单条耗时可能差十倍。我的做法是从慢查询日志里捞出 Top 20 的真实 SQL,喂给压测工具重放,这样测出来的数字才有说服力。

缓存状态不同指的是预热。线上重启后缓存是冷的,前几分钟性能会明显下降,如果压测是热缓存跑的,自然对不上。连接模型前面提过了,短连接加连接池的实际表现和长连接压测差别很大,尤其在高并发下建连开销不可忽略。并发模型指的是业务侧的线程池、协程调度、超时重试策略,这些都会放大数据库侧的压力。

6.2 IOPS 打满的识别与降载顺序

怎么判断 IOPS 真的打满了?我一般看四个信号同时出现:iostat的await持续超过 10 毫秒、aqu-sz队列长度持续大于 2、MySQL 的Innodb_buffer_pool_wait_free开始增长、业务侧 P99 响应时间明显上翘。四个里出现两个就要开始处理,出现三个基本可以确认。

降载的顺序很关键,顺序错了会做无用功。我的处理顺序是这样的:

  1. 先看慢查询和全表扫描。这是收益最高的一步,一个缺失索引可能就吃掉了 40% 的 IO。用摘要表按扫描行数排序,十分钟内能定位到问题 SQL。
  2. 再看缓存命中率。命中率从 99% 掉到 95%,意味着磁盘读请求涨了五倍。查是不是有大查询把 Buffer Pool 冲掉了,或者是不是 Buffer Pool 配小了。
  3. 然后看刷脏参数。innodb_io_capacity设置得比盘的实际能力低太多,会导致刷脏跟不上,checkpoint 被迫频繁推进;设置得太高又会把盘打满。一般设成盘 IOPS 能力的 50% 到 70%。
  4. 接着看批量任务。备份、归档、统计报表是不是在业务高峰跑,能不能挪到低峰,能不能限速。
  5. 最后才考虑加机器或换盘。硬件升级是最后手段,因为前面四步往往能解决八成问题,而且成本低得多。

这里有个反直觉的经验:磁盘 IOPS 打满时,加索引不一定能马上缓解,因为加索引本身是个重 IO 操作,会把本来就紧张的盘彻底压垮。正确做法是在低峰期用在线 DDL 加索引,并临时调低 DDL 的 IO 优先级。

6.3 常见问题速查表

现象可能原因优先排查方向
QPS 高但响应时间也高复杂查询、无索引、锁等待慢查询日志、摘要表、Threads_running
QPS 正常但 IOPS 飙高缓存命中率下降、全表扫描、刷脏抖动Buffer Pool 命中率、脏页比例、Innodb_data_reads
TPS 上不去,CPU 却很闲日志刷盘成为瓶颈、锁竞争、连接数不足Com_commit增长、innodb_log_waits、锁等待统计
TPS 和 QPS 比值异常大事务边界过大、只读查询包在事务里业务代码事务划分、Trx_*统计
压测 TPS 高但线上低数据分布、SQL 差异、缓存状态、连接模型用真实数据脱敏样本重放
IOPS 高但带宽低大量小随机 IO,块大小偏小iostat的rkb/s与r/s比值
重启后性能骤降缓存冷启动、预热未完成观察命中率恢复曲线,加预热脚本
主从延迟突然拉大从库 IOPS 打满、大事务、并行复制不足从库iostat、大事务排查、并行度参数

还有几个小众但很值经验的排查点。Innodb_log_waits大于零说明 redo log 空间不够等刷盘,这时候看 redo 容量是不是配小了;Innodb_buffer_pool_wait_free增长说明没有干净页可分配,得等后台刷脏;Table_open_cache_misses持续增长说明表打开缓存不够,每秒会多做很多文件系统调用。这些指标平时不起眼,真出问题时往往是一针见血。

最后分享一个我自己用得很顺手的习惯:给每个库建一份“指标基线档案”,记录它在正常业务日的 QPS、TPS、IOPS、连接数、命中率的日曲线。出问题时不用从零判断“这个数字高不高”,直接和基线对比就知道偏离了多少。这套东西搭起来花不了多少时间,但每次故障复盘能省下大量猜测的功夫。