那天下午三点,生产环境的MySQL CPU突然飙到了95%,告警短信像雪片一样飞进手机。值班群里炸开了锅,有人说是业务高峰期到了,有人怀疑是某条SQL出了大问题。作为DBA,我最怕的不是告警,而是“不知道去哪找原因”。
以前遇到这种情况,我会陷入一种混乱的排查流程:先看top确认是不是MySQL进程在吃CPU,然后htop看看是哪个线程在忙,接着连进数据库执行SHOW PROCESSLIST,看到一堆SLEEP和Query状态的连接,最后还得去翻慢查询日志。如果慢查询日志没开,或者开得不够细,那就只能干瞪眼。
但自从我们搭建了从pt-query-digest到Percona Monitoring Plugins(PMP)的完整监控体系,整个排查过程变得像开挂一样顺滑。今天就把这套实战经验毫无保留地分享出来,不仅告诉你怎么做,还要让你明白为什么这么做。
慢查询日志:数据库的黑匣子
在谈工具之前,我们必须先理解MySQL自身提供的最基础能力——慢查询日志。这玩意儿就像是飞机的黑匣子,记录着每一笔“异常飞行”的详细数据。
开启慢查询日志的正确姿势
很多运维同学开慢查询日志的方式非常简单粗暴:
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;
这样确实能打开日志,但有个大坑:重启MySQL服务后,所有配置全部丢失。这意味着如果你不在my.cnf里持久化配置,下次重启服务器,慢查询日志就没了,之前的排查依据全部白费。
正确的做法是在my.cnf中这样配置:
[mysqld]
# 开启慢查询日志
slow_query_log = 1
# 慢查询日志文件路径
slow_query_log_file = /var/log/mysql/slow-query.log
# 超过1秒的查询记录为慢查询(根据业务调整)
long_query_time = 1
# 记录没有使用索引的查询(非常有用的补充)
log_queries_not_using_indexes = 1
# 记录管理语句(如ALTER TABLE等)
log_slow_admin_statements = 1
# 记录执行时间超过min_examined_row_limit的查询
min_examined_row_limit = 100
注意几个关键点:
第一,slow_query_log_file的路径要确保MySQL进程有写权限,否则启动时会报错。
第二,long_query_time的设置要因地制宜。对于OLTP系统,1秒可能已经太宽松了;对于数据分析类系统,可能需要设置到5秒甚至更长。建议先设置为1秒,观察一周后再根据实际情况调整。
第三,log_queries_not_using_indexes这个参数被很多人忽视,但它非常有用。有些查询虽然执行时间很短,但可能全表扫描了几百万行数据,这种查询在并发量上来后会成为性能杀手。开启这个参数后,所有未使用索引的查询都会被记录,即使它们的执行时间很短。
慢查询日志的格式解析
开启慢查询日志后,你会看到类似这样的输出:
# Time: 2024-01-15T14:32:18.123456Z
# User@Host: app_user[app_user] @ localhost [] Id: 12345
# Query_time: 2.345678 Lock_time: 0.000123 Rows_sent: 100 Rows_examined: 5432100
SET timestamp=1705329138;
SELECT * FROM orders WHERE user_id = 12345 AND status = 'pending' ORDER BY create_time DESC LIMIT 20;
每一行都有它的含义:
Time:查询结束的时间戳User@Host:执行查询的用户和来源主机Query_time:查询执行的总时间,包括等待锁的时间Lock_time:等待锁的时间Rows_sent:返回给客户端的行数Rows_examined:服务器扫描的行数
这里有个非常重要的指标对比:Rows_examined和Rows_sent的比值。如果Rows_examined是Rows_sent的几百倍甚至几千倍,说明这个查询效率极低,可能在全表扫描。比如上面这个例子,扫描了543万行,只返回了100行,这个查询大概率需要优化。
pt-query-digest:慢查询日志的终极分析器
开了慢查询日志只是第一步,真正的问题是如何从大量的日志记录中快速定位问题。Manual阅读慢查询日志是不现实的,因为日志可能每天增长几十GB。这时候就需要Percona Toolkit中的神器——pt-query-digest。
安装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-query.log
这个命令会输出一个详细的分析报告,包含每个查询的统计信息、执行时间分布、锁等待时间、扫描行数等。但输出的文本格式在终端中阅读体验并不好,特别是当日志量很大的时候。
输出HTML报告:可视化分析的首选
为了获得更好的阅读体验,强烈建议输出HTML格式的报表:
pt-query-digest /var/log/mysql/slow-query.log --report --report-format=html > /var/www/html/slow-query-report.html
这样生成的HTML报告可以通过浏览器打开,交互体验非常好。报告中包含以下几个核心部分:
总体统计(Overall section):展示所有查询的总体概况,包括总查询次数、总执行时间、平均执行时间、P99执行时间等。
Top 5由执行时间决定的查询:按执行时间排序的前5个查询模板,这些是最需要优化的目标。
Top 5由扫描行数决定的查询:按Rows_examined排序的前5个查询,这些查询可能效率低下。
Top 5由锁等待时间决定的查询:按Lock_time排序的前5个查询,这些查询可能存在锁竞争问题。
每个查询的详细分析(Query N section):对每个唯一的查询模板进行详细分析,包括执行频率、执行时间分布、扫描行数分布、返回行数分布等。
高级用法:按时间范围分析
在实际生产环境中,我们往往需要分析特定时间段的慢查询。比如告警发生时的前后10分钟,这时候可以使用--since和--until参数:
pt-query-digest /var/log/mysql/slow-query.log \
--since "2024-01-15 14:30:00" \
--until "2024-01-15 14:40:00" \
--report --report-format=html > slow-query-report-14-30-14-40.html
这个功能非常实用,可以帮助你在告警发生后快速定位当时的慢查询。
高级用法:按查询类型分组
有时候我们想看看某类查询的整体情况,比如所有SELECT查询、所有UPDATE查询,或者所有访问特定表的查询。pt-query-digest支持通过--filter参数进行过滤:
# 只分析SELECT查询
pt-query-digest /var/log/mysql/slow-query.log --filter "\$event->{arg} =~ m/^SELECT/i" --report --report-format=html > select-queries.html
# 只分析访问orders表的查询
pt-query-digest /var/log/mysql/slow-query.log --filter "\$event->{arg} =~ m/orders/i" --report --report-format=html > orders-table-queries.html
高级用法:导出到Percona Monitoring and Management
如果你已经部署了PMM(Percona Monitoring and Management),可以将pt-query-digest的分析结果直接导入到PMM中,实现统一管理:
pt-query-digest /var/log/mysql/slow-query.log --dest pmm:mysql
这里pmm:mysql表示使用PMM内置的MySQL客户端连接到PMM服务器。需要注意的是,PMM服务器需要先配置好MySQL数据采集。
Percona Monitoring Plugins:从被动分析到主动监控
pt-query-digest是一个强大的分析工具,但它的局限在于事后分析。你需要先有慢查询日志,然后才能分析。而在告警发生的当下,你更需要的是实时监控和趋势分析,这样才能在问题发生前就发现异常。
这就是Percona Monitoring Plugins(PMP)的价值所在。PMP是一套专门为Percona Server和MySQL设计的监控脚本和插件,可以与Prometheus、Grafana等主流监控系统集成,实现实时性能监控。
PMP的核心价值
实时监控:PMP提供了一系列监控脚本,可以定期采集MySQL的性能指标,包括QPS、TPS、连接数、慢查询数、锁等待、缓冲池命中率等。
告警触发:基于采集的指标,可以设置告警规则。比如当慢查询数在1分钟内超过100个时触发告警,当缓冲池命中率低于95%时触发告警。
趋势分析:PMP采集的数据存储在Prometheus中,可以通过Grafana进行长期的趋势分析。你可以看到一周前、一个月前的性能基线,从而判断当前的性能是否正常。
问题定位:PMP不仅提供宏观指标,还通过pmm-admin工具提供细粒度的性能分析能力,包括当前活跃查询、锁等待情况、InnoDB状态等。
部署PMP监控
部署PMP监控的方式有两种:一种是部署独立的PMM服务器,另一种是直接在MySQL主机上安装PMP代理。
方式一:部署PMM服务器(推荐用于生产环境)
PMM Server是一个完整的监控平台,提供Web界面、数据存储、告警管理等功能。可以通过Docker快速部署:
docker run -d \
--name pmm-server \
-p 443:443 \
-v /opt/consul-data:/opt/consul-data \
-v /var/lib/mysql:/var/lib/mysql \
-v /var/lib/grafana:/var/lib/grafana \
-v /opt/nightlies:/opt/nightlies \
percona/pmm-server:2
部署完成后,通过浏览器访问https://<server-ip>,使用默认账号密码登录(admin/admin,首次登录会要求修改密码)。
然后在MySQL主机上安装PMM Client:
# CentOS/RHEL
yum install percona-pmm2-client
# Ubuntu/Debian
apt-get install percona-pmm2-client
安装完成后,将MySQL实例添加到PMM监控:
pmm-admin config --server-insecure-tls --server-url=https://admin:password@<pmm-server-ip>
pmm-admin add mysql --user=root --password=root_password --port=3306
方式二:直接集成Prometheus + Grafana(适合已有监控体系的环境)
如果你的环境已经部署了Prometheus和Grafana,可以直接在MySQL主机上安装PMP脚本,并将指标暴露给Prometheus。
首先安装PMP:
# CentOS/RHEL
yum install percona-monitoring-plugins
# Ubuntu/Debian
apt-get install percona-monitoring-plugins
然后创建Prometheus的scrape配置,指向PMP提供的指标端点(默认是http://localhost:9104/metrics):
scrape_configs:
- job_name: 'mysql'
static_configs:
- targets: ['<mysql-host>:9104']
最后在Grafana中导入Percona提供的官方Dashboard(Dashboard ID通常为7362),就可以可视化监控MySQL的各项性能指标了。
PMP提供的关键监控指标
PMP提供了非常丰富的监控指标,这里挑选几个对慢查询问题定位最有价值的指标进行说明。
慢查询相关指标
mysql_slow_queries_total:累计慢查询总数mysql_slow_queries_rate:慢查询速率(每秒)mysql_slow_queries_time_sum:慢查询总执行时间mysql_slow_queries_time_avg:慢查询平均执行时间
这些指标可以帮助我们实时监控慢查询的发生频率和严重程度。如果mysql_slow_queries_rate突然飙升,说明可能有异常查询在运行。
锁等待相关指标
mysql Innodb_row_lock_time:InnoDB行锁等待总时间mysql Innodb_row_lock_waits:行锁等待次数mysql Innodb_row_lock_time_avg:平均每次行锁等待时间
锁等待过高是MySQL性能问题的常见原因,这些指标可以帮助我们及时发现锁竞争问题。
缓冲池相关指标
mysql Innodb_buffer_pool_pages_free:缓冲池中空闲的页数mysql Innodb_buffer_pool_read_requests:缓冲池读取请求次数mysql Innodb_buffer_pool_reads:需要从磁盘读取的页数
缓冲池命中率是MySQL性能的关键指标。如果Innodb_buffer_pool_reads相对于Innodb_buffer_pool_read_requests的比例很高,说明缓冲池太小或者工作集太大,需要调整innodb_buffer_pool_size。
连接相关指标
mysql Threads_connected:当前连接的线程数mysql Threads_running:当前正在执行的查询线程数mysql Max_used_connections:历史上同时使用的最大连接数
连接数过多会导致MySQL资源争用,影响性能。通过监控这些指标,可以及时发现连接数异常增长的问题。
实战:从告警到定位的完整流程
光说不练假把式。下面我用一个真实的案例,完整演示从收到告警到定位慢查询瓶颈的全过程。
场景背景
某电商平台的MySQL主库,使用Percona Server 8.0,部署了PMM监控。某天下午,监控平台收到告警:MySQL服务器CPU使用率超过90%,持续5分钟。
第一步:查看实时监控指标
收到告警后,我首先登录Grafana,查看MySQL的综合Dashboard。重点关注以下几个面板:
General panel:显示QPS、TPS、连接数的时间序列。我看到QPS在告警时间点确实有小幅上升,但不是导致CPU飙升的主要原因。
CPU panel:显示MySQL进程的CPU使用率。确实如告警所示,CPU使用率维持在90%以上。
Threads panel:显示线程状态。我看到Threads_running从平时的5-10个突然飙升到50+,这说明有大量查询正在并发执行。
Slow queries panel:显示慢查询速率。果然,慢查询速率从平时的0-1个/秒飙升到20+个/秒。
这些宏观指标告诉我们:问题不是连接数过多,也不是QPS暴涨,而是有少数几个查询执行效率极低,占用了大量CPU资源。
第二步:查看当前活跃查询
为了进一步定位问题,我执行了以下SQL查看当前正在运行的查询:
SELECT
ID,
USER,
HOST,
DB,
COMMAND,
TIME,
STATE,
LEFT(INFO, 100) AS INFO
FROM information_schema.PROCESSLIST
WHERE COMMAND != 'Sleep'
ORDER BY TIME DESC;
结果让我大吃了一惊:排在第一位的是一个SELECT查询,已经运行了120秒,INFO显示:
SELECT o.*, u.name, u.email FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.create_time > '2023-01-01'
ORDER BY o.total_amount DESC
LIMIT 20
这个查询的问题很明显:它需要对orders表进行全表排序,然后根据total_amount排序后取前20行。如果orders表有1000万行数据,这个排序操作会消耗大量的CPU和内存。
第三步:分析慢查询日志
虽然通过SHOW PROCESSLIST已经看到了问题查询,但我还需要确认这个查询是否是偶尔出现还是持续出现,以及是否有其他类似的慢查询。
我使用pt-query-digest分析最近1小时的慢查询日志:
pt-query-digest /var/log/mysql/slow-query.log \
--since "1 hour ago" \
--filter "\$event->{arg} =~ m/^SELECT/i" \
--report --report-format=html \
> slow-query-report-1h.html
打开生成的HTML报告,我看到了以下关键信息:
Top 1 by Exec time:正是刚才看到的那个查询,平均执行时间3.2秒,占所有慢查询执行时间的78%。
Top 1 by Rows sent:同样是这个查询,平均返回20行,但扫描了500万行。
Top 1 by Rows examined:还是这个查询,扫描行数高达500万。
Top 1 by Lock time:这个查询的锁等待时间为0,说明问题不在锁竞争,而在查询本身效率低。
第四步:查看执行计划
为了确认问题,我执行了EXPLAIN分析这个查询:
EXPLAIN SELECT o.*, u.name, u.email FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.create_time > '2023-01-01'
ORDER BY o.total_amount DESC
LIMIT 20;
执行计划显示:
orders表使用了create_time索引进行范围扫描,这是正确的。- 但排序操作使用了
filesort,这意味着MySQL需要在内存或磁盘中进行外部排序,而不是通过索引直接获得有序结果。 type为ref,说明JOIN操作使用了索引,这是好的。
问题就出在这个filesort上。虽然create_time索引过滤掉了大部分数据,但剩余的记录仍然需要进行排序,而排序操作无法利用索引。
第五步:优化方案
针对这个问题,我提出了三个优化方案:
方案一:添加覆盖索引
如果业务只需要orders表的部分字段,可以创建一个覆盖索引,避免回表:
”`sql
