☰
MySQL状态排查实战:状态、操作、说明三维度全解析
2026/10/2 18:36:30 网站建设 项目流程

1. 为什么我一直坚持用"状态、操作、说明"这三个维度来写MySQL笔记

在学习MySQL的过程中,我踩过最大的坑不是SQL写不对,而是"看不懂数据库现在到底在干什么"。刚接触MySQL那会儿,我也跟大多数人一样,买了一本很厚的书,从安装开始看,然后建库建表、写增删改查。但真到了线上环境,遇到CPU突然飙高、连接数报警、某个SQL卡住不动的时候,书里教的东西完全帮不上忙。那时候我连SHOW PROCESSLIST的输出都读不顺,更别说区分Sending data和Copying to tmp table到底意味着什么了。

后来我换了种记笔记的方式,不再按"功能模块"去抄官方文档,而是按**"状态、操作、说明"**这三个维度去拆每一个知识点。这个习惯坚持了很久,慢慢从一条慢查询都排查不明白,到能比较从容地处理一些常见的数据库抖动问题。这篇笔记不是写给刚学会SELECT的人看的,也不是写给DBA专家看的,而是面向那些已经能日常使用MySQL、但遇到状态类问题容易发懵的初中级开发者或运维同学。

我理解的"状态"有两层意思。第一层是数据库的整体运行状态,比如实例活没活着、主从有没有延迟、连接数还剩多少、InnoDB的缓冲池命中率怎么样;第二层是"某一条SQL或者某一个会话"的状态,比如这条SQL正在等待锁、正在排序、正在写临时表。这两层状态对应着完全不同的操作手段,如果不把"状态"看清楚,"操作"就很容易变成瞎操作。

"操作"这个维度,我指的是针对某个状态应该执行的命令或者调整手段。比如发现连接数满了,先看max_connections是多少、再看threads_connected当前值、然后决定是杀会话还是扩连接数。操作一定要跟状态绑定记忆,否则你背再多的命令也没有意义。

"说明"则是用来补脑的,也就是每一个状态字段、每一个命令输出、每一个参数,它到底在表达什么,背后对应的MySQL内部逻辑是什么。只有把"说明"这一层补齐,"状态"和"操作"才能形成完整的闭环。

所以这篇文章我想把这套方法本身分享出来,同时以MySQL状态相关的核心命令为主线,把常用到的状态查看手段、输出字段含义、对应处理操作,全部串成一份可以直接抄作业的笔记。看完之后,你不仅能照着命令去执行,更重要的是知道每条命令读出来的信息该怎么理解。

2. 实例级状态:一条SQL如何检查MySQL当前是否"健康"

先说实例级的状态检查。无论是做日常巡检,还是线上出了故障,第一步一定是先确认MySQL这个实例本身的状态。很多同学一上来就执行SHOW PROCESSLIST,结果看到的只是会话层面的信息,实例级的"身体指标"完全没看。我的习惯是先跑几条固定命令,几秒钟之内把整体情况摸一遍。

2.1 存活状态与版本信息:Uptime、版本号、字符集一起看

第一步是看这个实例活着没有,版本是什么,跑了多久。版本和启动时间这两条信息在日常问题排查里特别有用。比如你查资料的时候,网上很多SQL写法是有版本差异的,information_schema里某些字段在不同版本里名字都不一样,知道版本就能少走弯路。而Uptime则是判断"是不是刚重启过"的关键证据。

我常用的是这一组命令:

-- 查看基本状态和版本 SHOW GLOBAL STATUS LIKE 'Uptime'; SHOW VARIABLES LIKE 'version'; SHOW VARIABLES LIKE 'version_comment';

Uptime的单位是秒。如果数据库明明有问题,但Uptime只有几百秒,那说明实例刚重启过,很多历史状态已经清零了,你的排查思路要立刻调整。version_comment会告诉你这是社区版还是企业版,社区版的某些功能和参数在部分场景下有限制,这个也要心里有数。

顺便提一句字符集。很多莫名其妙的乱码问题、排序问题、比较问题,根源都在字符集不一致。检查字符集我建议直接看一套完整的:

SHOW VARIABLES LIKE 'character_set_server'; SHOW VARIABLES LIKE 'character_set_database'; SHOW VARIABLES LIKE 'collation_server';

character_set_server是实例级默认字符集,character_set_database是当前库的字符集。如果建表的时候没显式指定,表就会继承character_set_database。线上我见过最多的情况是:程序连接用的字符集是utf8mb4,但表本身是latin1,一旦存入表情符号或者生僻字就直接报错或者变成问号。所以笔记里我始终留了一条提醒:任何一条查询结果异常,先在字符集这里打个勾。

2.2 核心健康指标:Threads_connected、Threads_running、Aborted_connects

