一、 为什么你需要这套“组合拳”?
很多开发人员甚至DBA在面对MySQL性能问题时,第一反应往往是:“卡了,跑个慢查询日志看看。”然后对着几千行的日志发呆,最后无奈地优化了索引,问题却依然偶发。为什么?因为慢查询日志(Slow Log)只是“案发现场的照片”,它告诉你出了什么事,但没告诉你“谁干的”、“为什么干”以及“干了多少次”。
Percona Monitoring and Reports (PMR) 模板提供了实时、可视化的健康指标,能帮你迅速定位“现在哪里痛”;而 pt-query-digest 则是事后深度法医分析工具,能帮你从海量日志中提炼出真正的性能杀手,并按时间窗口、指纹聚合进行归因。
本文将通过一个真实的线上生产案例,带你完整走一遍从监控告警 -> 日志捕获 -> 深度剖析 -> 精准优化 -> 验证闭环的全流程。我们不仅讲理论,更会提供可落地的脚本和命令,确保你看完就能用。
二、 第一阶段:Percona Monitoring 实时定位(发现“谁在痛”)
假设你收到了监控告警:CPU使用率飙升至95%,平均响应时间超过2秒。 此时,你无法立即分析历史日志,你需要知道当前发生了什么。
2.1 核心监控指标解读
Percona Monitoring插件(通常通过Prometheus + Grafana,或Percona Monitoring for MySQL)会暴露以下关键指标。别只看CPU,要看相关性:
| 指标 | 健康值 | 异常信号 | 解读 |
|---|---|---|---|
Threads_running |
< 10 | > 50 | 并发连接数。如果高,说明有查询阻塞了其他查询。 |
Innodb_buffer_pool_read_requests vs Innodb_buffer_pool_reads |
命中率 > 99% | 命中率 < 95% | 缓存失效。大量物理IO,说明热点数据不在内存中。 |
Slow_queries (增量) |
0/s | > 10/s | 慢查询生成速率。这是pt-query-digest要分析的对象。 |
Queries (总QPS) |
稳定 | 剧烈波动 | 查询总量突然增加,可能是突然来了大流量或死循环扫描。 |
Handler_read_rnd_next |
低 | 极高 | 全表扫描。每读一行就要执行一次,这是性能杀手。 |
Lock_wait_time |
0 | 高 | 锁等待。事务持有锁时间过长,或死锁冲突。 |
2.2 实战:用SQL快速定位当前“凶手”
在等待日志采集的同时,你可以直接登录MySQL,运行以下诊断脚本,捕捉当前正在执行的慢查询。
-- 1. 查看当前正在执行的查询(按时间排序,看最久的)
SELECT
ID,
USER,
HOST,
DB,
COMMAND,
TIME,
STATE,
LEFT(INFO, 100) AS SHORT_INFO
FROM information_schema.PROCESSLIST
WHERE COMMAND != 'Sleep'
ORDER BY TIME DESC
LIMIT 20;
-- 2. 关键观察点:
-- - TIME > 0 且正在执行:说明有长耗时查询
-- - STATE = 'Sending data': 通常在读表数据,可能涉及全表扫描或临时表
-- - STATE = 'Sorting result': 正在排序,可能缺少索引
-- - STATE = 'Waiting for table metadata lock': 元数据锁等待,说明有DDL或大事务持有锁
案例场景:
你发现一条SQL:SELECT * FROM orders WHERE create_time > '2023-01-01' AND status = 0,执行了15秒,STATE为Sending data,且Threads_running高达80。这表明该查询在全表扫描后返回大量数据,且阻塞了其他80个请求。
立即行动:不要犹豫,如果业务允许,先KILL掉这个会话,恢复服务。但务必保留会话ID和SQL,这是后续用pt-query-digest分析的依据。
三、 第二阶段:开启并采集慢查询日志(确保证据链完整)
Percona监控告诉你“现在有事”,但你需要历史数据来证明“这个问题持续多久了”、“频率如何”。这需要依赖慢查询日志。
3.1 配置最佳实践
默认配置往往不够用。建议使用以下优化后的配置,写入my.cnf并重启(或动态设置):
[mysqld]
# 开启慢查询日志,阈值设为1秒(根据业务调整,建议0.5-1s)
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1
# 【重要】记录未使用索引的查询(即使它没超过long_query_time)
log_queries_not_using_indexes = 1
# 【重要】记录全表扫描的行数,便于pt-query-digest识别
log_slow_extra = 1
# 【重要】每个查询执行时间,便于分析CPU耗时
log_slow_slave_statement = 1
为什么log_slow_extra和log_queries_not_using_indexes如此重要?
因为很多慢查询并非因为执行时间长,而是因为频率高(如每秒1000次的低效查询),它们可能因为long_query_time设置过高而被忽略。log_queries_not_using_indexes能捕获这些“小而美”的毒药。
3.2 使用Percona工具采集(替代手动复制文件)
不要直接cp慢日志文件,这会导致日志截断问题。使用pt-query-digest直接读取正在写入的日志文件。
# 实时监视慢日志,模拟tail -f
pt-query-digest --review --output=slowlog_file /var/log/mysql/slow.log
但更常见的是,你已经有过去24小时的慢日志文件。接下来,进入核心环节。
四、 第三阶段:pt-query-digest 深度剖析(法医级分析)
pt-query-digest 是Percona Toolkit中最强大的工具,它能将慢日志中的SQL进行指纹化(Fingerprinting),按维度聚合,并生成报告。
4.1 基础命令:生成综合报告
假设你有一个8小时前的慢日志文件slow.log.2023-10-25:
pt-query-digest --report --digest-language=mysql slow.log.2023-10-25 > report.html
重点分析报告中的几个关键部分:
1. Overall:全局视角
看Total列中的Query数量。如果某个指纹的Query次数占总慢查询的80%,那它就是头号嫌疑犯。
2. Top 10 by Avg Wait Time:平均等待时间TOP10
这里不是看执行时间,而是看等待锁的时间(Lock Wait)。如果某个查询Avg Wait很高,说明它被锁住了,或者它锁住了别人。
3. Top 10 by Rows Sent:按返回行数排序
这是最容易被忽视的杀手! 一个查询可能只执行了0.1秒,但如果它返回了100万行,客户端网络带宽、内存都会被打爆。
- 案例:
SELECT id, name FROM users没有LIMIT,返回了全表数据。
4. Fingerprint:指纹对比
pt-query-digest会将SQL参数化,例如:
- 原始SQL1:
SELECT * FROM orders WHERE id = 1 - 原始SQL2:
SELECT * FROM orders WHERE id = 99999 - 指纹:
SELECT * FROM orders WHERE id = ?
这让你能看到同一类查询的整体影响,而不是单个SQL实例。
4.2 高级技巧:按时间窗口切片分析
性能问题往往与业务高峰相关。你可以按小时切片,看看问题是否集中在某个时间点。
# 生成每小时的摘要报告
pt-query-digest --filter "$event->{ts} ge $arg{start} && $event->{ts} le $arg{end}" \
--start 2023-10-25T08:00:00 \
--end 2023-10-25T09:00:00 \
slow.log.2023-10-25
实战发现:你发现08:00-09:00的慢查询主要是某个定时任务(cron job)触发的全表统计查询,而14:00-15:00的慢查询主要是用户下单时的关联查询。
4.3 生成EXPLAIN计划:一步到位
pt-query-digest可以直接对每个指纹生成EXPLAIN计划,无需你手动复制SQL。
# 连接数据库,自动获取EXPLAIN
pt-query-digest --review D=your_db,t.query_review \
--explain D=your_db,h=localhost,u=root,p=your_password \
slow.log.2023-10-25
在输出的HTML报告中,你会看到每个Top查询的完整EXPLAIN结果,包括key(使用的索引)、rows(扫描行数)、Extra(是否有Using filesort或Using temporary)。
五、 第四阶段:常见性能瓶颈与解决方案(结合案例)
通过pt-query-digest的EXPLAIN输出,我们通常能识别出以下几类典型问题。
5.1 全表扫描(Full Table Scan)
现象:type列为ALL,rows数很大,Extra无Using index。
案例:
EXPLAIN SELECT * FROM orders WHERE status = 0 AND create_time > '2023-01-01';
-- 输出: type=ALL, rows=1000000, key=NULL
问题:status字段选择性低(只有0/1/2),create_time没有索引,导致MySQL放弃使用任何索引。
解决方案:
- 创建复合索引:
ALTER TABLE orders ADD INDEX idx_status_time (status, create_time); - 优化查询:如果业务允许,将
SELECT *改为SELECT id, user_id, amount,利用覆盖索引(Covering Index)避免回表。-- 优化后 SELECT id, user_id, amount FROM orders WHERE status = 0 AND create_time > '2023-01-01'; -- EXPLAIN: type=range, key=idx_status_time, Extra=Using where; Using index
5.2 文件排序(Using filesort)
现象:Extra列出现Using filesort,且rows很大。
案例:
EXPLAIN SELECT * FROM orders WHERE user_id = 123 ORDER BY create_time DESC LIMIT 10;
-- 输出: type=ref, key=idx_user_id, rows=5000, Extra=Using filesort
问题:虽然有idx_user_id索引,但排序字段create_time不在索引中,MySQL需要将5000行数据加载到内存中进行排序。
解决方案:
- 联合索引:将排序字段加入索引。
ALTER TABLE orders ADD INDEX idx_user_time (user_id, create_time); -- 优化后EXPLAIN: Extra=Using index condition; Using where (如果用了覆盖索引) - 大结果集分页优化:避免
LIMIT 100000, 10这种深分页。使用游标法(基于上次查询的最大ID):SELECT * FROM orders WHERE user_id = 123 AND create_time < 'last_create_time' ORDER BY create_time DESC LIMIT 10;
5.3 临时表(Using temporary)
现象:Extra列出现Using temporary,通常伴随GROUP BY或DISTINCT。
案例:
EXPLAIN SELECT city, COUNT(*) FROM users GROUP BY city;
-- 输出: Extra=Using temporary; Using filesort
问题:MySQL需要在临时表中存储中间结果,并对临时表进行排序。
解决方案:
- 索引优化:确保
GROUP BY的字段有索引。ALTER TABLE users ADD INDEX idx_city (city); - 避免SELECT * + GROUP BY:只选择需要的字段。
5.4 锁等待与死锁
现象:pt-query-digest报告中Avg Wait Time高,或Lock Time占比大。
案例:
-- 事务A
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- 此时事务B
UPDATE accounts SET balance = balance + 100 WHERE id = 1; -- 阻塞
解决方案:
- 缩短事务:避免在事务中进行长时间的网络请求或计算。
- 一致性读:使用
SELECT ... LOCK IN SHARE MODE或FOR UPDATE时,确保有索引,避免锁住整张表。 - 死锁检测:开启
innodb_deadlock_detect=ON,并在应用层捕获死锁异常进行重试。
六、 第五阶段:验证与持续监控(闭环)
优化完成后,不能只依赖静态的EXPLAIN。你需要验证优化效果。
6.1 使用pt-query-digest对比优化前后
# 保存优化前的报告
pt-query-digest slow.log.before > before_report.html
# 运行一段时间,收集优化后的慢日志
pt-query-digest slow.log.after > after_report.html
# 使用pt-diff对比两个报告
pt-diff before_report.html after_report.html
pt-diff会清晰地告诉你:哪些查询消失了,哪些查询的执行时间下降了,哪些新出现的慢查询需要关注。
6.2 建立长期监控看板
将Percona监控与pt-query-digest定期报告结合:
- 每日自动生成:使用cron定时任务,每天对前一天的慢日志运行
pt-query-digest,并将报告发送到你的邮箱或钉钉/Slack。0 8 * * * pt-query-digest --report /var/log/mysql/slow.log.$(date -d yesterday +%Y-%m-%d) | mail -s "MySQL Slow Log Report" dba@company.com - Grafana告警:设置
Slow_queries增量告警,如果某小时内慢查询数量超过阈值(如100个),立即通知。
七、 给初学者的小结与心态建议
- 不要迷信单一工具:Percona监控看现在,
pt-query-digest看过去。两者结合,才能既有即时响应能力,又有深度复盘能力。 - 索引是银弹,但不是万能药:加索引能解决80%的慢查询,但要注意索引维护成本(写性能下降)和选择性(低基数字段无效)。
- 理解“为什么”:看到
Using filesort,不要只想着加索引,要问自己:为什么需要排序?数据量有多大?能否在应用层排序? - 保持客观:性能优化是一个迭代过程。今天的最优解,明天可能因为数据量增长而失效。持续监控,持续调整。
记住,MySQL性能优化不是玄学,而是一门基于数据的科学。pt-query-digest就是你的显微镜,Percona监控就是你的雷达。用好它们,你就能在数据的海洋中,精准地捕捞到那些偷走你系统性能的黑客。
最后,送大家一句话:在优化之前,先问自己:“我真的知道慢在哪里吗?” 如果答案是“否”,请先运行pt-query-digest。
