咱们今天不聊那些枯燥的理论,直接切入正题。想象一下,你负责的一个电商大促系统,流量峰值到了,用户反馈页面转圈圈,后台报警电话被打爆。这时候,你是坐在办公室里对着黑乎乎的命令行发呆,还是能一眼看穿数据库到底在“卡”在哪里?
这就是我们要解决的问题:如何构建一套从底层日志到顶层可视化的完整监控体系,不仅看到“慢”,还要知道“为什么慢”,甚至提前预判“死锁”。
第一阶段:知己知彼——慢查询日志不仅是记录,更是线索
很多新手DBA(数据库管理员)有个误区,觉得开了slow_query_log就万事大吉了。其实,慢查询日志就像是一个黑匣子,它记录了发生了什么,但不会告诉你原因。如果配置不当,它甚至会变成性能杀手。
1. 精准捕获:不仅仅是“慢”
默认情况下,MySQL只记录执行时间超过10秒的SQL。但在高并发场景下,500毫秒的查询可能就已经是瓶颈了。我们需要调整参数,让监控更敏感。
在my.cnf或mysql.conf.d/mysql.cnf中,建议这样配置:
[mysqld]
# 开启慢查询日志
slow_query_log = 1
# 慢查询日志文件路径
slow_query_log_file = /var/log/mysql/slow.log
# 将阈值设为1秒,宁可错杀,不可放过。后续再过滤
long_query_time = 1
# 记录未使用索引的查询(即使很快),这对优化至关重要
log_queries_not_using_indexes = 1
# 记录执行时间极短但不符合上述条件的查询数量,避免日志过大
min_examined_row_limit = 100
关键点解释:
long_query_time = 1:别设太低,比如0.1秒。否则全表扫描几百万行的数据量小,或者缓存命中的简单查询都会涌入日志,导致磁盘IO飙升,反而拖慢数据库。1秒是一个很好的平衡点。log_queries_not_using_indexes = 1:这是神器。很多时候,SQL跑得慢不是因为逻辑复杂,而是因为没走索引。这个开关能让你发现所有“裸奔”的查询。
2. 解析日志:从文本到结构化数据
直接看几GB的日志文件?那是自虐。我们需要工具。这里推荐两个梯队:
入门级:
mysqldumpslow。MySQL自带工具,简单粗暴。# 查看执行次数最多的前10条慢SQL mysqldumpslow -s c -t 10 /var/log/mysql/slow.log # 查看平均执行时间最长的前10条 mysqldumpslow -s t -t 10 /var/log/mysql/slow.log进阶级:
pt-query-digest。Percona Toolkit的神器。它能将慢查询日志转化为分析报告,甚至能生成HTML报告,包含时序图、分位数统计等。pt-query-digest --since 1h /var/log/mysql/slow.log > slow_report.html注:在实际生产环境中,我们通常不直接处理原始日志文件,而是通过代理层实时采集,后面会讲。
案例分享:
有一次,我发现线上有一个SELECT * FROM orders WHERE status = 'pending'经常出现在慢查询里。用EXPLAIN一看,status字段没有索引。加上索引后,响应时间从800ms降到了5ms。这就是log_queries_not_using_indexes的价值。
第二阶段:实时脉搏——Prometheus + Exporter 架构设计
慢查询日志是“事后诸葛亮”,我们需要“实时监测”。这时候,Prometheus出场了。但MySQL本身不直接支持Prometheus,我们需要一个中间人:Exporter。
1. 选型:mysqld_exporter
目前最主流的选择是官方维护的 mysqld_exporter。它是一个轻量级的Go程序,连接到MySQL,收集各种指标(QPS、TPS、连接数、锁等待、InnoDB状态等),并以Prometheus格式暴露出来。
部署步骤(Docker方式,最快捷):
docker run -d \
--name mysqld-exporter \
--restart=always \
-p 9104:9104 \
-e DATA_SOURCE_NAME="user:password@tcp(127.0.0.1:3306)/" \
prom/mysqld-exporter
注意:
- 你需要为Exporter创建一个专用的监控账号,权限只需
PROCESS,REPLICATION CLIENT,SELECT。不要给root权限!CREATE USER 'exporter'@'localhost' IDENTIFIED BY 'StrongPassword'; GRANT PROCESS, REPLICATION CLIENT, SELECT ON *.* TO 'exporter'@'localhost'; FLUSH PRIVILEGES;
2. 配置 Prometheus 抓取
在prometheus.yml中添加job:
scrape_configs:
- job_name: 'mysql'
static_configs:
- targets: ['localhost:9104']
# 每15秒采集一次,高频监控可以设为5s,但会增加存储压力
scrape_interval: 15s
重启Prometheus后,访问 http://localhost:9104/metrics,你应该能看到成千上万行以mysql_开头的指标。
核心指标解读:
mysql_global_status_questions:总查询数。mysql_global_status_threads_connected:当前连接数。mysql_innodb_buffer_pool_pages_free:InnoDB缓冲池空闲页数(越低越好,说明内存利用率高)。mysql_slave_status_seconds_behind_master:主从延迟(秒)。
第三阶段:可视化大屏——Grafana 的艺术
有了数据,怎么看得爽?Grafana是标配。但市面上现成的Dashboard模板很多,很多只是把指标堆砌在一起,缺乏业务视角。我们来定制一个“高并发场景专用”的大屏。
1. 推荐模板ID
如果你不想从头画,可以直接导入模板ID:7362 (MySQL Dashboard) 或 13105 (MySQL by Percona)。这些模板已经涵盖了大部分基础指标。
2. 自定义关键面板:从“看数据”到“看问题”
面板一:连接数与QPS趋势
- 目的:判断是否达到连接池上限,或流量突增。
- 查询:
select time, value from mysql_global_status_threads_connected where time > now() - 1h union all select time, value from mysql_global_status_queries where time > now() - 1h - 技巧:设置阈值告警。当连接数超过最大配置的80%时,标红。
面板二:InnoDB Buffer Pool 命中率
- 目的:内存是否够用?
- 计算:
$\( Hit Rate = 1 - \frac{Read\_requests}{Data\_read\_hits} \)$
注意:Grafana中通常直接使用
mysql_innodb_buffer_pool_read_requests和mysql_innodb_buffer_pool_reads计算。 - 标准:理想情况应 > 99%。如果长期低于95%,考虑增加
innodb_buffer_pool_size。
面板三:锁等待与死锁监控(重点!)
高并发下的卡顿,80%源于锁。
- 关键指标:
mysql_global_status_innodb_row_lock_time_avg:平均锁等待时间。mysql_global_status_innodb_row_lock_waits:锁等待次数。mysql_global_status_innodb_deadlocks:死锁次数。
- 可视化:使用Stat面板显示当前活跃的死锁数,使用Graph显示锁等待时间的变化曲线。
面板四:Top SQL 实时监控
这是最实用的功能。结合 pt-query-digest 或 MySQL的 performance_schema,我们可以实时展示消耗CPU或IO最高的SQL。
- 方法:启用MySQL 5.7+ 的
performance_schema,特别是events_statements_summary_by_digest。 - Prometheus Exporter配置:确保
mysqld_exporter开启了--collect.perf_schema.eventsstatements。 - Grafana查询:
使用PromQL查询类似
rate(mysql_perf_schema_events_statements_sum_timer_wait[5m]),并按SQL摘要分组排序。
第四阶段:实战演练——解决高并发卡顿与死锁
理论说完,我们来模拟一个真实的高并发场景,看看这套体系如何帮你破案。
场景描述
双十一零点,秒杀系统上线。用户反映下单失败,后台显示“交易超时”。数据库CPU飙升至100%。
第一步:Grafana 宏观定位
打开大屏,我看到:
- Threads Running 瞬间从5涨到200+(最大允许500)。
- InnoDB Row Lock Time 曲线急剧上升,平均等待时间从1ms变成500ms。
- Deadlocks 计数器开始跳动。
初步判断:不是CPU算不过来,而是大量线程在争抢同一行数据或索引,导致锁等待。
第二步:深入分析——谁在争抢?
在Grafana的“Top SQL”面板中,我发现了这条SQL:
UPDATE inventory SET stock = stock - 1 WHERE product_id = 1001 AND stock > 0;
分析:
- 热点行竞争:所有秒杀请求都在更新同一款商品(product_id=1001)的库存。
- 间隙锁(Gap Lock):即使有索引,InnoDB在可重复读隔离级别下,可能会加间隙锁,导致其他事务无法插入或更新相关范围。
- 死锁风险:如果不同事务以不同顺序更新关联表,极易死锁。
第三步:解决方案
方案A:应用层解耦(推荐)
不要在数据库层面直接扣减库存。
- Redis预扣减:使用Lua脚本在Redis中原子性扣减库存。
local stock = tonumber(redis.call('get', KEYS[1])) if stock > 0 then redis.call('decr', KEYS[1]) return 1 else return 0 end - 异步落库:将扣减成功的请求放入消息队列(Kafka/RabbitMQ),由消费者慢慢更新MySQL库存。
方案B:数据库层面优化(如果必须直连DB)
- 调整隔离级别:如果业务允许,改为RC(Read Committed),减少间隙锁的使用。
- 优化SQL:确保
product_id有唯一索引。 - 批量更新:如果可能,合并多个用户的扣减请求,一次性更新,减少锁持有时间。
第四步:验证效果
实施Redis预扣减后,重新压测。
- Grafana上,
Threads Running稳定在20左右。 InnoDB Row Lock Time回归基线。- 数据库CPU负载下降至30%。
死锁排查小技巧:
如果依然出现死锁,开启SHOW ENGINE INNODB STATUS\G,查看LATEST DETECTED DEADLOCK部分。它会清晰列出两个事务分别持有什么锁,申请什么锁,形成闭环。在Grafana中,你可以创建一个Panel,定期抓取这个状态并解析关键字段,实现死锁原因的自动化归因。
第五部分:运维避坑指南与最佳实践
1. 监控本身的开销
Prometheus和Exporter本身也会消耗资源。
- 限制采集频率:不要对所有指标都进行1s采集。对于慢变指标(如连接数),可以延长间隔。
- 裁剪指标:
mysqld_exporter默认采集很多指标,有些你可能根本不用。通过配置文件排除不必要的collector,例如:--no-collector.info_schema.processlist
2. 数据存储周期
Prometheus默认只保留15天数据。对于长期趋势分析(如月度对比),需要接入VictoriaMetrics或Thanos进行长期存储。
3. 告警噪音
不要对每个小波动都发钉钉/邮件。
- 阈值设置:使用P99延迟而不是平均值。
- 静默期:设置告警冷却时间,防止风暴。
- 分级告警:
- P0(致命):主库宕机、数据损坏 -> 电话轰炸。
- P1(严重):慢查询激增、主从延迟>60s -> 钉钉+短信。
- P2(警告):连接数>80% -> 仅内部IM通知。
4. 代码中的监控埋点
除了数据库层面的监控,应用层也要配合。在Java/Python代码中,使用Micrometer或Prometheus Client库,暴露应用级的JVM内存、GC次数、HTTP请求耗时。将这些指标与MySQL指标放在同一个Grafana面板中,方便关联分析。例如:当MySQL锁等待升高时,观察应用线程池是否也满了。
结语:监控不是终点,而是起点
搭建Prometheus+Grafana+Exporter这套体系,只是第一步。真正的价值在于数据驱动的决策。
当你看着大屏上那条平滑的QPS曲线,和那条几乎贴近X轴的锁等待时间线时,那种掌控感是无与伦比的。它让你从“救火队员”变成了“防火专家”。
记住,高并发场景下的性能优化,没有银弹。它是架构设计、代码质量、数据库配置和监控运维的综合体现。希望这套实战指南,能成为你应对未来流量洪峰时的坚实盾牌。
如果有具体的报错信息或奇怪的慢SQL,欢迎随时拿出来讨论。毕竟,每一个坑,都是成长的垫脚石。
