数据库越来越慢怎么办 从小公司生产事故说起 聊聊Percona监控工具 Prometheus配合Grafana MySQL性能诊断实战指南
那天凌晨三点,我的手机疯狂震动。
我揉着惺忪的睡眼打开手机,微信群里已经炸开了锅。”线上挂了!”“用户全在骂!”“数据库CPU飙到99%了!”
作为一家小公司的技术负责人,这种场景我经历过太多次。说实话,那时候我们连个像样的监控都没有,全靠用户反馈才知道出问题了。等到我们反应过来,业务已经停了快一个小时。
今天这篇,我想把我们从血泪教训中总结出来的MySQL性能诊断经验,掰开揉碎了讲给你听。尤其是那些还在用”等出了问题再补救”模式的小团队,希望这篇文章能帮你少走点弯路。
我们到底经历了什么
先聊聊我们那次事故。
那是一家做电商的小公司,日活大概几万,数据库用的是一台普通的MySQL 5.7,单节点,没有任何高可用架构。某天大促活动,流量突然暴增了十倍。
我们的应用服务器扛得住,但数据库直接跪了。
当时监控告警群一片混乱,我们完全不知道问题出在哪。是慢查询?是锁竞争?还是连接数爆了?没人知道。只能一个个排查,等排查完,业务已经瘫痪了很久。
事故复盘的时候,我们总结了几点问题:
- 没有实时监控:全靠用户报障才知道出问题
- 没有慢查询日志分析工具:只能手动查日志
- 不知道MySQL内部状态:连接数、buffer pool命中率、IO情况完全靠猜
- 缺乏性能基线:不知道正常的”慢”和异常的”慢”有什么区别
从那以后,我们痛定思痛,开始搭建一套完整的MySQL监控和诊断体系。今天分享的内容,就是这套体系的实战总结。
第一步:让数据说话——Prometheus + Grafana 搭建监控体系
监控是什么?监控就是你给数据库请了一个24小时不间断的私人医生。它不会休息,不会抱怨,还能在你还没发现问题之前就告诉你:”老板,心率有点快,注意观察。”
为什么选 Prometheus + Grafana?
市面上监控工具很多,Zabbix、Nagios、Prometheus… 为什么我们最终选择了这个组合?
- Prometheus:云原生时代的事实标准,时序数据库出身,专门为 metrics 设计,查询能力强
- Grafana:可视化之王,支持多种数据源,插件生态丰富
- MySQL exporter:Percona 提供的官方 exporter,数据采集稳定可靠
环境准备
假设我们有一台MySQL 5.7 / 8.0 的服务器,IP是 192.168.1.100,端口是 3306。
首先我们需要创建一个专门用于监控的MySQL用户:
CREATE USER 'exporter'@'localhost' IDENTIFIED BY 'YourStrongPassword123!';
GRANT PROCESS, REPLICATION CLIENT, SELECT ON *.* TO 'exporter'@'localhost' WITH GRANT OPTION;
FLUSH PRIVILEGES;
注意:MySQL 8.0 使用
caching_sha2_password认证插件,exporter 可能不兼容。如果遇到问题,可以改用mysql_native_password:> ALTER USER 'exporter'@'localhost' IDENTIFIED WITH mysql_native_password BY 'YourStrongPassword123!'; > ``` ### 部署 MySQL Exporter MySQL Exporter 是一个轻量级的代理程序,负责从MySQL采集指标数据并暴露给Prometheus。 ```bash # 下载 exporter(以 linux-amd64 为例) wget https://github.com/percona/mysql_exporter/releases/download/v0.15.1/percona-mysql-exporter_0.15.1_linux_amd64.tar.gz tar -xzf percona-mysql-exporter_0.15.1_linux_amd64.tar.gz cd percona-mysql-exporter_0.15.1_linux_amd64 # 创建配置文件 cat > .my.cnf << 'EOF' [client] user=exporter password=YourStrongPassword123! EOF chmod 600 .my.cnf # 启动 exporter ./percona-mysql-exporter --config.my-cnf=.my.cnf --collect.global_status --collect.global_variables --collect.processlist --collect.slave_status
默认情况下,exporter 会在 9104 端口暴露指标数据。
配置 Prometheus
编辑 prometheus.yml:
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
- job_name: 'mysql'
static_configs:
- targets: ['192.168.1.100:9104']
metrics_path: /metrics
scrape_interval: 10s
重启 Prometheus 让它生效:
systemctl restart prometheus
此时你可以访问 http://your-server:9104/metrics 看看 exporter 是否正常返回数据。
导入 Grafana 仪表板
Grafana 社区有非常成熟的 MySQL 仪表板,我们不需要从零开始。
- 打开 Grafana,进入 Dashboards → Import
- 输入仪表板 ID:7362(这是 Percona 官方推荐的 MySQL 监控仪表板)
- 选择 Prometheus 作为数据源
- 点击 Import
导入成功后,你会看到一个功能丰富的监控面板,包含:
- MySQL 总体状态(连接数、QPS、TPS)
- InnoDB Buffer Pool 命中率
- 慢查询统计
- 主从复制状态
- 操作系统资源使用(CPU、内存、IO、网络)
第二步:看懂数据——关键指标解读
有了监控,接下来就是学会看数据了。很多新人看到 Grafana 上飘红的曲线就慌,但其实大多数情况下,指标本身不是问题,趋势变化才是。
必须关注的核心指标
| 指标 | 含义 | 正常范围 | 告警阈值 |
|---|---|---|---|
mysql_global_status_threads_connected |
当前连接数 | < max_connections 的 70% | > 80% |
mysql_global_status_threads_running |
活跃连接数 | < 总 CPU 核心数 × 2 | > 核心数 × 4 |
mysql_global_status_uptime |
数据库运行时间 | - | 如果突然变小,说明重启过 |
mysql_global_status_slow_queries |
慢查询累积数 | 增长平稳 | 短时间内暴涨 |
mysql_global_status_queries |
总查询数 | - | 结合 QPS 计算 |
mysql_global_status_com_select_insert_update_delete |
各类操作数量 | - | 分析慢查询来源 |
mysql_global_status_innodb_buffer_pool_reads |
Buffer Pool 未命中直接读磁盘的次数 | 接近 0 | 持续 > 100/s |
mysql_global_status_innodb_buffer_pool_read_requests |
Buffer Pool 读请求总数 | - | 计算命中率 |
如何计算 Buffer Pool 命中率?
-- 在 MySQL 中直接查询
SHOW STATUS LIKE 'Innodb_buffer_pool%';
或者通过 Prometheus 查询:
(1 - (mysql_global_status_innodb_buffer_pool_reads / mysql_global_status_innodb_buffer_pool_read_requests)) * 100
正常应该在 95% 以上。如果持续低于 90%,说明你的 Buffer Pool 可能设置得太小,或者查询模式有问题。
连接数监控的坑
很多小团队看到连接数飙升就慌,其实要区分 总连接数 和 活跃连接数。
Threads_connected:当前所有连接数,包括空闲的Threads_running:当前正在执行查询的线程数
如果 Threads_connected 很高但 Threads_running 很低,说明有很多空闲连接没被回收,可以考虑优化连接池配置。
如果两者都很高,那就要警惕了,可能是慢查询导致连接被占用,或者是连接池配置不当。
第三步:发现问题——慢查询追踪实战
回到我们那次事故。如果当时我们有监控,第一时间就能看到 QPS 突然飙升,慢查询数指数级增长。这时候应该怎么做?
开启慢查询日志
首先确认慢查询日志是否开启:
SHOW VARIABLES LIKE 'slow_query%';
如果没开启,可以这样配置:
-- 临时开启(重启后失效)
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1; -- 超过1秒的查询记录为慢查询
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';
-- 永久配置,修改 my.cnf
[mysqld]
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1
用 pt-query-digest 分析慢查询
Percona Toolkit 里的 pt-query-digest 是分析慢查询的神器。
# 安装 Percona Toolkit
apt-get install percona-toolkit # Debian/Ubuntu
yum install percona-toolkit # CentOS/RHEL
# 分析慢查询日志
pt-query-digest /var/log/mysql/slow.log > /tmp/slow_report.txt
# 查看报告
cat /tmp/slow_report.txt
报告会按查询频率、平均耗时、总耗时等维度排序,一眼就能看出哪些查询是”罪魁祸首”。
Prometheus + Grafana 实时追踪
除了离线分析,我们还可以用 Prometheus 实时追踪慢查询。
在 Grafana 中创建面板,使用以下 PromQL:
# 慢查询速率
rate(mysql_global_status_slow_queries[5m])
# 各类查询的速率(用于分析查询模式)
rate(mysql_global_status_com_select[5m])
rate(mysql_global_status_com_insert[5m])
rate(mysql_global_status_com_update[5m])
rate(mysql_global_status_com_delete[5m])
当发现慢查询突然增多时,可以进一步用 PROCESSLIST 查看当前正在执行的慢查询:
SELECT id, user, host, db, command, time, state, LEFT(info, 100)
FROM information_schema.processlist
WHERE command != 'Sleep'
ORDER BY time DESC
LIMIT 20;
第四步:深入诊断——Percona Monitoring Plugins 进阶
前面我们只用了 MySQL Exporter 的基础指标。Percona 还提供了更丰富的监控插件,可以采集更细粒度的数据。
安装 Percona Monitoring Plugins
# 下载并安装
wget https://downloads.percona.com/downloads/percona-monitoring-plugins/percona-monitoring-plugins-1.5.3/binary/tarballs/percona-monitoring-plugins-1.5.3.tar.gz
tar -xzf percona-monitoring-plugins-1.5.3.tar.gz
cd percona-monitoring-plugins-1.5.3
# 编译安装
cmake . -DCMAKE_INSTALL_PREFIX=/usr/local/percona-monitoring-plugins
make && make install
采集 InnoDB 详细指标
Percona 的 exporter 支持采集 InnoDB 的很多细节指标,比如:
- Row lock 等待次数和时长
- Deadlock 发生次数
- Buffer Pool 页生命周期
- IO 操作详细统计
在 Grafana 中导入仪表板 ID: 8563(Percona Server for MySQL 详细监控),可以看到更丰富的图表。
关键 InnoDB 指标解读
# InnoDB 行锁等待时间
rate(mysql_global_status_innodb_row_lock_time[5m])
# InnoDB 死锁次数
rate(mysql_global_status_innodb_deadlocks[5m])
# Buffer Pool 脏页比例
(mysql_global_status_innodb_dblwr_writes * mysql_global_status_innodb_page_size
/ mysql_global_variables_innodb_buffer_pool_size) * 100
如果行锁等待时间持续很高,说明你的应用可能存在并发热点问题,需要优化事务粒度或者加索引减少锁范围。
如果死锁频繁发生,需要检查是否有循环依赖的锁,或者事务顺序不一致。
第五步:实战案例——从监控告警到问题定位
光说不练假把式。下面我把我们那次事故的完整排查过程,用今天学的知识重新走一遍。
事故时间线
03:00 - Prometheus 告警:MySQL Threads_running 超过阈值(> 20)
03:02 - 值班人员查看 Grafana,发现 QPS 从平时的 500 飙升到 5000+,但平均查询耗时从 5ms 涨到了 800ms
03:05 - 查看慢查询面板,发现 SELECT ... FROM orders WHERE create_time > ? 这个查询的 P99 延迟从 10ms 涨到了 3 秒
03:10 - 登录数据库执行 SHOW PROCESSLIST,发现有 50+ 个查询在等待 Sending data 状态
03:15 - 执行 EXPLAIN 分析慢查询,发现这个查询没有走索引,在做全表扫描
03:20 - 确认 create_time 字段没有索引,立即加索引
03:25 - 观察监控曲线,QPS 开始回落,查询耗时恢复正常
事后复盘
这次事故的根本原因是什么?
- 缺少索引:新上线的查询没有覆盖索引,大促流量放大这个问题
- 监控滞后:我们只有简单的 Ping 监控,没有 Query 级别的监控
- 没有慢查询告警:慢查询日志没有实时监控,只能等用户报障
如果我们当时有今天这套监控体系,问题可以在 3分钟内 被发现,而不是等用户疯狂打电话。
第六步:搭建告警体系——让问题在用户发现之前
监控的目的是发现问题,但人工盯着 Grafana 不现实。我们需要告警。
配置 Alertmanager
编辑 alertmanager.yml:
global:
resolve_timeout: 5m
route:
group_by: ['alertname', 'instance']
group_wait: 10s
group_interval: 10s
repeat_interval: 1h
receiver: 'wechat'
receivers:
- name: 'wechat'
wechat_configs:
- corp_id: 'your-corp-id'
to_user: '@all'
agent_id: 'your-agent-id'
api_secret: 'your-api-secret'
定义告警规则
创建 rules.yml:
groups:
- name: mysql
rules:
- alert: MySQLHighThreadsRunning
expr: mysql_global_status_threads_running > 20
for: 2m
labels:
severity: warning
annotations:
summary: "MySQL 活跃连接数过高"
description: "当前活跃连接数为 {{ $value }},可能影响性能"
- alert: MySQLSlowQueriesRateHigh
expr: rate(mysql_global_status_slow_queries[5m]) > 10
for: 5m
labels:
severity: critical
annotations:
summary: "MySQL 慢查询速率异常"
description: "当前慢查询速率 {{ $value }} qps,请检查慢查询日志"
- alert: MySQLBufferPoolHitRatioLow
expr: (1 - (mysql_global_status_innodb_buffer_pool_reads / mysql_global_status_innodb_buffer_pool_read_requests)) * 100 < 90
for: 10m
labels:
severity: warning
annotations:
summary: "Buffer Pool 命中率偏低"
description: "当前命中率 {{ $value }}%,建议增加 innodb_buffer_pool_size"
- alert: MySQLReplicationLag
expr: mysql_slave_status_seconds_behind_master > 30
for: 5m
labels:
severity: critical
annotations:
summary: "主从延迟过高"
description: "当前延迟 {{ $value }} 秒"
在 Prometheus 中引用规则文件:
rule_files:
- 'rules.yml'
告警分级策略
不要所有告警都一股脑发出去,那样会产生告警疲劳。我们建议这样分级:
| 级别 | 场景 | 响应时间 | 通知方式 |
|---|---|---|---|
| P0 - 致命 | 数据库宕机、主从中断 > 5分钟 | 立即 | 电话 + 群通知 |
| P1 - 严重 | 慢查询暴增、连接数超限 | 5分钟 | 群通知 + IM |
| P2 - 警告 | 命中率下降、磁盘使用率高 | 30分钟 | 群通知 |
| P3 - 信息 | 常规趋势告警 | 下一个工作日 | 邮件 |
第七步:日常维护——保持数据库健康
监控和告警是治标,日常维护才是治本。以下是我们团队每天/每周/每月会做的检查项:
每日检查
-- 1. 检查主从同步状态
SHOW SLAVE STATUS\G
-- 2. 检查锁等待
SELECT * FROM information_schema.innodb_lock_waits;
-- 3. 检查表空间使用率
SELECT
table_schema,
table_name,
ROUND(data_length/1024/1024, 2) AS 'Data_MB',
ROUND(index_length/1024/1024, 2) AS 'Index_MB',
ROUND((data_length + index_length)/1024/1024, 2) AS 'Total_MB'
FROM information_schema.tables
ORDER BY (data_length + index_length) DESC
LIMIT 20;
每周检查
- 分析慢查询日志,识别高频慢查询
- 检查表碎片情况,对碎片严重的表执行
OPTIMIZE TABLE - 检查索引使用情况,删除未使用的索引
- Review 告警记录,优化告警规则
每月检查
- 数据库性能基线对比,观察趋势变化
- 备份恢复演练,确保备份可用
- 架构评审,评估是否需要分库分表
写在最后:监控不是一蹴而就
回想我们那次事故,最让我感慨的是:我们在最基础的监控上花了太多时间补救,却忽略了日常的积累。
这套 Prometheus + Grafana + Percona Exporter 的监控体系,我们花了大概两周时间搭建完成。但正是这两周,让我们在后来的无数次性能问题上从容应对。
监控不是目的,让问题可见、让数据说话、让决策有据才是。
如果你也是小团队,没有专门的 DBA,那这套体系尤其值得投入。它不会替你解决所有问题,但它能确保你在问题发生时,不是瞎子摸象,而是有方向、有数据、有底气地应对。
最后送大家一句话:在数据库上省钱,就是在生产事故上花钱。 这句话我们是用真金白银换来的教训,希望对你有用。
如果你在实际搭建过程中遇到问题,欢迎留言交流。每一行监控配置背后,都是实打实的经验。