实例级的"健康指标"我只看三个状态变量,它们能快速告诉你数据库是不是顶不住了。

SHOW GLOBAL STATUS LIKE 'Threads_connected'; SHOW GLOBAL STATUS LIKE 'Threads_running'; SHOW GLOBAL STATUS LIKE 'Aborted_connects';

Threads_connected是当前打开的连接数。这个值并不等于并发执行SQL的数量,因为很多连接是空闲的,只是连在那里没干活。Threads_running才是真正代表"此刻正在执行语句的线程数",这个值一旦持续超过CPU核心数,说明有SQL在抢占CPU资源。Aborted_connects则记录的是连接建立失败的次数,如果这个值在短时间内猛增,多半是客户端配置问题、网络问题或者max_connections被顶满了。

我举个实际场景。某次值班收到告警,说连接数达到上限的90%。我登录上去第一件事就是查这三个值,结果发现Threads_connected确实很高,但Threads_running只有两三个。这说明大部分连接都阻塞在某个地方,没有真正在跑。再结合SHOW PROCESSLIST一看,发现有大量连接卡在Waiting for table metadata lock上。这就是典型的"看起来连接数爆炸,其实是元数据锁导致连接全部堆积"的场景。如果只看到连接数高就盲目加max_connections,问题根本解决不了。

max_connections本身也是要记的:

SHOW VARIABLES LIKE 'max_connections';

默认值通常是151,但很多云厂商会调到更高。这个参数不是越大越好,因为每个连接都要占用线程栈和内存,连接数过大反而会拖垮系统。判断"连接数是不是真的不够",不能只看绝对值,要看Threads_connected是稳定在一个合理范围,还是持续往上涨并且Aborted_connects同步增加。如果仅仅是偶尔冲到上限,先查有没有SQL卡死、有没有连接泄漏,再决定要不要调参,这个顺序不能反。

2.3 慢查询与错误日志:先看历史再下结论

实例状态里还有一类很容易被忽略的信息:慢查询日志和错误日志。慢查询日志是排查性能问题的核心依据,但很多同学的MySQL默认是关闭慢查询日志的,等出了问题再开就来不及了。

建议长期开着慢查询,并且要设置合理的阈值。我一般是这么配的:

slow_query_log = 1 slow_query_log_file = /var/log/mysql/slow.log long_query_time = 2 log_queries_not_using_indexes = 1

long_query_time单位是秒,设置为2秒意味着执行超过2秒的SQL都会被记录。log_queries_not_using_indexes会把没走索引的查询也记录下来,这个开关在初期排查隐患时特别有用。开了之后,每天扫一眼慢日志,能发现很多平时感知不到的问题,比如某个接口的SQL随着数据量增长从1秒慢慢退化成了5秒。

查看慢日志最直接的命令:

mysqldumpslow -s at /var/log/mysql/slow.log

-s at表示按平均执行时间排序。这个工具会把结构相似、参数不同的SQL归并成一条,方便你看出一类问题的整体耗时。如果你用的是MySQL 8.0,performance_schema里的events_statements_summary_by_digest表也能起到类似统计作用,而且不需要依赖文件解析。

错误日志则要去log_error变量指定的路径查看:

SHOW VARIABLES LIKE 'log_error';

错误日志里常见的信息包括连接被拒绝、InnoDB初始化问题、主从复制报错等。每次排查问题之前,先看一眼错误日志,往往比在流程里瞎猜更高效。有一次我排查主从延迟,翻了一堆状态变量都没找到原因,最后是在relay log相关的错误日志里看到了Slave I/O thread断开的记录,才定位到是网络抖动导致的。

3. 连接与线程状态:SHOW PROCESSLIST的真正读法

实例级状态看完了,接下来要把镜头拉近到"会话"这一层。SHOW PROCESSLIST是MySQL排查问题过程中出场率最高的命令,但很多人都只是看一眼有没有State是Locked的记录,这个理解太粗了。

3.1 每个字段到底在说什么:Id、User、Host、db、Command、Time、State、Info

先看完整输出长什么样。执行下面的命令,或者直接查information_schema.processlist表:

SHOW FULL PROCESSLIST;

不加FULL的时候,Info字段会被截断,只看得到SQL的前100个字符左右,加了FULL才能看到完整SQL。在生产环境里我一般用SHOW FULL PROCESSLIST,但要注意,如果连接非常多,输出会很长,这时候更好用的是后面要讲的查询information_schema.processlist的方式。

