你有没有过这种崩溃时刻:业务群里突然炸锅,用户反馈页面转圈圈转了两分钟还没加载出来。你冲进服务器一看,CPU占用率直接飙到99%,内存也在疯狂跳动。第一反应肯定是:“慢查询日志里有谁在搞鬼?”
于是你打开慢查询日志,grep 一搜,确实有条 SQL 跑了 5 秒钟。你以为找到了凶手,赶紧加上索引、优化一下,重启服务准备迎接欢呼。结果呢?五分钟后 CPU 又红了,页面还是卡得怀疑人生。这时候你才发现,那个 5 秒的 SQL 其实是“背锅侠”,真正的凶手可能正在角落里偷偷锁表,或者在后台疯狂全表扫描没人知道。
别急,这种情况太常见了。慢查询日志只是记录了“结果”,它不会告诉你“过程”中发生了什么。当你面对 CPU 飙升、锁等待、连接池爆满这些复杂问题时,单靠 slow_log 就像是用手电筒在漆黑的房间里找钥匙——照得到亮处,却照不到阴影里的真凶。
今天我不给你堆砌枯燥的理论,而是直接带你走进一线 DBA 的实战现场,聊三个真正能救命的神器:pt-query-digest 的深层分析、performance_schema 的动态透视,以及 sys 库的一键诊断。我会用真实的案例场景,把这些工具怎么用的、为什么有效,掰开了揉碎了讲给你听。哪怕你是刚入门的开发者,也能跟着操作,从此不再被“找不到原因的慢 SQL”折磨。
第一个陷阱:为什么慢查询日志会“撒谎”?
在介绍工具之前,我们必须先理解一个核心痛点:慢查询日志(Slow Query Log)的局限性。
很多小伙伴有个误区,觉得开启了 long_query_time 就万事大吉了。其实,慢查询日志存在几个致命的盲区,这些盲区正是导致“抓不到真凶”的根本原因:
时间精度不够:默认慢查询日志只记录执行时间超过阈值的 SQL。但如果一条 SQL 只跑了 1.1 秒(假设阈值是 1 秒),它被记录了。可你发现 CPU 依然很高,查这条 SQL 发现索引正常啊?这时你就懵了。真相可能是:这条 SQL 在等待锁!它在
Sending data状态卡了 3 秒,但实际执行时间只有 0.5 秒。慢查询日志记录的是Query_time(服务器处理时间),不包括等待锁的时间(Lock_time 是另一回事,而且很小)。你以为它跑得快,其实它在排队排到崩溃。无法看到执行计划的变化:慢查询日志只记录 SQL 文本和耗时,不记录
EXPLAIN的输出。同样的 SQL,在不同的数据分布、不同的 buffer pool 命中率下,执行计划可能完全不同。你看到日志里这条 SQL 平时很快,突然慢了,慢查询日志里看不出任何区别,但背后可能是优化器选择了错误的索引。采样偏差:如果开启
log_queries_not_using_indexes,你会看到大量没走索引的 SQL,但它们可能跑得飞快(比如只返回一行数据)。真正拖垮系统的,往往是那些“看似正常”的全表扫描大查询,或者高频次的短查询累积效应,慢查询日志对这种“蚂蚁搬家”式的性能杀手毫无感觉。
所以,当我们说“慢查询抓不到真凶”时,指的是:我们需要的不只是“谁慢”,而是“为什么慢”、“慢在哪里”、“还有谁在抢资源”。这就需要更强大的工具。
工具一:pt-query-digest —— 慢查询日志的“法医”
如果说慢查询日志是案发现场的录像带,那 pt-query-digest 就是那位能把录像带一帧一帧回放、提取指纹、对比时间线的顶级法医。它是 Percona Toolkit 中最强大的工具之一,专门用于分析慢查询日志、通用日志甚至二进制日志。
为什么它能抓到真凶?
它能做的不是简单的分类,而是基于规则的聚合分析。比如,你有 100 条看似不同的 SQL,但它们只是参数不同(WHERE id = 1 和 WHERE id = 2),pt-query-digest 会将它们归一化为一个模板 WHERE id = ?,然后告诉你:“这个模板总共执行了 10000 次,总耗时 500 秒,平均每次 50 毫秒,其中有一次执行了 5 秒。”
这就打破了“单条 SQL 看起来正常,整体系统却崩了”的迷思。
实战案例:定位“隐形”的高频查询
假设你收到报警,MySQL 负载高,但慢查询日志里最近一小时只有一条 SQL 超标,耗时 3 秒。你优化了这条 SQL,问题依旧。
第一步:安装 Percona Toolkit
在 CentOS/RHEL 上:
yum install percona-toolkit
在 Ubuntu/Debian 上:
apt-get install percona-toolkit
第二步:运行分析
不要只盯着慢查询日志看,我们要用 pt-query-digest 对它进行深度解剖。假设你的慢查询日志在 /var/log/mysql/slow.log:
pt-query-digest /var/log/mysql/slow.log > analysis_report.txt
这会产生一个冗长的报告,但我们更关心核心部分。你可以用 --filter 和 --group-by 来聚焦:
# 按总耗时排序,只看前10个最贵的查询模式
pt-query-digest --order-by Query_time:sum --limit 10 /var/log/mysql/slow.log
# 按执行次数排序,找出高频“蚂蚁”
pt-query-digest --order-by Query_time:count --limit 10 /var/log/mysql/slow.log
第三步:解读报告中的“真凶”
在输出的报告中,你会看到类似这样的结构:
# Rank ID Total Time Queries Executed
# ---- - ---------- ------- --------
# 1 1 120.5s 1 SELECT * FROM orders WHERE user_id = ? AND status = ?
# 2 2 85.2s 5000 SELECT id, name FROM products WHERE category_id = ?
# 3 3 45.1s 100 UPDATE inventory SET stock = stock - 1 WHERE product_id = ?
注意看第 2 行!这条 SELECT products 每次只跑 0.017 秒(85.2s / 5000),看起来完全不慢,根本进不了慢查询日志(假设阈值是 1 秒)。但是,它一天执行了 5000 次,总共占用了 85.2 秒的 CPU 时间!这就是典型的“高频短查询”累积效应,慢查询日志完全漏掉了它。
更可怕的是,pt-query-digest 还能告诉你这条 SQL 的执行计划:
# Query 2: 0.01 QPS, 0.00x concurrency, ID 0x1234 at byte 5678
# This item is included in the report because it matches --limit.
# Scores: Apdex = 0.95 (T=0.01 Thr=1.00)
# Time range: 2023-10-27 10:00:00 to 10:05:00
# Attribute pct total min max avg 95% stddev median
# ============ === ======= ======== ======== ======== ======== ======== ========
# Count 1 5000
# Exec time 70 85s 15ms 25ms 17ms 24ms 2ms 16ms
# Lock time 2 2s 0 1ms 400us 800us 200us 300us
# Rows sent 5 500k 100 105 100 100 0 100
# Rows exam 3 30M 6000 6500 6000 6400 100 6000
# Query size 20 15MB 3000 3100 3000 3050 20 3000
# Profile
# Rank ID Query Time s/Call %CPU %Disk Rows Exam Rows Sent
# ---- ----- ---------------- ------ ----- ----- --------- ---------
# 1 2 SELECT id, name FROM products WHERE category_id = ? 17.04ms 100 95.2 6000 100
看到 Rows Exam 是 6000,而 Rows Sent 是 100 了吗?这意味着每条 SQL 都扫描了 6000 行才返回 100 行!没有走索引,或者索引失效。这就是 CPU 飙升的真正元凶:5000 次全表扫描(或索引扫描),每次扫 6000 行,总共 3000 万次行扫描!
解决方案:给 category_id 加索引,或者检查为什么索引没生效(比如隐式类型转换)。优化后,Rows Exam 降到 100,CPU 瞬间下降 70%。
pt-query-digest 还能做很多事,比如:
- 按时间切片分析,找出负载高峰期的查询模式。
- 对比两个时间段的日志,找出新增的性能问题 SQL。
- 生成可视化图表(需要配合
pt-top-sql或导出到 CSV)。
它的核心优势是:把“噪音”变成“信号”,让你看到那些单独看不出来、合起来却致命的查询模式。
工具二:performance_schema —— 实时动态透视眼
如果 pt-query-digest 是事后法医,那 performance_schema 就是事中的监控摄像头。它是 MySQL 5.5 引入、后续版本不断增强的内置性能监控框架。和慢查询日志不同,performance_schema 是实时、细粒度、低开销地记录数据库内部发生的每一件事:每条 SQL 的执行步骤、每个锁的等待、每次 I/O 操作、每个阶段的耗时。
为什么它能抓到真凶?
因为它能回答:“这条 SQL 到底慢在哪个环节?” 是花在读取数据上?还是花在排序上?还是花在等待锁上?performance_schema 提供了 events_statements_summary_by_digest、events_waits_current、metadata_locks 等视图,让你能看到 SQL 执行的“解剖图”。
实战案例:排查 CPU 飙升与锁等待
还是那个场景:CPU 飙到 99%,但慢查询日志里只有零星几条 SQL。这次,我们不用日志,直接用 performance_schema 查实时状态。
第一步:确认 performance_schema 已启用
默认情况下,MySQL 5.7+ 是启用的。你可以检查:
SHOW VARIABLES LIKE 'performance_schema';
如果为 ON,我们可以直接查询。
第二步:找出当前正在等待锁的会话
CPU 高往往伴随着大量的锁等待。当一个线程在等锁时,它不会占用 CPU,但它会阻塞其他线程,导致整体吞吐量下降,用户感知为“卡”。而为了唤醒和处理这些请求,CPU 反而可能因为上下文切换而飙升。
-- 查看当前正在等待锁的语句
SELECT
p.PROCESSLIST_ID AS 'PID',
p.PROCESSLIST_USER AS '用户',
p.PROCESSLIST_HOST AS '主机',
p.PROCESSLIST_DB AS '数据库',
p.PROCESSLIST_TIME AS '等待时长(秒)',
e.EVENT_NAME AS '等待事件',
e.STATE AS '状态',
e.DURATION AS '持续时间',
k.OBJECT_SCHEMA AS '锁所属表库',
k.OBJECT_NAME AS '锁所属表名',
k.DEADLOCK AS '是否死锁'
FROM performance_schema.threads t
JOIN performance_schema.events_waits_current e
ON t.PROCESSLIST_ID = e.THREAD_ID
JOIN performance_schema.metadata_locks k
ON t.PROCESSLIST_ID = k.OWNER_THREAD_ID
WHERE e.EVENT_NAME LIKE '%lock%'
OR e.EVENT_NAME LIKE '%mutex%'
OR e.EVENT_NAME LIKE '%rwlock%'
ORDER BY p.PROCESSLIST_TIME DESC;
如果这条查询返回结果,你就看到了“真凶”:某个会话(比如 PID 123)正在等待一张表(比如 orders)的元数据锁(Metadata Lock)。而持有这张锁的另一个会话(PID 456)可能正在执行一个长时间的事务,或者根本没有提交。
第三步:找出持有锁的“罪魁祸首”
-- 查看当前正在执行且持有锁的长事务或长查询
SELECT
p.PROCESSLIST_ID AS 'PID',
p.PROCESSLIST_USER AS '用户',
p.PROCESSLIST_INFO AS '当前执行的SQL',
p.PROCESSLIST_TIME AS '运行时长(秒)',
t.TRX_STATE AS '事务状态',
t.TRX_STARTED AS '事务开始时间',
t.TRX_ROWS_MODIFIED AS '修改行数',
t.TRX_LOCK_STRUCTURES AS '持有的锁结构数'
FROM information_schema.PROCESSLIST p
LEFT JOIN information_schema.INNODB_TRX t
ON p.PROCESSLIST_ID = t.TRX_ID
WHERE p.PROCESSLIST_TIME > 10 -- 运行超过10秒的
OR t.TRX_STATE IS NOT NULL -- 或者是有活跃 InnoDB 事务的
ORDER BY p.PROCESSLIST_TIME DESC;
这时候你可能会发现,PID 456 执行了一条 DELETE FROM orders WHERE create_time < '2020-01-01',已经跑了 5 分钟,还没提交。这条 SQL 没有进慢查询日志(因为还没结束,或者阈值设得高),但它持有了 orders 表的大量行锁和元数据锁,导致其他所有访问 orders 表的 SQL 都在等待。
第四步:深入分析 SQL 的执行阶段耗时
如果锁问题排除了,CPU 高是因为某条 SQL 真的在疯狂计算,那我们可以用 performance_schema 看这条 SQL 的每一个执行阶段:
-- 假设我们怀疑的 SQL 摘要(digest)是 0x1234...
SELECT
DIGEST_TEXT AS 'SQL模板',
COUNT_STAR AS '执行次数',
SUM_TIMER_WAIT / 1000000000000 AS '总等待时间(秒)',
AVG_TIMER_WAIT / 1000000000000 AS '平均等待时间(秒)',
SUM_ROWS_EXAMINED AS '总扫描行数',
SUM_ROWS_SENT AS '总返回行数'
FROM performance_schema.events_statements_summary_by_digest
WHERE DIGEST_TEXT LIKE '%orders%'
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 5;
如果想看某条具体 SQL 的每一步耗时(比如排序花了多少、临时表花了多少),需要开启 setup_instruments 中的 statement/sql/select 等 instrument,并查询 events_statements_history_long 或 events_statements_current。但要注意,开启详细监控会有轻微性能开销,适合在问题复现时临时开启。
-- 临时开启某个SQL的详细监控(以一条具体SQL为例)
-- 1. 先找到这个SQL的DIGEST_TEXT
-- 2. 然后查询它的步骤
SELECT
EVENT_NAME AS '阶段',
TIMER_START,
TIMER_END,
(TIMER_END - TIMER_START) / 1000000000000 AS '耗时(秒)'
FROM performance_schema.events_stages_current
WHERE NESTING_EVENT_ID IN (
SELECT EVENT_ID
FROM performance_schema.events_statements_current
WHERE DIGEST_TEXT = '你的SQL摘要'
)
ORDER BY TIMER_START;
performance_schema 的强大之处在于实时性和细粒度。它不会等你配置日志、重启服务、积累数据,而是即时反映当前数据库内部的每一丝波动。对于“CPU 飙升”这种实时性要求极高的问题,它是首选。
工具三:sys 库 —— 一键诊断的“小白友好型”仪表盘
如果你觉得 performance_schema 的表结构太复杂,记不住那么多字段,那 sys 库就是你的救星。sys 库是 MySQL 5.7 引入的一个“系统-schema”,它基于 performance_schema 和 information_schema,提供了大量预定义的视图和存储过程,将复杂的性能数据转化为人类可读、一键执行的诊断命令。
简单说,sys 库就是给 performance_schema 穿了件漂亮的衣服,还配了个语音助手。它不增加新功能,但极大地降低了使用门槛。
为什么它能抓到真凶?
因为它提供了场景化的诊断视图。你不需要知道底层表怎么关联,只需要调用 sys.schema_unused_indexes、sys.schema_table_statistics_with_buffer、sys.host_by_cpu 等视图,就能直接得到“哪个表最慢”、“哪个用户最占用 CPU”、“哪些索引没用”等结论。
实战案例:CPU 飙升与锁表的一键排查
还是那个 CPU 99% 的场景,现在我们用 sys 库来“一键”定位。
第一步:查看谁在占用 CPU
-- 按用户维度统计 CPU 使用情况
SELECT
user,
sys.format_time(SUM(cpu_time)) AS '总CPU时间',
COUNT(*) AS '语句数'
FROM sys.x$host_summary
GROUP BY user
ORDER BY SUM(cpu_time) DESC;
如果某个非 mysql.sys 用户的 CPU 时间异常高,你就知道要
