嘿,朋友。咱们今天不聊那些枯燥的理论定义,直接切入正题。想象一下,现在已经是深夜两点,你的线上系统突然像被施了定身咒一样,页面加载转圈圈,用户投诉电话打爆了客服部。作为后端开发或者DBA,这时候你最需要的不是慌,而是一套像侦探破案一样严密的排查思路。
很多人觉得MySQL慢就是“加索引”或者“升配置”,这当然没错,但治标不治本。真正的瓶颈往往隐藏在那些看不见的地方:是锁竞争?是全表扫描?还是网络IO在捣鬼?今天,我就带你从最底层的慢查询日志(Slow Query Log)抓起,一路打通到Prometheus + Grafana的可视化大屏,手把手教你构建一套“上帝视角”的性能监控体系。咱们要把这个黑盒变成透明的玻璃房。
第一步:开启慢查询日志——捕捉“犯罪现场”的证据
一切性能问题的起点,通常都源于那条该死的SELECT语句执行时间超过了预期。MySQL自带的慢查询日志(SLOW_QUERY_LOG)就是第一道防线。很多新手只开了日志,却忘了配置阈值,结果导致日志文件一夜之间爆满,甚至拖垮磁盘IO,这就本末倒置了。
1. 精准配置:不要“大海捞针”,要“有的放矢”
在生产环境,我们不需要记录所有查询,只需要记录那些真正耗时的。建议通过动态参数调整(无需重启MySQL,但需SUPER权限):
-- 开启慢查询日志
SET GLOBAL slow_query_log = 'ON';
-- 设置慢查询阈值为 1 秒。注意:这里单位是秒,支持小数,如 0.5 代表 500ms
SET GLOBAL long_query_time = 1;
-- 关键一步:只记录没有使用索引的查询,即使它很快。这能帮你发现全表扫描的隐患
SET GLOBAL log_queries_not_using_indexes = 'ON';
-- 设置日志路径,确保MySQL进程有写入权限
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';
专家提示:log_queries_not_using_indexes 这个开关非常有用。有时候一个查询只花了 0.1 秒,但它走了全表扫描。如果数据量翻倍,它可能瞬间变成 2 秒。这种“潜在炸弹”比那些偶尔卡顿的复杂查询更危险。
2. 解读慢查询日志:不仅仅是看时间
日志里长这样:
# Time: 2023-10-27T14:30:00.000000Z
# User@Host: app_user[app_user] @ localhost [] Id: 12345
# Query_time: 2.500000 Lock_time: 0.000100 Rows_sent: 1 Rows_examined: 500000
SET timestamp=1698397800;
SELECT * FROM orders WHERE user_id = 10086;
别光盯着Query_time看。我们要关注三个核心指标:
- Query_time vs Lock_time:如果
Lock_time占比很高,说明你的事务冲突严重,可能是行锁或表锁导致的等待。 - Rows_examined vs Rows_sent:这是最直观的指标。如果查了50万行才返回1行,那绝对是索引失效或者逻辑设计有问题。理想情况下,这两个数字应该接近。
- Full Scan Flag:如果开启了
log_queries_not_using_indexes,这条日志旁边可能会有# Query_time相关的标记,提醒你没用索引。
3. 使用 mysqldumpslow 或 pt-query-digest 分析
手动看日志太痛苦了。对于小团队,可以用MySQL自带的mysqldumpslow:
# 按查询时间排序,查看前10条最慢的
mysqldumpslow -s t -t 10 /var/log/mysql/slow.log
但对于生产环境,我强烈推荐使用Percona Toolkit里的pt-query-digest。它能将日志转化为JSON或表格,自动聚类相似的SQL语句,并给出详细的统计信息(如最小、最大、平均执行时间)。
# 分析慢查询日志,生成摘要报告
pt-query-digest /var/log/mysql/slow.log > slow_report.txt
你会看到类似这样的输出,清晰地告诉你哪类SQL占据了80%的时间:
# Rank ID Query ID Response Time Calls R/Call V/M Item
# ==== ===================================== ============== ===== ====== ==== ==== ========
# 1 0x... 3.5000 (25.0%) 140 0.0250 0.00 SELECT users WHERE email = ?
这一步,你就完成了从“模糊感觉”到“具体SQL”的缩小范围过程。
第二步:深入内核——性能模式(Performance Schema)与 Sys Schema
慢查询日志是事后诸葛亮,而Performance Schema则是实时监控。从MySQL 5.5开始引入,5.7和8.0大幅增强。它就像给MySQL装了无数个传感器,记录每一颗螺丝钉的震动。
1. 为什么不用SHOW PROCESSLIST就够了?
SHOW PROCESSLIST只能看到当前正在执行的线程状态(Sleep, Running, Query等),它是个快照,而且信息极其有限。你不知道某个连接为什么卡住,也不知道它的内存消耗。
2. 利用 sys 库快速定位热点
MySQL 5.7+ 提供了一个名为sys的数据库,它对performance_schema进行了友好的封装,让非DBA也能看懂。
查看当前最耗CPU的SQL:
SELECT * FROM sys.statements_with_runtimes_in_95th_percentile ORDER BY exec_count DESC LIMIT 10;查看等待事件最多的SQL(IO或锁阻塞):
SELECT * FROM sys.statements_with_wait_classes ORDER BY total_latency DESC LIMIT 10;这里会告诉你,SQL是在等待IO(
wait/io/file/innodb/data),还是在等待锁(wait/lock/table/sql/handle)。如果是锁等待,直接去查sys.innodb_lock_waits:SELECT * FROM sys.innodb_lock_waits;这张表会直接告诉你:谁在等谁?阻塞链是怎样的?你可以直接拿到阻塞者的
PROCESSLIST_ID,然后KILL掉那个不合理的长事务。
3. 内存与临时表分析
有时候卡顿不是因为SQL慢,而是因为MySQL在拼命做临时表排序,或者内存溢出导致频繁换页。
-- 查看使用了临时表的SQL
SELECT * FROM sys.statements_with_temp_tables ORDER BY tmp_tables DESC LIMIT 5;
如果tmp_tables很高,且tmp_disk_tables(写到磁盘的临时表)也很高,那你的sort_buffer_size可能太小,或者SQL里没有合适的索引导致必须文件排序。
第三步:操作系统层面——别让Linux成为瓶颈
MySQL跑在Linux上,如果操作系统本身都喘不过气,MySQL再优化也没用。很多时候,所谓的“MySQL慢”,其实是磁盘IO瓶颈或网络延迟造成的。
1. CPU与上下文切换
使用top或htop看整体负载。但要注意,load average高不一定代表CPU忙,也可能是大量进程在等待IO。
重点观察us(用户态)、sy(内核态)和wa(IO等待)。如果wa很高,说明磁盘读写跟不上,MySQL线程都在空转等待数据。
关键指标:Context Switches(上下文切换)
如果每秒上下文切换次数(cs)过高(例如超过10000次/秒),说明CPU花在切换线程上的时间比执行代码还多。这通常是因为线程数过多,或者锁竞争激烈。
# 查看上下文切换统计
vmstat 1 5
2. 磁盘IO深度解析
iostat -x 1 是你的好朋友。
- %util:磁盘利用率。如果接近100%,说明磁盘饱和。
- await:平均每次IO请求的等待时间。如果这个值很高(例如超过10ms),说明IO子系统压力大。
- svctm:服务时间。
对于InnoDB引擎,最重要的是确认你的数据文件、日志文件(redo log, binlog)是否分布在不同的物理磁盘上。如果都在同一个机械硬盘上,随机读写性能会灾难性地下降。SSD是必须的,RAID卡要有电池保护(BBU)。
3. 内存与Swap
检查free -h。如果swap使用量不为0,恭喜你,你的物理内存不够用了,系统正在把MySQL的数据页交换到磁盘,这会带来毁灭性的性能下降。
# 检查是否有交换发生
cat /proc/vmstat | grep pgswapin
如果有频繁的pgswapin/out,必须增加物理内存,或者调整swappiness参数。
第四步:全局视野——Prometheus + Grafana 可视化监控
当你有了单点排查的能力,接下来需要的是全局监控。当问题发生时,你能在Grafana上看到过去一周的趋势,而不是只能看当下的日志。这是现代运维的标准配置。
1. 架构选型
- Exporter:
mysqld_exporter(由Prometheus官方维护)。它负责采集MySQL的各项指标,暴露给Prometheus。 - Storage: Prometheus。时序数据库,擅长处理高并发写入和时间序列查询。
- Visualization: Grafana。强大的图表引擎,社区有大量现成的MySQL Dashboard模板。
2. 部署 mysqld_exporter
你需要创建一个专门的监控用户给exporter访问:
CREATE USER 'exporter'@'%' IDENTIFIED BY 'password';
GRANT PROCESS, REPLICATION CLIENT, SELECT ON *.* TO 'exporter'@'%';
FLUSH PRIVILEGES;
然后启动exporter:
mysqld_exporter --config.my-cnf=/etc/mysql/debian.cnf
默认监听9104端口。
3. 配置 Prometheus
在prometheus.yml中添加job:
scrape_configs:
- job_name: 'mysql'
static_configs:
- targets: ['localhost:9104']
4. 关键监控指标解读
在Grafana中导入社区模板(如ID: 7362),你会看到各种图表。不要只看总数,要看趋势和比率。以下是几个决定生死的关键指标:
Threads_connected vs Threads_running:
Threads_connected:当前建立的连接总数。如果接近max_connections,新连接会被拒绝。Threads_running:当前正在执行SQL的线程数。这个才是反映实时压力的指标。如果Threads_running持续高位,说明处理不过来,需要优化SQL或增加并发能力。
InnoDB Row Operations:
Innodb_rows_read,Innodb_rows_inserted,Innodb_rows_updated,Innodb_rows_deleted。- 观察这些值的每秒增长率。如果
rows_read激增,可能发生了全表扫描或热点数据访问。
QPS (Queries Per Second) & TPS (Transactions Per Second):
- QPS =
Questions/Uptime - TPS = (
Com_commit+Com_rollback) /Uptime - 监控QPS的突增或突降。突降可能意味着上游应用挂了;突增可能意味着出现了爬虫或恶意攻击。
- QPS =
Buffer Pool Hit Rate:
- 公式:
(1 - Innodb_buffer_pool_reads / Innodb_buffer_pool_read_requests) * 100% - 理想值应大于99%。如果低于95%,说明内存不足,数据经常要从磁盘读,性能会断崖式下跌。
- 公式:
Replication Lag (主从延迟):
- 通过
Seconds_Behind_Master监控。如果延迟过大,读取从库会导致数据不一致,或者从库崩溃。
- 通过
5. 告警策略:不要只靠看
监控的目的是为了告警。配置Alertmanager,设置合理的阈值:
- Critical:
Threads_running > 50持续1分钟(假设单机承受上限为50)。 - Warning:
Innodb_buffer_pool_hit_rate < 95%持续5分钟。 - Critical:
Replication_Lag > 60s。 - Warning:
Disk Usage > 80%。
第五步:实战案例——一次真实的“卡顿”排查全过程
理论说完了,我们来模拟一个真实场景。
现象: 某电商APP在晚上8点高峰期,订单详情页加载缓慢,平均响应时间从200ms飙升到2s,错误率微涨。
排查步骤:
看大盘(Prometheus/Grafana):
- 发现
Threads_running在19:55左右开始攀升,最高达到120。 QPS平稳,但TPS略有下降。Innodb_row_lock_time(行锁等待总时间)曲线出现尖峰。- 初步判断:不是流量洪峰导致的CPU过载,而是锁竞争或慢SQL阻塞。
- 发现
查慢日志(Slow Query Log):
- 导出19:50-20:10的慢日志。
- 使用
pt-query-digest分析。 - 发现一条SQL占据了大量时间:
UPDATE order_status SET status = 2 WHERE order_id = ? AND user_id = ?; - 这条SQL执行时间平均1.5秒。
分析SQL(Explain):
- 对这条SQL执行
EXPLAIN。 - 结果显示:
type: ref,key: idx_user_id,rows: 10。看起来用了索引? - 等等,仔细看
Extra列,没有Using index condition。 - 进一步检查表结构,发现
order_id是主键,但user_id上有索引。 - 为什么慢?因为
UPDATE语句会加排他锁。如果另一个事务正在修改同一条user_id下的其他订单,或者存在长事务持有锁,就会导致等待。
- 对这条SQL执行
查锁等待(Performance Schema):
- 登录MySQL,执行:
SELECT * FROM sys.innodb_lock_waits; - 发现确实有阻塞。阻塞者是一个后台定时任务,正在批量更新某个大用户的订单状态,且没有提交事务(或者事务过大)。
- 登录MySQL,执行:
解决方案:
- 短期:找到那个长事务的PID,评估后是否需要Kill。如果是误操作,立即恢复。
- 长期:
- 优化后台任务,分批次提交事务,避免长时间持锁。
- 检查
UPDATE语句的逻辑,确认是否真的需要同时更新order_id和user_id。如果业务允许,可以简化条件。 - 增加
user_id索引的覆盖范围,或者考虑读写分离,确保查询走从库,减少主库压力。
验证:
- 修复后,观察Grafana上的
Threads_running和Innodb_row_lock_time曲线,确认回归正常水平。
- 修复后,观察Grafana上的
结语:监控不是终点,而是优化的起点
从慢查询日志的微观洞察,到Performance Schema的中观诊断,再到Prometheus宏观的全局可视,这套组合拳能解决90%以上的MySQL性能问题。
但请记住,工具只是辅助。理解业务逻辑才是根本。很多时候,性能问题源于不合理的需求设计(比如一次性导出百万级数据)。作为专家,你不仅要会调优SQL,更要敢于对产品经理说:“这个需求在现有架构下不可行,我们需要重构。”
希望这篇指南能成为你手中的利剑,在面对任何数据库卡顿问题时,都能从容不迫,一击必中。如果有具体的报错或奇怪的慢SQL,欢迎随时拿来讨论,我们一起拆解。