输出里的核心字段,我按自己理解给你拆开:

  • Id:会话ID,也就是thread_id。杀会话的时候用的就是它,对应KILL <Id>命令。
  • User:连接使用的账号。这个字段可以帮你看是不是某个特定业务账号把连接占满了。
  • Host:客户端来源IP和端口。排查"哪个应用服务器发起的问题请求"时,这个字段最直接。如果发现来自同一台机器的连接特别多,那多半是那边的连接池配置出了问题。
  • db:当前连接的默认数据库。有可能是空的,这并不代表有问题,只是没有执行USE db。
  • Command:当前连接正在执行的命令类型。常见的值有Query、Sleep、Connect、Quit、Binlog Dump。Sleep表示连接空闲,正在等待下一条命令,属于正常状态;如果大量Sleep连接占据连接数,通常是连接池没有及时回收空闲连接。
  • Time:当前状态持续的时间,单位是秒。注意,它表示的是"处于当前State的时间",不是这条SQL的总执行时间。
  • State:当前执行阶段的状态描述。这是最需要积累经验去读的字段,常见的有Sending data、Waiting for table metadata lock、Waiting for lock、Sorting result、Copying to tmp table等。
  • Info:正在执行的SQL语句。如果Command是Sleep,Info通常是NULL。

读SHOW PROCESSLIST的核心思路是:先找Time特别大且State不是Sleep的记录,再根据State去判断卡在哪个环节,最后决定是等待、杀会话,还是去优化这条SQL。单纯看有没有Locked是远远不够的。

3.2 常见State的判别与应对:Sending data、Waiting for table metadata lock、Waiting for lock

这里我把几个高频出现的State单独拿出来说,因为它们的含义和处理方式差别很大。

Sending data

这个State是最容易被误读的。字面意思是"正在发送数据",但实际它涵盖的阶段很宽,从读取数据、过滤、计算到把结果返回客户端,都可能处于这个状态。所以看到Sending data不要急于判断"就是网络传输慢",它更可能是在执行查询的核心逻辑。

如果一条SQL长时间停留在Sending data,优先去看执行计划:

EXPLAIN SELECT ...;

重点看type字段和rows字段。type如果是ALL,说明全表扫描;rows如果特别大,说明扫描行数很多。这种情况下,优化的目标就是建立合适的索引,减少扫描行数。

Waiting for table metadata lock

这个State这两年出现频率很高,罪魁祸首往往是"在一个事务里执行了DDL"或者"长事务没提交"。元数据锁是MySQL为了保证表结构在DDL执行期间不被其他会话修改而加的锁。问题在于,如果一个会话开启了事务但一直没提交,它就会持有表上的元数据锁,导致后续的ALTER TABLE一直卡在Waiting for table metadata lock。

遇到这种情况,光看SHOW PROCESSLIST很难判断是谁堵住了DDL。我的排查思路是三步走:

第一步,查当前有哪些长事务:

SELECT * FROM information_schema.innodb_trx WHERE trx_state = 'RUNNING' ORDER BY trx_started ASC;

第二步,把trx_mysql_thread_id和SHOW PROCESSLIST里的Id对上,确认是哪个会话在持有锁。第三步,评估这个事务能不能直接杀。如果能,用KILL <Id>清理掉,DDL就能继续执行。如果这个事务背后是重要业务,就得等它自己提交或回滚。

Waiting for lock

这个State表示会话在等待获取某一行或某一个表上的锁,通常是行锁冲突。排查行锁冲突,我会用InnoDB提供的信息:

SELECT * FROM sys.innodb_lock_waits;

在MySQL 5.7及以上的版本里,sys.innodb_lock_waits这个视图会把谁在等待锁、谁持有锁、等待了多久,都列得清清楚楚。如果没启用sys库,也可以用performance_schema里的data_lock_waits和data_locks表,不过那个读起来更费劲一些。

3.3 用information_schema.processlist替代SHOW FULL PROCESSLIST

在连接数很多的环境里,SHOW FULL PROCESSLIST的输出会让你眼花缭乱。我更喜欢直接查information_schema.processlist表,这样可以使用SQL来筛选。

比如我只想看所有非Sleep状态、执行时间超过30秒的会话:

SELECT id, user, host, db, command, time, state, LEFT(info, 150) AS info FROM information_schema.processlist WHERE command <> 'Sleep' AND time >= 30 ORDER BY time DESC;

这个查询能非常快地定位到"疑似问题SQL"。再比如我想看每个用户分别占用了多少连接:

SELECT user, COUNT(*) AS cnt FROM information_schema.processlist GROUP BY user ORDER BY cnt DESC;

还有一招很好用:按来源IP维度看。某些场景下,如果你发现某台应用服务器的连接数异常多,基本可以判断那边连接池配置不合理,比如minIdle设置过大,或者应用没有正确归还连接。用下面这条SQL按Host聚合一下,一目了然:

SELECT host, COUNT(*) AS cnt FROM information_schema.processlist GROUP BY host ORDER BY cnt DESC;

