你是不是也遇到过这种崩溃的瞬间?
晚上11点,手机突然响了,业务方发来消息:“线上卡爆了,用户全在骂娘。”你打开监控,CPU飙到95%,QPS断崖式下跌,业务接口响应时间从200ms直接跳到了8秒以上。这时候,第一反应不是慌,而是——冷静地打开那台让你又爱又恨的MySQL服务器。
说实话,很多工程师在处理这类问题时,第一步就错了:直接上SHOW PROCESSLIST,看着一堆Sleep和Query的线程发懵,或者盲目地重启服务(虽然有时候管用,但治标不治本)。今天,我想跟你聊聊一条真正能落地的排查路径:从慢查询日志入手,用Percona Toolkit里的神器pt-query-digest,把隐藏的MySQL性能瓶颈揪出来,彻底解决卡顿问题。
为什么是慢查询日志?它是MySQL的“黑匣子”
想象一下,飞机出事后的黑匣子。MySQL也有自己的黑匣子,那就是慢查询日志(Slow Query Log)。它记录的是那些“跑得慢”的SQL语句——通常是执行时间超过阈值的查询。
但这里有个坑:默认开启的慢查询日志,往往没多大用。为什么?因为很多人只是把slow_query_log=1打开,然后啥也不管,结果日志文件几天后变得几个G大,里面全是些执行时间0.1秒的“伪慢查询”。等你真出问题了,想查的时候,要么日志被轮转删掉了,要么淹没在海量数据里找不到重点。
所以,第一步不是“开日志”,而是正确地配置日志。
配置慢查询日志:别偷懒,参数要设对
在MySQL配置文件(my.cnf或my.cnf)中,你需要关注这几个关键参数:
[mysqld]
# 开启慢查询日志,路径随意,但建议放在独立磁盘
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
# 记录慢查询的时间阈值,单位是秒。
# 注意:很多新人设为0,这会记录所有查询,包括0.01秒的SELECT,日志直接爆炸
long_query_time = 1
# 关键!记录没有使用索引的查询。
# 很多卡顿问题根源就是全表扫描,这个开关能帮你抓住它们
log_queries_not_using_indexes = 1
# 可选:全局锁定的最小时间,记录需要长时间LOCK的语句
log_slow_admin_statements = 1
设完之后,记得重启MySQL或者动态修改(如果版本支持):
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;
SET GLOBAL log_queries_not_using_indexes = 'ON';
然后,你可以去验证一下是否生效:
SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'log_queries_not_using_indexes';
这时候,你就可以去观察了。等日志里真的出现几条记录时,别急着分析原始日志。因为原始的慢查询日志是人读的格式,对机器友好,对人不太友好。每个查询都长这样:
# Time: 2023-10-27T10:15:32.123456Z
# User@Host: app_user[app_user] @ localhost [] Id: 1234
# Query_time: 2.345678 Lock_time: 0.000123 Rows_sent: 1 Rows_examined: 987654
SET timestamp=1698394532;
SELECT * FROM orders WHERE status = 'pending' AND create_time > '2023-01-01';
注意看Rows_examined: 987654,而Rows_sent: 1。这就很说明问题了:查了98万行,只返回1行。这基本上就是性能杀手。但问题是,线上可能有成千上万条这样的日志,你怎么看?
这时候,就需要请出今天的真正主角了。
引入Percona Toolkit:不是所有工具都叫神器
Percona Toolkit是一组命令行工具,专为MySQL性能分析和故障排查设计。它由Percona公司开发(这家公司在MySQL领域是权威级存在),其中pt-query-digest是整个工具箱里的“核武器”。
为什么不用mysqldumpslow? MySQL自带的这个工具也能分析慢查询日志,但它太简陋了。它只能做简单的聚合,看不出查询的时间分布规律,处理不了的哈希模式,也没有网络传输能力。而pt-query-digest能把慢查询日志变成一个结构化的分析报告,甚至能生成图表、导出到数据库、通过网络流式传输。
安装pt-query-digest
首先,你需要安装Percona Toolkit。在CentOS/RHEL上:
yum install percona-toolkit
在Ubuntu/Debian上:
apt-get install percona-toolkit
安装好后,验证一下版本:
pt-query-digest --version
实战:用pt-query-digest分析慢查询日志
假设你的慢查询日志路径是/var/log/mysql/slow.log,文件很大,有500MB。直接打开看?别做梦了。
第一步:基础分析,快速看清全局
最简单的用法,直接跑:
pt-query-digest /var/log/mysql/slow.log
这会让你看到一个超级详细的文本报告。报告分为几个部分:Overall、Query、Top 10 等等。
Overall部分会告诉你:
- 总共有多少条查询
- 时间范围
- 最大、平均、最小执行时间
- 哪些查询模式(Query ID)出现最多
- 哪些查询最耗时
比如,你可能会看到类似这样的输出:
# Profile
# Rank Query ID Response time Calls V/M Query
# ---- ----------- ------------- ------- ------ -----
# 1 0xA1B2C3D4 45.2341 (85.0%) 120 0.38 SELECT orders with status filter
# 2 0xE5F6G7H8 4.1234 (7.7%) 50 0.15 SELECT users join profiles
# 3 0xI9J0K1L2 2.5678 (4.8%) 30 0.09 UPDATE orders SET status = ?
# ...
这里的V/M是变异系数(Variability/Mean),越高说明这个查询的执行时间越不稳定,可能受负载影响大。
重点看Rank 1的查询:它占了总响应时间的85%,执行了120次。这就是当前的性能瓶颈所在。
第二步:深入挖掘,看清具体SQL
光看ID不够,你得知道它到底执行了什么SQL。加--group-by fingerprint参数,可以按查询指纹分组:
pt-query-digest --group-by fingerprint /var/log/mysql/slow.log
什么是fingerprint?就是SQL的模式化版本。比如:
SELECT * FROM orders WHERE status = 'pending' AND create_time > '2023-01-01';
SELECT * FROM orders WHERE status = 'shipped' AND create_time > '2023-06-01';
这两条SQL看起来不同,但fingerprint会把字面量替换成占位符,变成:
SELECT * FROM orders WHERE status = ? AND create_time > ?
这样,相同模式的查询就被聚合在一起了。这对于分析非常有用,因为线上往往只有几种SQL模式在跑,但参数不同。
第三步:生成报告,导出为HTML或PDF
如果同事或者老板要看,给他们一堆命令行输出他们肯定不满意。pt-query-digest支持生成HTML报告:
pt-query-digest --report /var/log/mysql/slow.log > report.html
然后打开report.html,你会看到一个非常美观的图表界面,包括:
- 查询时间分布图
- 各查询模式的耗时占比饼图
- 具体SQL语句的详细信息
- 索引使用情况分析
这对于汇报工作非常友好。
第四步:从慢查询日志中提取最坏的查询
有时候,你只关心最坑的那几条SQL。可以用--order-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
--limit 10%意思是只显示最耗时的10%的查询模式,这样可以过滤掉那些偶发的、影响不大的慢查询。
关键指标解读:如何判断瓶颈?
拿到报告后,别急着改代码,先看懂指标。以下是几个核心指标:
1. Query_time:执行时间
这是最直观的。但要注意区分:
- Query_time:查询总耗时,包括网络传输、锁等待等。
- Lock_time:等待锁的时间。如果这个值很大,说明有锁竞争。
- Rows_sent:返回的行数。
- Rows_examined:扫描的行数。
黄金法则:如果Rows_examined远大于Rows_sent,说明查询效率极低,大概率是没有用到索引,或者索引设计不合理。
比如,你看到一条查询Rows_examined: 1000000,Rows_sent: 10,那就是在告诉你要加索引。
2. Exec_time中的P99
报告里通常会给出执行时间的百分位数,比如P99(99%的查询都低于这个时间)。如果P99很高,说明有少数查询特别慢,可能是数据倾斜或者热点数据问题。
3. Fingerprint的稳定性
如果一个查询模式的执行时间波动很大(V/M高),可能原因有:
- 数据量变化大
- 缓存命中率不稳定
- 锁竞争
这时候,需要结合业务场景分析,比如是否是批量操作导致。
实战案例:从日志到解决方案
让我们模拟一个真实场景。
场景描述
线上电商系统,订单查询接口响应时间从200ms飙升到5秒。用户反馈页面卡死。DBA接到报警,开始排查。
步骤1:确认慢查询日志已开启
检查my.cnf,确认slow_query_log=1,long_query_time=1。发现之前设的阈值是0.5秒,但线上查询大部分在0.8秒左右,导致日志量巨大。调整为1秒,减少噪音。
步骤2:运行pt-query-digest
pt-query-digest --group-by fingerprint --order-by query_time:sum --limit 10% /var/log/mysql/slow.log > db_report.html
打开报告,发现排名第一的查询模式是:
SELECT o.*, u.name, u.email
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.status = ?
AND o.create_time >= ?
ORDER BY o.create_time DESC
LIMIT 20
这个查询的总执行时间占了80%,Rows_examined平均是50万,而Rows_sent只有20。
步骤3:分析原因
为什么这么慢?
- 全表扫描:
orders表有500万数据,status和create_time都没有复合索引。 - JOIN效率低:
users表虽然小,但每次JOIN都要回表。 - 排序开销大:
ORDER BY create_time DESC在没有索引的情况下,需要临时表排序。
步骤4:解决方案
方案一:添加复合索引
ALTER TABLE orders
ADD INDEX idx_status_create_time (status, create_time);
这样,查询可以直接通过索引过滤status和create_time,避免全表扫描。
方案二:优化JOIN
如果users表经常JOIN,可以考虑冗余字段,或者使用子查询优化。
方案三:避免大分页
如果查询有LIMIT,确保索引能覆盖排序字段。
步骤5:验证效果
重新生成报告,对比优化前后的指标。通常,Rows_examined会从50万降到几千,执行时间从5秒降到100毫秒以内。
进阶技巧:实时监控和告警
pt-query-digest不仅可以分析历史日志,还可以实时监控股份库的慢查询。
方法1:从进程列表实时捕获
pt-query-digest --processlist --no-report /var/log/mysql/slow.log
这个命令会持续监控MySQL的进程列表,捕获慢查询并追加到日志中。适合在线分析,不需要停机。
方法2:结合定时任务自动化分析
可以写一个cron脚本,每天凌晨分析前一天的慢查询日志,并发送邮件报告:
0 2 * * * pt-query-digest --report --group-by fingerprint /var/log/mysql/slow.log | mail -s "MySQL Slow Query Report" dba@company.com
方法3:导出到数据库,方便查询
如果你有很多天的日志,可以导出到数据库里,方便用SQL进一步分析:
pt-query-digest --review D=percona,h=127.0.0.1,P=3306,u=root,p=password --create-review-table /var/log/mysql/slow.log
这会创建一个query_review表,存储所有查询的指纹、执行时间、rows_examined等信息,你可以随时用SQL查询:
SELECT * FROM query_review
ORDER BY query_time_sum DESC
LIMIT 10;
常见陷阱和注意事项
陷阱1:索引不是万能的
加了索引不一定快。如果索引选择性低(比如status只有几个值),索引效果有限。这时候,可能需要考虑分区表或者更改业务逻辑。
陷阱2:忽略锁等待
有时候查询慢不是因为SQL本身,而是因为等待锁。用pt-query-digest分析时,注意Lock_time指标。如果锁等待时间长,可能需要优化事务粒度,或者调整隔离级别。
陷阱3:日志轮转导致数据丢失
生产环境的慢查询日志通常会轮转(rotate)。如果不及时分析,日志可能被覆盖。建议设置自动化脚本,每天备份并分析。
陷阱4:过度依赖pt-query-digest
它是分析工具,不是修复工具。分析报告后,还需要DBA或开发人员进行SQL优化、索引调整、配置修改等实际操作。
结语:从被动救火到主动预防
回到开头那个场景。如果你们团队能养成定期分析慢查询日志的习惯,用pt-query-digest建立监控基线,很可能在问题爆发前就发现异常。比如,发现某个查询的Rows_examined在逐渐上升,就可以提前优化,而不是等到线上卡顿才去救火。
MySQL性能优化不是一蹴而就的,它是一个持续的过程。pt-query-digest帮你做的,是把复杂的日志数据变成清晰的可操作信息。掌握了它,你就有了从海量日志中快速定位瓶颈的能力。
下次再遇到“线上卡爆了”的报警,不妨深呼吸,打开终端,运行那行pt-query-digest命令。你会发现,原来问题没那么可怕。
毕竟,在数据的海洋里,我们不是在盲人摸象,而是在用正确的工具,看清每一个字节。