information_schema.processlist里的字段和SHOW PROCESSLIST是对应的。有一点要注意,info字段同样可能被截断,如果你需要看完整SQL,可以用information_schema.processlist表里的info字段在MySQL 8.0里默认返回完整语句,前提是你有这个表的查询权限。普通业务账号可能看不到其他会话的详细信息,这个权限限制在安全上是合理的。

4. 存储引擎与缓存状态:InnoDB的关键计数器和"内功"怎么看

如果说实例状态是"体检报告",那InnoDB的状态就是"运动负荷报告"。MySQL的默认存储引擎是InnoDB,大部分线上场景对性能的感知,本质上都来自InnoDB的运行数据。这块我不会展开讲存储引擎的实现原理,而是聚焦在日常运维真正会用到的状态指标上。

4.1 InnoDB Buffer Pool命中率:一次计算理解缓存重要性

InnoDB的数据是存在磁盘上的,但读写操作都发生在内存里的Buffer Pool。如果Buffer Pool太小,频繁的磁盘读写会拖垮性能,所以判断"内存够不够大"最直接的方式,就是看Buffer Pool的命中率。

先取两个状态值:

SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read_requests'; SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_reads';

Innodb_buffer_pool_read_requests表示从Buffer Pool中成功读取逻辑页的次数;Innodb_buffer_pool_reads表示需要从磁盘读取页的次数。命中率的计算公式是:

命中率 = (Innodb_buffer_pool_read_requests - Innodb_buffer_pool_reads) / Innodb_buffer_pool_read_requests * 100%

正常的线上系统,这个命中率应该在99%以上才算合理。如果低于95%,说明Buffer Pool可能偏小,或者数据访问模式有问题,比如大量全表扫描会把有用的热点数据挤出内存。这时候看Buffer Pool大小:

SHOW VARIABLES LIKE 'innodb_buffer_pool_size';

在MySQL 5.7以上的版本里,innodb_buffer_pool_size可以设置多个实例,通过innodb_buffer_pool_instances参数控制,但通常生产环境默认1到8个实例就够了。对大部分单机MySQL来说,Buffer Pool设置为物理内存的50%到70%是常见做法。但这只是经验值,具体要结合你的数据量和业务访问特征来定。

另外,MySQL 5.7之后可以直接用sys库里的视图:

SELECT * FROM sys.metrics WHERE variable_name LIKE 'innodb_buffer_pool%';

sys.metrics会直接给出百分比形式的指标,比如innodb_buffer_pool_hit_rate,不需要自己手动算。

4.2 InnoDB行锁与事务状态:trx、locks、lock_waits三张表

事务和锁的状态是排查"数据库突然变慢"的高频入口。很多开发同学看到SQL执行变慢,第一反应是优化SQL,但有时候SQL本身没有问题,纯粹是因为它等锁等了很长时间。这时候需要直接看InnoDB当前有哪些事务在跑、谁锁了谁。

我习惯查这三张表:

SELECT * FROM information_schema.innodb_trx; SELECT * FROM information_schema.innodb_locks; SELECT * FROM information_schema.innodb_lock_waits;

innodb_trx表记录的是当前所有未结束的事务,关键字段有trx_id、trx_state、trx_started、trx_mysql_thread_id、trx_query。通过trx_started可以找到启动时间最早的长事务。trx_mysql_thread_id可以关联到SHOW PROCESSLIST的Id,方便定位到具体会话。trx_query会显示事务最近执行的那条SQL。

innodb_locks表记录了当前被持有和正在等待的锁。innodb_lock_waits则描述了锁等待关系。三张表配合起来的经典用法是:先看innodb_lock_waits,确定哪个trx_id在等待哪个trx_id的锁;再从innodb_trx里找到阻塞者的trx_mysql_thread_id;最后决定是否需要KILL掉阻塞者。

MySQL 8.0之后,innodb_locks表被拆得更细了,performance_schema里的data_locks和data_lock_waits成为了主要参考。但从使用习惯上说,innodb_trx这张表依然是最稳定的入口。

还有一点要提醒:长事务不仅仅是锁的问题,还会拖慢binlog的清理、膨胀UNDO日志,甚至导致主从延迟。所以我在巡检时一定会查一下有没有运行时间超过几分钟的事务。写业务代码的时候,也要时刻记住"事务要短平快",不要在事务里做耗时的外部调用。

4.3 临时表与排序状态:Copying to tmp table什么时候需要警惕

SHOW PROCESSLIST里还有一个State叫Copying to tmp table,对应的是SQL执行过程中要创建临时表。临时表会占用内存或磁盘,如果频繁使用磁盘临时表,性能会明显下滑。

先看全局累积的临时表使用情况:

SHOW GLOBAL STATUS LIKE 'Created_tmp_tables'; SHOW GLOBAL STATUS LIKE 'Created_tmp_disk_tables';

Created_tmp_tables表示创建了多少个内存临时表,Created_tmp_disk_tables表示其中落到磁盘的临时表数量。如果Created_tmp_disk_tables / Created_tmp_tables比例偏高,或者绝对值很大,说明很多查询的中间结果集超过了内存临时表的上限,被迫写磁盘了。

在MySQL 5.7里,临时表相关的参数有tmp_table_size和max_heap_table_size,内存临时表的大小受这两个参数中较小的那个限制。超过限制就会被转换成磁盘临时表。到了MySQL 8.0,临时表改用了TempTable引擎,由internal_tmp_mem_storage_engine控制,默认也是TempTable。

遇到Copying to tmp table频繁出现,通常有三个优化方向。第一,优化SQL本身,避免大的GROUP BY、ORDER BY、DISTINCT操作产生过多的中间结果。第二,确保关联查询的关联字段上有索引,这样不仅减少临时表,也能减少排序压力。第三,如果确认是合理的排序需求,再看需要调整临时表内存参数。

5. 从状态到操作:一套完整的问题定位链路

前面把状态相关的命令拆得比较散,这一节我想用一次完整的"实战演练"把它们串起来。假设你收到一条告警:业务反馈某个报表查询接口响应时间从200毫秒涨到了10秒,数据库CPU使用率还冲到了80%以上。你会怎么排查?

5.1 第一步清单:先收集哪些状态数据

我的习惯是,接到告警后不要急着改代码,先把下面这些状态数据一次性收集齐。因为MySQL很多状态是累计值,错过了现场,有些线索就再也找不回来了。

先在上报问题的那个时间点,收集实例级状态快照:

SHOW GLOBAL STATUS LIKE 'Threads_running'; SHOW GLOBAL STATUS LIKE 'Threads_connected'; SHOW GLOBAL STATUS LIKE 'Uptime'; SHOW GLOBAL STATUS LIKE 'Created_tmp_disk_tables'; SHOW GLOBAL STATUS LIKE 'Innodb_row_lock_current_waits';

然后立刻查看当前正在执行的所有会话:

SELECT id, user, host, db, command, time, state, LEFT(info, 200) AS info FROM information_schema.processlist WHERE command <> 'Sleep' ORDER BY time DESC;

这条SQL是我整个排查链路里最核心的一步。它会按"当前状态持续时间"倒序展示所有活跃会话,time最大的那几条,往往就是问题所在。注意这里的time单位是秒,如果某条SQL已经跑了60秒以上,几乎可以确定它就是告警的源头或之一。

同时看慢查询日志里最近几分钟的SQL记录:

tail -n 200 /var/log/mysql/slow.log

慢日志会告诉你哪些SQL是真正"慢"的,但要注意,慢日志统计的是执行时间,不代表它此刻还在跑。所以要配合processlist一起看。

5.2 顺着processlist找到SQL,再用EXPLAIN验证

假设上面那条查询返回了一条记录:

id=12345, user=app_rw, time=45, state=Sending data, info=SELECT ... FROM report_order WHERE create_time BETWEEN ... AND ... GROUP BY user_id

接下来我不急着kill,先看它为什么慢。用EXPLAIN分析这条SQL:

EXPLAIN SELECT ... FROM report_order WHERE create_time BETWEEN ... AND ... GROUP BY user_id;

EXPLAIN输出里重点看几个字段:

  • type:如果是ALL,说明全表扫描;如果是range或者ref,说明用到了索引范围扫描或者等值匹配。
  • key:实际用到的索引名。如果没有显示任何索引,说明这条SQL没有命中索引。
  • rows:预估扫描行数。如果这个值是几百万甚至几千万,即使类型是range,也可能很慢。
  • Extra:如果出现Using filesort,说明MySQL需要额外排序;如果出现Using temporary,说明用到了临时表,这两个都是性能隐患。

在当年的这个案例里,EXPLAIN显示type=ALL,rows估算为800万行,Extra里有Using temporary和Using filesort。这说明create_time上没有合适的索引,导致查询只能先把全表数据捞出来,再在内存里分组排序。时间自然就长了。

优化方案是加一个联合索引:

ALTER TABLE report_order ADD INDEX idx_create_time_user_id (create_time, user_id);

加完索引后再次EXPLAIN,type变成了range,rows降到了几万,Extra里的Using temporary和Using filesort也消失了。这个案例经典的地方在于:问题并不在数据库配置,而在SQL写法与索引设计上。如果一开始急着加CPU核数、调Buffer Pool,基本属于南辕北辙。

5.3 遇到等待类状态时的"锁链路"排查顺序

刚才的案例是Sending data,属于CPU密集型的慢。但还有一类慢是"等待型"的,比如State是Waiting for table metadata lock或者Waiting for lock。这种情况CPU可能不高,但连接数堆积得很快,用户体感同样是"卡死了"。

等待型问题的排查顺序我总结成四个动作:

第一步,用前面提到的information_schema.processlist找到所有处于Waiting状态的会话,记录它们的id和info。

第二步,查询innodb_trx表,找出所有未提交的事务,尤其是trx_started时间很早的事务:

SELECT trx_id, trx_state, trx_started, trx_mysql_thread_id, trx_query FROM information_schema.innodb_trx ORDER BY trx_started ASC;

第三步,如果确认是行锁冲突,用sys.innodb_lock_waits直接看阻塞链:

SELECT * FROM sys.innodb_lock_waits\G

这个视图会显示出waiting_pid、blocking_pid、wait_age等字段。blocking_pid就是那个持有锁不放的会话ID。

第四步,跟业务确认blocking_pid对应的会话能不能安全终止。如果是对账任务、批量脚本之类可控的任务,直接KILL掉然后让任务重跑即可:

KILL 45678;

如果阻塞者是核心业务的常驻连接,不能随便杀,那就只能等待,同时要把"为什么长事务一直不提交"这个问题反馈给应用团队去修复。

这里有一个特别容易踩的坑:KILL之前一定要确认这个会话对应的是不是已经失联的客户端。如果客户端早就没了,但服务端线程还傻傻地挂着,这种会话就是纯粹的僵尸连接,杀掉它是安全的。如果客户端还活着,你把它的会话杀了,它会收到一条"Connection was killed"之类的报错,对业务是有影响的。所以在KILL之前,多看一眼host和time字段,判断一下它是不是已经空闲了很久。

6. 主从复制状态:从SHOW SLAVE STATUS读懂延迟与故障

对于使用了主从架构的MySQL环境,主从复制的状态检查是日常运维里绕不开的一环。很多同学只知道一条SHOW SLAVE STATUS命令,但看到那一大堆字段就懵了。这里我把最需要关注的字段单独拉出来说明,另外也提一下MySQL 8.0里的命令变化。

6.1 核心字段速查:Slave_IO_Running、Slave_SQL_Running、Seconds_Behind_Master

SHOW SLAVE STATUS\G的输出有上百行,但真正需要每天关注的其实就那么几个字段。我按优先级排序:

  • Slave_IO_Running:I/O线程是否在运行。这个线程负责从主库拉取binlog。如果值是No,说明I/O线程断了,可能是网络问题、主库binlog被清理、复制账号权限异常等。
  • Slave_SQL_Running:SQL线程是否在运行。这个线程负责把从库中继日志里的SQL取出来执行。如果值是No,说明SQL线程停止了,最常见的诱因是从库执行SQL时发生错误,比如主键冲突、表不存在等。
  • Seconds_Behind_Master:从库SQL线程相对主库的延迟秒数。注意,这个值并不完全精确,它表示的是"从库已经执行到的binlog位置对应的时间戳"与"从库当前时间"的差值。如果I/O线程已经断了,这个值也可能显示为NULL或固定值,不代表复制没有延迟。

除了这三个,"Master_Log_File"和"Read_Master_Log_Pos"是I/O线程已经从主库读取到的binlog文件名和位置;"Relay_Log_File"和"Relay_Log_Pos"是SQL线程已经执行到的中继日志位置。如果I/O线程读取的进度和SQL线程执行的进度差距越来越大,说明延迟在积累。

在MySQL 8.0里,SHOW SLAVE STATUS换了个名字,变成了SHOW REPLICA STATUS。字段名也有变化,比如Slave_IO_Running变成了Replica_IO_Running,Slave_SQL_Running变成了Replica_SQL_Running。如果你用的是8.0,执行SHOW REPLICA STATUS\G,别在旧命令上浪费时间。

6.2 延迟排查:不是所有延迟都能靠加带宽解决

看到Seconds_Behind_Master涨到几百甚至几千,第一反应是什么?很多人会说"主从之间的网络带宽不够",然后去加带宽。但实际上,主从延迟的成因很多,盲目加带宽往往解决不了问题。

常见的延迟原因我归纳成四类:

第一类是大事务。比如主库一次性DELETE几百万行、跑了一次大批量UPDATE,这些操作在从库执行时要重放一遍,耗时自然比主库执行时间要长。这种延迟是"物理上无法避免"的,只能通过优化应用层写法来避免超大的事务。

第二类是从库有查询在跟SQL线程抢资源。从库通常还承担着读流量,如果某个报表查询特别消耗CPU或磁盘I/O,SQL线程的执行就会被拖慢。这时候要么优化查询,要么把这类查询挪到别的只读实例上,让从库专心跑复制。

第三类是单线程复制的瓶颈。旧版本MySQL的复制模型是单SQL线程的,即便有多个库多个表,也是串行执行。MySQL 5.6之后引入了slave_parallel_workers,5.7之后又改进了并行复制策略,如果没配置并行复制,延迟问题会随着主库写入量增大而越来越严重。检查一下:

SHOW VARIABLES LIKE 'slave_parallel_workers';

如果是0,说明并行复制没有开启。在支持并行的场景下,适当调大这个值对降低延迟有明显帮助。但是注意,并行复制能不能发挥效果,取决于binlog格式和事务是否涉及同一个表的冲突。如果大量事务都在修改同一张表,并行度再高也串行。

第四类是从库所在机器的硬件太弱。磁盘性能、CPU性能落后于主库,也会导致SQL线程追不上。这种情况加带宽确实没有意义,该升配置就得升配置。

排查延迟时,我还会用一条命令直接看SQL线程当前执行到的位置和主库binlog位置之间的差距,判断延迟是在I/O线程还是在SQL线程:

SHOW SLAVE STATUS\G

重点对比Read_Master_Log_Pos和Exec_Master_Log_Pos。如果Read_Master_Log_Pos长期小于主库当前binlog写入位置,说明主库到从库的网络传输或I/O线程有问题;如果Read_Master_Log_Pos已经追上了,但Exec_Master_Log_Pos差得很远,说明瓶颈在从库的SQL执行侧。

6.3 复制中断的恢复:跳过错误前必须确认影响面

复制中断是最紧急的故障之一。看到Slave_SQL_Running变成No,大多数人的第一反应是执行:

STOP SLAVE; SET GLOBAL sql_slave_skip_counter = 1; START SLAVE;

在MySQL 8.0里对应的是STOP REPLICA;和START REPLICA;。这个操作的意思是"跳过一条SQL错误",让复制继续往前跑。它能解决一些偶发性的错误,比如主键冲突、重复插入等,但有个大前提:你必须要先判断跳过的这条SQL会造成多大的数据不一致。

举个实际例子。一次从库报错Error 'Duplicate entry',说明SQL线程尝试插入一条主键已经存在的记录。出现这种错误,往往是因为主库上先执行了某条事务,从库重放时因为没有正确执行STOP SLAVE相关的操作,导致数据产生了偏差。如果盲目跳过一条,这个偏差会一直存在,后续所有依赖这条数据的查询结果都不对。跳过之前,至少要确认:

  1. 报错的SQL是什么类型的操作,INSERT、UPDATE、DELETE分别处理方式不同;
  2. 报错涉及的具体表和数据,在从库上能不能通过主库的数据补齐;
  3. 如果无法确认影响,宁可靠重建从库来恢复,也不要盲目跳过。

在低峰期重建从库的方式也很成熟:用mysqldump或xtrabackup做一次物理备份,恢复到从库之后,再通过CHANGE MASTER TO(8.0里是CHANGE REPLICATION SOURCE TO)重新指向主库继续复制。虽然耗时可能比较长,但至少数据是准的。

7. 状态变量与Performance Schema:给笔记补充"为什么"的另一层来源

前面讲了很多状态变量,但状态变量只回答"是什么"和"是多少",很少回答"为什么"。比如Created_tmp_disk_tables很大,你知道临时表落盘多,但不知道是哪些SQL导致的。这时候就需要performance_schema出场了。

7.1 用events_statements_summary定位"罪魁祸首SQL"

performance_schema是MySQL内置的监控诊断工具,开启后会在内存里记录各类事件的统计信息。从MySQL 5.7开始,performance_schema默认就是开启的。我最常用的一张表是events_statements_summary_by_digest,它会把相似的SQL语句聚合到一行,统计它们的执行次数、总耗时、平均耗时、扫描行数等。

查询执行时间最长的前10条SQL:

SELECT SCHEMA_NAME, DIGEST_TEXT, COUNT_STAR, SUM_TIMER_WAIT / 1000000000000 AS total_sec, AVG_TIMER_WAIT / 1000000000000 AS avg_sec, SUM_ROWS_EXAMINED, SUM_ROWS_SENT FROM performance_schema.events_statements_summary_by_digest ORDER BY SUM_TIMER_WAIT DESC LIMIT 10;

注意TIMER_WAIT的单位是皮秒,所以除以1000000000000才能换算成秒。DIGEST_TEXT是SQL的模板化文本,参数值会被替换成?,这样可以把结构相同的SQL聚在一起。SUM_ROWS_EXAMINED和SUM_ROWS_SENT的比值非常关键,如果前者远大于后者,说明这条SQL读取了大量行但只返回了少量结果,典型的"扫描多、返回少",十有八九存在索引问题。

这套方法解决了慢查询日志的一个痛点:慢日志只记录超过long_query_time的SQL,但有些SQL单次执行不算慢,比如30毫秒,可它每秒被执行几百次,累计消耗的数据库资源远超那条执行2秒的慢SQL。events_statements_summary_by_digest则能按累计消耗排序,把"总量上的大头"找出来。

7.2 sys库视图:把状态信息转为可读性更强的报告

sys库可以理解成是建立在performance_schema之上的一层"视图封装",专门为了让人更容易读懂。比如sys.session表直接提供了当前所有会话的详细信息,包括连接状态、执行时间、等待事件等,比直接查information_schema.processlist还要直观。

查看当前占用数据库资源最高的会话:

SELECT * FROM sys.session WHERE command = 'Query' AND time > 10 ORDER BY time DESC;

sys.session里有一个statement_latency字段,直接显示当前语句已经执行了多长时间;lock_latency字段则显示锁等待耗时。如果你的MySQL版本是5.7以上,sys库默认就有,不需要额外安装。它能帮你省掉很多手动计算字段的麻烦。

7.3 两个容易被忽略但很有用的状态指标

最后补充两个状态指标,它们不像Threads_running那么显眼,但关键时刻很有用。

第一个是Table_open_cache相关的:

SHOW GLOBAL STATUS LIKE 'Open_tables'; SHOW GLOBAL STATUS LIKE 'Opened_tables';

Open_tables表示当前打开的表缓存数量,Opened_tables表示累计打开过的表数量。如果Opened_tables增长很快,说明表缓存不够用,每次访问表都要重复打开和关闭文件,浪费了文件句柄和I/O。对应参数table_open_cache可以调大,但要结合open_files_limit的上限来考虑。

第二个是Sort_merge_passes:

SHOW GLOBAL STATUS LIKE 'Sort_merge_passes';

Sort_merge_passes表示排序过程中,因为内存排序缓冲区不够而不得不进行磁盘合并排序的次数。如果这个值在持续增长,说明sort_buffer_size偏小,或者存在大量需要排序的SQL。这种问题会直接体现在"查询偶尔突然变慢"上。调整sort_buffer_size比加CPU核数更对症。这个参数是会话级的,所以只在需要考虑排序操作的会话上生效,设置过大也会浪费内存。

8. 我做状态巡检时固定执行的命令集

文章最后,把我在巡检时固定执行的一套命令完整列出来。这套命令不一定适合所有环境,但作为一个基线参考,覆盖了绝大多数需要关注的状态维度。建议把它们存成脚本或者SQL文件,每次巡检时跑一遍。

-- 1. 实例基础信息 SHOW VARIABLES LIKE 'version'; SHOW GLOBAL STATUS LIKE 'Uptime'; SHOW VARIABLES LIKE 'character_set_server'; SHOW VARIABLES LIKE 'max_connections'; -- 2. 连接与线程状态 SHOW GLOBAL STATUS LIKE 'Threads_connected'; SHOW GLOBAL STATUS LIKE 'Threads_running'; SHOW GLOBAL STATUS LIKE 'Aborted_connects'; SHOW GLOBAL STATUS LIKE 'Connection_errors_max_connections'; -- 3. 当前活跃会话 SELECT id, user, host, db, command, time, state, LEFT(info, 200) AS info FROM information_schema.processlist WHERE command <> 'Sleep' ORDER BY time DESC; -- 4. InnoDB相关 SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read_requests'; SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_reads'; SHOW GLOBAL STATUS LIKE 'Innodb_row_lock_current_waits'; SHOW GLOBAL STATUS LIKE 'Innodb_deadlocks'; -- 5. 临时表与排序 SHOW GLOBAL STATUS LIKE 'Created_tmp_tables'; SHOW GLOBAL STATUS LIKE 'Created_tmp_disk_tables'; SHOW GLOBAL STATUS LIKE 'Sort_merge_passes'; -- 6. 长事务与锁等待 SELECT trx_id, trx_state, trx_started, trx_mysql_thread_id, trx_query FROM information_schema.innodb_trx ORDER BY trx_started ASC; SELECT * FROM sys.innodb_lock_waits; -- 7. 主从复制状态(主库执行) SHOW REPLICA STATUS\G

这套命令跑完,基本能回答三个问题:MySQL实例整体状态如何?当前有没有会话卡住?锁和事务有没有异常?至于更深层的优化,比如索引设计、SQL改写,那是拿到这些状态之后的分析工作了。

我自己在长期记录MySQL笔记的过程里,最大的体会是:状态是表象,操作是手段,说明才是理解的根基。如果你能把一条SHOW命令的每个字段都讲清楚"它为什么存在、什么情况下会变化、变化了意味着什么",面对大部分MySQL运行问题就不会慌。这份笔记也只是个起点,MySQL版本在升级,状态变量在增加,但状态驱动的排查思路一直有效。

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

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

立即咨询