某公司MySQL数据库突发慢查询导致订单系统瘫痪经排查缺失有效监控工具后续引入Prometheus配合mysqld_exporter和pt-query-digest搭建可视化监控体系后平均响应时间从3秒降至200毫秒故障发现时间从2小时缩短至5分钟本文详解主流MySQL性能监控工具选型配置与实战案例
那个噩梦般的下午
2023年秋天的一个普通工作日下午两点,国内某电商平台的订单系统突然崩了。
不是那种突然全灰的崩,而是用户点”提交订单”之后,页面一直在转圈,转啊转,像极了你在等一个永远不会来的外卖。客服群里的消息开始爆炸,运营同事的脸色比服务器还青。
技术团队冲进会议室的时候,第一反应是查代码——是不是昨晚上线的新功能有bug?
查了两个小时,没找到。
又查数据库连接池,也没问题。
再查应用服务器CPU,也正常。
最后把目光转向MySQL。这一看,才发现问题的根源:有超过200个慢查询在同时跑,最慢的一个跑了47秒,而这些慢查询在整个下午都没有人知道它们存在。
为什么不知道?因为没有监控。
准确地说,不是没有监控,而是他们只有最原始的监控——一个报警阈值设得极宽的Zabbix,只能监控数据库是否活着,却看不出来数据库”活得怎么样”。
那一刻,团队里最资深的DBA老王叹了口气,说了一句让整个部门重新认识监控重要性的话:
“我们就像是在盲人骑瞎马,半夜临江 half 危。”
三个月后,同样的系统,同样的业务量级,他们的平均响应时间从3秒降到了200毫秒,故障发现时间从2小时缩短到了5分钟。
这中间发生了什么?一个完整的监控体系重建。
一、先搞明白:MySQL慢查询到底是怎么”杀人”的
在讲工具之前,我想先跟你聊聊慢查询这件事本身。因为如果你不理解慢查询是怎么发生的,就算给你再厉害的监控工具,你也看不懂报警在说什么。
1.1 什么是慢查询
MySQL里有一个参数叫long_query_time,默认值是10秒。意思是:一条SQL执行时间超过10秒,MySQL就会把它记下来,这就是慢查询。
你可以把这个参数调低,比如调到0.1秒(100毫秒),这样稍微慢一点的查询也会被记录下来。
但问题来了:慢查询是怎么产生的?
最常见的几种情况:
第一种:没有走索引。
-- 假设你有一张订单表 orders,有字段 user_id、status、create_time
-- 下面这条查询没有走索引,全表扫描
SELECT * FROM orders
WHERE status = 'pending'
AND create_time > '2023-09-01';
如果这张表有1000万条数据,这一条查询就要把1000万行全部扫一遍。在内存里做比较,很耗时间。
第二种:索引失效了。
-- 下面这条SQL虽然用到了 user_id 字段,但如果 user_id 是字符串类型
-- 而你传的是数字,MySQL会做隐式类型转换,索引就失效了
SELECT * FROM orders WHERE user_id = 123456;
-- 如果 user_id 是 VARCHAR 类型,应该写成:
SELECT * FROM orders WHERE user_id = '123456';
这种bug特别隐蔽,因为查询结果是正确的,只是慢得离谱。
第三种:锁竞争。
-- 多个事务同时修改同一行数据
-- 比如两个用户同时抢最后一件商品
UPDATE orders SET status = 'paid' WHERE order_id = 99999 AND status = 'pending';
如果两个请求同时打到这行数据上,一个会等另一个提交,等待期间这个请求就是”慢”的。
第四种:大数据量下的排序和分组。
-- 没有分页,直接拉取全部数据再排序
SELECT * FROM orders ORDER BY create_time DESC;
几百万行数据,MySQL要在内存里排序,排不动了就写到临时文件,这个过程非常慢。
1.2 慢查询的连锁反应
慢查询最可怕的地方不在于它慢,而在于它会传染。
想象一下这个场景:
- 某个慢查询占用了一个数据库连接,持续了30秒
- 在这30秒里,这个连接一直被占用,其他请求排队等待
- 用户等不及了,刷新页面,又发一条请求
- 这条新请求也在排队
- 连接池里的连接逐渐被慢查询占满
- 新的正常请求进来,发现没有连接可用,直接报错
- 应用层超时,返回给用户”系统繁忙”
- 用户以为没提交成功,又点一次
- 请求量翻倍,数据库彻底扛不住
这就是为什么一个慢查询能把整个系统拖垮。慢查询不是孤立的问题,它是一个系统性崩溃的导火索。
二、主流MySQL监控工具全景图
回到那家电商公司的故事。他们在重建监控体系的时候,做了大量的调研和选型工作。我把他们最终调研过的工具都整理出来,帮你建立一个完整的认知框架。
2.1 工具分类
MySQL监控工具大致可以分为以下几类:
| 类别 | 代表工具 | 核心特点 |
|---|---|---|
| 传统监控 | Zabbix、Nagios | 轻量、稳定,但功能较基础 |
| 专业数据库监控 | Percona Monitoring、MySQL Enterprise Monitor | 深度集成,功能专业 |
| 云原生监控 | Prometheus + Grafana | 灵活、可扩展,生态强大 |
| SQL分析工具 | pt-query-digest、mysqldumpslow | 专注于慢查询分析 |
| 商业APM | Dynatrace、New Relic、阿里云ARMS | 全链路追踪,企业级 |
| 云厂商方案 | AWS CloudWatch、阿里云云监控 | 开箱即用,与云平台深度集成 |
2.2 深度对比
让我逐一讲解这些工具,告诉你它们的优缺点,以及适合什么场景。
Zabbix —— 老牌选手,稳但不够深
Zabbix是很多公司最早使用的监控工具。它的优势是稳定、成熟、社区活跃,对于基础的服务器监控(CPU、内存、磁盘、网络)表现优秀。
但Zabbix在数据库监控方面的短板很明显:
- 它只能告诉你数据库”活着”还是”死了”,以及基本的性能指标(QPS、TPS)
- 它无法深入分析SQL的执行情况
- 它不知道你哪些SQL慢了,也不知道为什么慢
- 报警阈值需要人工设置,容易漏报或误报
适合场景:小型公司,数据库压力不大,只需要基础的健康检查。
Percona Monitoring and Management (PMM) —— 数据库专家的最爱
PMM是Percona公司推出的开源数据库监控方案,它几乎是专为MySQL设计的。
PMM的核心优势:
- 深度集成MySQL内部指标:PMM可以直接读取MySQL的performance_schema,获取到非常详细的执行信息
- 自带query_analyzer:可以实时分析慢查询,告诉你哪条SQL有问题,缺少什么索引
- 可视化强:基于Grafana,但预配置了大量MySQL专用的dashboard
- 资源占用低:相比其他方案,PMM对生产环境的影响最小
PMM的短板:
- 部署相对复杂,需要安装PMM Server和PMM Client
- 定制化能力不如Prometheus灵活
- 社区活跃度不如Prometheus
代码示例:安装PMM Client
# 在MySQL服务器上安装PMM Client
sudo docker run \
-d \
--restart=always \
--name=pmm-client \
-v /etc/hostname:/etc/hostname \
-v /etc/ resolv.conf:/etc/resolv.conf \
-v /proc:/host/proc:ro \
-v /sys:/host/sys:ro \
-v /var/run:/var/run:ro \
-v /var/lib/mongodb:/usr/local/bin/mongodb_exporter/data \
-e VIRTUAL_HOST=pmm-client \
-e VIRTUAL_PORT=42000 \
-e PMM_SERVER=10.0.0.100 \
-e PMM_SERVER_USER=admin \
-e PMM_SERVER_PASSWORD=password \
--network=host \
percona/pmm-client:latest
Prometheus + Grafana —— 云原生时代的标准答案
这是那家电商公司最终选择的方案,也是目前业界最主流的MySQL监控方案。
为什么选择Prometheus?
- 多维数据模型:Prometheus的数据是按时间序列存储的,每个指标都有标签(label),可以灵活地按任意维度查询
- 强大的查询语言PromQL:可以写出非常复杂的查询逻辑
- 生态极其丰富:有大量的exporter可以选择,不只是MySQL
- 告警能力强:配合Alertmanager可以实现复杂的告警逻辑
- 开源免费:Percona PMM底层其实也是用的Prometheus
Prometheus监控MySQL的核心组件:
- mysqld_exporter:负责采集MySQL的性能指标(QPS、连接数、InnoDB状态等)
- node_exporter:负责采集服务器层面的指标(CPU、内存、磁盘等)
- Grafana:负责可视化展示,把Prometheus的数据变成漂亮的图表
- Alertmanager:负责告警,当指标超过阈值时发送通知
部署架构:
MySQL Server
│
├── mysqld_exporter (采集MySQL指标)
│ │
│ ▼
│ Prometheus (存储和查询)
│ │
│ ├── Grafana (可视化)
│ │
│ └── Alertmanager (告警)
│
└── node_exporter (采集服务器指标)
代码示例:mysqld_exporter配置
# mysqld_exporter 启动参数
--collect.global_status
--collect.global_variables
--collect.slave_status
--collect.engine_innodb_status
--collect.heartbeat
--collect.info_schema.innodb_metrics
--collect.info_schema.tables
--collect.info_schema.processlist
--collect.info_schema.query_response_time
--collect.binlog_size
--collect.perf_schema.eventsstatements
--collect.perf_schema.eventswaits
--collect.perf_schema.file_events
--collect.perf_schema.tablelocks
# 在mysql中为exporter创建专用账号
CREATE USER 'exporter'@'localhost' IDENTIFIED BY 'password' WITH MAX_USER_CONNECTIONS 3;
GRANT PROCESS, REPLICATION CLIENT, SELECT ON *.* TO 'exporter'@'localhost';
FLUSH PRIVILEGES;
pt-query-digest —— 慢查询分析的瑞士军刀
如果说Prometheus是”监控系统”,那pt-query-digest就是”诊断工具”。
pt-query-digest是Percona Toolkit中的一个工具,专门用来分析MySQL的慢查询日志。它的功能非常强大:
- 慢查询分类汇总:把相似SQL归为一类,统计每类的执行次数、总时间、平均时间
- Top N分析:找出最慢的SQL、查询最多的SQL、锁等待最多的SQL
- 生成报告:可以生成文本报告、HTML报告,甚至可以直接输出到数据库
- 实时分析:支持实时分析正在发生的慢查询
代码示例:分析慢查询日志
# 分析慢查询日志,生成详细报告
pt-query-digest /var/log/mysql/slow.log \
--report \
--report-format trends \
--output report > /tmp/slow_query_report.txt
# 只看最慢的10条SQL
pt-query-digest /var/log/mysql/slow.log --limit 10%
# 按查询类型分组分析
pt-query-digest --group-by fingerprint /var/log/mysql/slow.log
# 输出为JSON格式,方便程序处理
pt-query-digest --output json /var/log/mysql/slow.log > report.json
# 分析最近1小时的慢查询
pt-query-digest --since 1h /var/log/mysql/slow.log
报告示例解读:
# Profile
Rank ID Query ID Response Time Calls R/Call V/M Item
==== == ========== ============= ===== ====== ==== =====
1 0xAD8B 0xABC12345 45.23s (XX%) 150 0.30s 0.00 SELECT orders
2 0xB123 0xDEF67890 12.56s (XX%) 500 0.02s 0.00 SELECT users
3 0xC456 0xGHI11223 8.90s (XX%) 50 0.18s 0.00 UPDATE orders
# Query 1: 0xAD8B 0xABC12345
# Query ID: 0xABC12345
# Response time: 45.23s
# Count: 150
# Avg: 0.30s
# M-Max: 47.12s
# M-Median: 0.28s
# Explanation:
# SELECT * FROM orders WHERE status = ? AND create_time > ?
# Missing index on (status, create_time)
# Suggested indexes: ALTER TABLE orders ADD INDEX idx_status_time (status, create_time);
商业APM方案
最后简单提一下商业APM(Application Performance Monitoring)方案,如Dynatrace、New Relic、阿里云ARMS等。
这些方案的特点是全链路追踪:从用户发起请求,到应用服务器,再到数据库,整个过程都可以看到。它们的优点是开箱即用,不需要自己搭建复杂的采集链路;缺点也很明显——贵。
对于中小型公司来说,Prometheus + Grafana + pt-query-digest的组合在功能上已经足够,成本也低得多。
三、那家电商公司的完整重建方案
了解了工具之后,让我带你回到那家电商公司的故事,看看他们具体是怎么搭建监控体系的。
3.1 需求分析
他们在搭建之前,先做了详细的调研:
他们面临的问题:
- 慢查询无法及时发现,等用户反馈才知道有问题
- 出了问题不知道是数据库的问题还是应用的问题
- 没有历史数据对比,无法判断当前性能是否正常
- 报警不及时,等报警发现时系统已经瘫痪
- 排查问题依赖人工,效率极低
他们的目标:
- 实时监控MySQL性能,异常5分钟内发现
- 能够看到SQL的执行情况,快速定位慢查询
- 有历史数据,可以做趋势分析和容量规划
- 自动告警,不需要人工盯着
- 方案要稳定,不能因为监控工具本身出问题
3.2 架构设计
他们最终设计的架构如下:
┌─────────────────────────────────────────────────────────────────┐
│ 监控架构图 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ MySQL │ │ MySQL │ │ MySQL │ │
│ │ (主库) │ │ (从库1) │ │ (从库2) │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ mysqld_ │ │ mysqld_ │ │ mysqld_ │ │
│ │ exporter │ │ exporter │ │ exporter │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ └───────────────────┼───────────────────┘ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ Prometheus │ │
│ │ (数据采集&存储) │ │
│ └────────┬─────────┘ │
│ │ │
│ ┌──────────────┼──────────────┐ │
│ ▼ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Grafana │ │ Alertmanager │ │ pt-query- │ │
│ │ (可视化) │ │ (告警通知) │ │ digest │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────────────────────────────────────┐ │
│ │ 钉钉/邮件/电话告警 │ │
│ └──────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
3.3 详细部署步骤
第一步:部署mysqld_exporter
# 在每台MySQL服务器上部署mysqld_exporter
# 使用Docker方式部署,简单且易于管理
# 创建监控账号
mysql -u root -p << EOF
CREATE USER 'mysqld_exporter'@'localhost' IDENTIFIED BY 'StrongP@ssw0rd!';
GRANT PROCESS, REPLICATION CLIENT, SELECT ON *.* TO 'mysqld_exporter'@'localhost';
FLUSH PRIVILEGES;
EOF
# 创建配置文件
cat > /etc/mysql/exporter.cnf << EOF
[client]
user=mysqld_exporter
password=StrongP@ssw0rd!
EOF
# 启动mysqld_exporter
docker run -d \
--name mysqld_exporter \
--restart=always \
--network host \
-v /etc/mysql/exporter.cnf:/etc/mysql_exporter.cnf \
prom/mysqld-exporter \
--config.my-cnf=/etc/mysql_exporter.cnf \
--collect.global_status \
--collect.global_variables \
--collect.slave_status \
--collect.engine_innodb_status \
--collect.info_schema.innodb_metrics \
--collect.info_schema.processlist \
--collect.perf_schema.eventsstatements \
--collect.perf_schema.eventswaits
# 验证exporter是否正常工作
curl http://localhost:9104/metrics | head -20
第二步:部署Prometheus
# prometheus.yml 配置
global:
scrape_interval: 15s # 每15秒采集一次
evaluation_interval: 15s # 每15秒评估一次告警规则
scrape_timeout: 10s # 采集超时时间
# 告警规则
rule_files:
- "alert_rules.yml"
# 告警配置
alerting:
alertmanagers:
- static_configs:
- targets:
- 'alertmanager:9093'
# 采集目标
scrape_configs:
# MySQL监控
- job_name: 'mysql'
static_configs:
- targets:
- 'mysql-master:9104'
- 'mysql-slave1:9104'
- 'mysql-slave2:9104'
labels:
instance_type: 'mysql'
env: 'production'
# 服务器监控
- job_name: 'node'
static_configs:
- targets:
- 'mysql-master:9100'
- 'mysql-slave1:9100'
- 'mysql-slave2:9100'
labels:
instance_type: 'server'
env: 'production'
# 应用服务器监控
- job_name: 'application'
static_configs:
- targets:
- 'app-server1:8080'
- 'app-server2:8080'
labels:
instance_type: 'app'
env: 'production'
第三步:配置告警规则
# alert_rules.yml
groups:
- name: mysql_alerts
rules:
# MySQL主从延迟告警
- alert: MySQLReplicationDelay
expr: mysql_slave_status_seconds_behind_master > 30
for: 1m
labels:
severity: warning
annotations:
summary: "MySQL主从延迟超过30秒"
description: "实例 {{ $labels.instance }} 的主从延迟为 {{ $value }} 秒"
# MySQL连接数告警
- alert: MySQLConnections
expr: mysql_global_status_threads_connected > 500
for: 2m
labels:
severity: critical
annotations:
summary: "MySQL连接数过高"
description: "实例 {{ $labels.instance }} 的当前连接数为 {{ $value }},超过阈值500"
# InnoDB缓冲池命中率告警
- alert: InnoDBBufferPoolHitRate
expr: (1 - mysql_global_status_innodb_buffer_pool_read_requests / mysql_global_status_innodb_buffer_pool_reads) * 100 < 95
for: 5m
labels:
severity: warning
annotations:
summary: "InnoDB缓冲池命中率低于95%"
description: "当前命中率为 {{ $value }}%"
# 慢查询数量告警
- alert: MySQLSlowQueries
expr: rate(mysql_global_status_slow_queries[5m]) > 10
for: 5m
labels:
severity: critical
annotations:
summary: "慢查询速率过高"
description: "最近5分钟平均每秒慢查询超过10条"
- name: application_alerts
rules:
# 接口响应时间告警
- alert: APIResponseTime
expr: histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) > 2
for: 5m
labels:
severity: warning
annotations:
summary: "API接口P95响应时间超过2秒"
description: "当前P95响应时间为 {{ $value }} 秒"
第四步:部署Alertmanager
# alertmanager.yml
global:
resolve_timeout: 5m
route:
group_by: ['alertname', 'instance']
group_wait: 10s
group_interval: 10s
repeat_interval: 1h
receiver: 'default-receiver'
routes:
- match:
severity: critical
receiver: 'critical-receiver'
repeat_interval: 30m
- match:
severity: warning
receiver: 'warning-receiver'
receivers:
- name: 'default-receiver'
webhook_configs:
- url: 'http://alert-webhook:5001/webhook'
send_resolved: true
- name: 'critical-receiver'
wechat_configs:
- corp_id: 'your-corp-id'
api_secret: 'your-api-secret'
api_url: 'https://qyapi.weixin.qq.com/cgi-bin/'
send_resolved: true
webhook_configs:
- url: 'http://alert-webhook:5001/critical'
send_resolved: true
- name: 'warning-receiver'
email_configs:
- to: 'dba-team@company.com'
from: 'alertmanager@company.com'
smarthost: 'smtp.company.com:587'
auth_username: 'alertmanager'
auth_password: 'your-email-password'
webhook_configs:
- url: 'http://alert-webhook:5001/warning'
send_resolved: true
第五步:pt-query-digest集成
#!/bin/bash
# auto_analyze_slowlog.sh - 自动分析慢查询日志
SLOW_LOG="/var/log/mysql/slow.log"
REPORT_DIR="/data/reports/slow_query"
DATE=$(date +%Y%m%d)
# 创建报告目录
mkdir -p ${REPORT_DIR}/${DATE}
# 分析过去1小时的慢查询
pt-query-digest \
--since 1h \
--filter "\$event->{Time} = int(\$event->{Time})" \
--output report \
--report-format long \
${SLOW_LOG} > ${REPORT_DIR}/${DATE}/report_$(date +%H%M%S).txt
# 生成Top10慢查询摘要
pt-query-digest \
--limit 10% \
--order-by Query_time \
--report-format summary \
${SLOW_LOG} > ${REPORT_DIR}/${DATE}/top10_slow_queries.txt
# 如果有新的慢查询,发送告警
SLOW_COUNT=$(grep -c "Query_time" ${SLOW_LOG} | tail -1)
if [ ${SLOW_COUNT} -gt 50 ]; then
echo "慢查询数量异常:${SLOW_COUNT}条" | mail -s "MySQL慢查询告警" dba@company.com
fi
# 每天清理7天前的报告
find ${REPORT_DIR} -type f -mtime +7 -delete
3.4 Grafana可视化面板
他们为MySQL监控定制了一套Grafana面板,包含以下核心内容:
面板1:MySQL核心指标概览
- QPS(每秒查询数)
- TPS(每秒事务数)
- 连接数趋势
- 慢查询数量趋势
面板2:InnoDB引擎指标
- 缓冲池命中率
- 脏页比例
- 行锁定等待
- 死锁次数
面板3:复制状态
- 主从延迟
- 复制线程状态
- 复制错误数
面板4:SQL执行分析
- Top 10 最慢SQL
- SQL执行频率分布
- 锁等待分析
面板5:服务器资源
- CPU使用率
- 内存使用率
- 磁盘I/O
- 网络流量
下面是一个典型的Grafana面板JSON配置示例:
{
"annotations": {
"list": []
},
"editable": true,
"gnetId": null,
"graphTooltip": 1,
"id": 1,
"links": [],
"panels": [
{
"title": "QPS & TPS",
"type": "graph",
"targets": [
{
"expr": "rate(mysql_global_status_queries[5m])",
"legendFormat": "QPS",
"refId": "A"
},
{
"expr": "rate(mysql_global_status_comm_handler_threads[5m])",
"legendFormat": "TPS",
"refId": "B"
}
]
},
{
"title": "连接数",
"type": "graph",
"targets": [
{
"expr": "mysql_global_status_threads_connected",
"legendFormat": "当前连接数",
"refId": "A"
},
{
"expr": "mysql_global_status_threads_created",
"legendFormat": "已创建连接数",
"refId": "B"
}
]
},
{
"title": "慢查询",
"type": "graph",
"targets": [
{
"expr": "rate(mysql_global_status_slow_queries[5m])",
"legendFormat": "慢查询速率",
"refId": "A"
}
]
}
],
"refresh": "30s",
"time": {
"from": "now-1h",
"to": "now"
},
"timezone": "browser",
"title": "MySQL核心指标",
"uid": "mysql-overview",
"version": 1
}
四、监控体系上线后的变化
这套系统上线后,那个电商公司的运维团队经历了几个阶段的变化。
第一阶段:数据积累期(第1-2周)
最初两周,团队成员都在学习怎么看这些图表。老王带着大家逐个指标过,解释每个指标的含义和正常范围。
一个有趣的发现:在慢查询分析中,他们发现了一个之前从未注意到的问题——每周六上午10点,系统都会出现一个性能高峰。追查后发现,是某个定时任务在每周六凌晨2点执行全表统计,统计结果在周六上午被缓存刷新,导致查询变慢。
如果没有这个监控体系,这个问题永远不会被发现。
第二阶段:阈值调优期(第3-4周)
告警规则不是一成不变的。最初的阈值设得比较激进,导致每天都有很多误报。团队成员逐步调整:
- 将”连接数超过500”的阈值调整为”连接数超过500且持续5分钟”
- 将”慢查询速率超过10条/秒”调整为”慢查询速率超过20条/秒且持续3分钟”
- 增加了告警的静默期,避免同一问题重复报警
第三阶段:自动化响应期(第2个月起)
当监控体系稳定运行后,他们开始探索自动化响应:
- 自动重启卡死的MySQL进程:当检测到某个查询长时间持有锁时,自动kill掉这个查询
- 自动扩容:当连接数超过80%时,自动触发扩容流程
- 自动备份慢查询样本:每天将慢查询日志分析结果保存到对象存储,方便后续分析
第四阶段:数据驱动决策期(第3个月起)
到了这个阶段,监控数据开始真正发挥作用。团队可以用历史数据来做容量规划,用慢查询数据来指导索引优化,用趋势数据来评估新功能的影响。
一个具体的案例:他们在上线一个新功能前,先用监控数据评估了当前系统的负载情况,发现某个表在高峰期已经有接近极限的连接数。他们在上线前就对这个表做了分库分表,避免了上线后的性能问题。
五、实战中的坑与解决方案
任何系统在落地过程中都会遇到问题。让我分享几个他们在实践中遇到的典型问题及解决方案。
坑1:监控工具本身成为性能瓶颈
问题:初期部署了过多的采集器,导致MySQL服务器的CPU和内存占用明显上升。
原因:
- mysqld_exporter采集了过多的指标
- 采集频率过高(每秒采集一次)
- 没有对采集进行限流
解决方案:
# 优化后的mysqld_exporter配置
# 只采集必要的指标,关闭不需要的采集器
--collect.info_schema.processlist # 必需:查看当前运行的SQL
--collect.global_status # 必需:核心性能指标
--collect.global_variables # 必需:查看配置
--collect.slave_status # 必需:主从状态
--collect.engine_innodb_status # 必需:InnoDB状态
# 以下指标生产环境谨慎开启
# --collect.perf_schema.eventsstatements # 性能开销较大,建议只在必要时开启
# --collect.info_schema.tablestats # 性能开销较大
# 降低采集频率
# Prometheus配置中,将scrape_interval从15s调整为30s
scrape_interval: 30s
坑2:告警疲劳
问题:每天收到几十条告警,大部分是误报或无关紧要的告警,导致真正的紧急告警被忽略。
原因:
- 告警阈值设置不合理
- 告警分组不够精细
- 缺少告警降噪机制
解决方案:
# 引入告警抑制和静默
# Alertmanager配置中增加抑制规则
inhibit_rules:
# 当MySQL主库不可用时,抑制从库延迟告警
- source_match:
severity: 'critical'
alertname: 'MySQLDown'
target_match:
severity: 'warning'
alertname: 'MySQLReplicationDelay'
equal: ['instance']
# 当整个集群不可用时,抑制单个节点的告警
- source_match:
severity: 'critical'
alertname: 'ClusterDown'
target_match_re:
severity: '.*'
equal: ['env']
# 设置告警静默时间
# 比如凌晨2点到5点,除了critical级别的告警,其他告警静默
silences:
- matchers:
- name: severity
value: warning|info
isRegex: true
startsAt: "2024-01-01T02:00:00Z"
endsAt: "2024-01-01T05:00:00Z"
comment: "凌晨维护窗口期,静默非紧急告警"
坑3:pt-query-digest分析结果与Prometheus告警不一致
问题:Prometheus告警显示慢查询数量很高,但pt-query-digest分析后发现大部分是正常业务查询。
原因:
long_query_time设置过低(设为0.1秒)- 部分慢查询是正常业务,只是响应时间略长
- 没有区分业务类型
解决方案:
# 调整long_query_time为更合理的值
# 对于电商系统,建议设为0.5秒或1秒
SET GLOBAL long_query_time = 0.5;
# 在pt-query-digest中增加业务类型过滤
pt-query-digest \
--filter "(\$event->{db} = 'orders' || \$event->{db} = 'users') && \
\$event->{Query_time} > 1" \
--top 20 \
/var/log/mysql/slow.log
# 使用pt-query-digest的--digest机制对SQL进行标准化分析
pt-query-digest --digest-time-threshold 0.1 /var/log/mysql/slow.log
坑4:多实例多环境的管理复杂度
问题:公司有多个MySQL实例,分布在多个环境(开发、测试、预发布、生产),管理起来非常复杂。
解决方案:
# 使用统一的服务发现机制
# Prometheus支持多种方式的服务发现
scrape_configs:
- job_name: 'mysql-production'
consul_sd_configs:
- server: 'consul.consul.svc.cluster.local:8500'
services: ['mysql-production']
relabel_configs:
- source_labels: [__meta_consul_tags]
regex: '.*mysql.*'
action: keep
- source_labels: [__meta_consul_service_metadata_env]
regex: 'production'
action: keep
- job_name: 'mysql-staging'
consul_sd_configs:
- server: 'consul.consul.svc.cluster.local:8500'
services: ['mysql-staging']
relabel_configs:
- source_labels: [__meta_consul_service_metadata_env]
regex: 'staging'
action: keep
# 使用Grafana的模板变量实现多环境统一看板
# 在Grafana中设置变量
__variable__mysql_env:
query: label_values(mysql_global_status_threads_connected, env)
refresh: 10s
includeAll: true
# 面板查询中使用模板变量
mysql_global_status_threads_connected{env="$mysql_env"}
六、从3秒到200毫秒:性能优化的具体实践
监控体系搭建完成后,真正的挑战才开始——如何利用监控数据来优化性能。
6.1 第一个月:找到那些”隐形”的慢查询
通过pt-query-digest分析,他们发现了几个之前完全未知的慢查询问题:
问题1:缺少联合索引
-- 原始查询(全表扫描,平均耗时2.3秒)
SELECT * FROM orders
WHERE user_id = 12345
AND status = 'pending'
AND create_time > '2023-09-01'
ORDER BY create_time DESC
LIMIT 20;
-- 添加联合索引后(耗时降至80毫秒)
ALTER TABLE orders
ADD INDEX idx_user_status_time (user_id, status, create_time);
问题2:隐式类型转换导致索引失效
-- 原始查询(user_id是VARCHAR类型,但传的是数字)
-- 耗时1.8秒
SELECT * FROM orders WHERE user_id = 123456;
-- 修正后(正确传递字符串类型)
-- 耗时120毫秒
SELECT * FROM orders WHERE user_id = '123456';
-- 在应用层增加类型校验
-- Java代码示例
public List<Order> getOrders(Long userId) {
// 确保传入的是字符串类型,避免隐式转换
String userIdStr = String.valueOf(userId);
return orderMapper.selectByUserId(userIdStr);
}
问题3:大事务导致锁等待
-- 原始代码(事务过大,持有锁时间长)
@Transactional
public void processOrder(Long orderId) {
// 查询订单
Order order = orderMapper.selectById(orderId);
// 处理业务逻辑(可能调用多个外部接口)
paymentService.pay(order); // 外部支付接口,可能耗时数秒
inventoryService.deduct(order); // 库存扣减
logisticsService.create(order); // 物流创建
// 更新订单状态
orderMapper.updateStatus(orderId, "paid");
}
-- 优化后(缩小事务范围)
// 查询和更新放在事务中,外部调用移出事务
public void processOrder(Long orderId) {
// 先处理外部业务(不在事务中)
paymentService.pay(order);
inventoryService.deduct(order);
logisticsService.create(order);
// 只有状态更新在事务中
transactionTemplate.execute(status -> {
Order order = orderMapper.selectById(orderId);
orderMapper.updateStatus(orderId, "paid");
return null;
});
}
6.2 第二个月:建立SQL审核机制
发现问题后,他们建立了SQL审核机制,从源头防止慢查询的产生。
# SQL审核工具(基于py-sqlparse)
import sqlparse
from sqlparse.tokens import Keyword, DML
class SQLAuditTool:
"""SQL审核工具,检查潜在的性能问题"""
def __init__(self):
self.banned_keywords = ['SELECT *', 'SELECT *']
self.required_indexes = ['WHERE', 'ORDER BY', 'GROUP BY']
def audit(self, sql: str) -> dict:
"""审核SQL,返回审核结果"""
results = {
'valid': True,
'warnings': [],
'errors': [],
'suggestions': []
}
# 解析SQL
parsed = sqlparse.parse(sql)[0]
tokens = list(parsed.tokens)
# 检查是否使用了SELECT *
if any('SELECT *' in str(token).upper() for token in tokens):
results['errors'].append('禁止使用SELECT *,请指定具体字段')
results['valid'] = False
# 检查WHERE条件中的字段类型
where_clause = self._extract_where(tokens)
if where_clause:
# 检查是否有隐式类型转换的风险
for token in where_clause:
if '=' in str(token):
parts = str(token).split('=')
if len(parts) == 2:
left = parts[0].strip()
right = parts[1].strip()
# 检查是否存在引号缺失的情况
if left.isidentifier() and not right.startswith("'"):
try:
float(right)
results['warnings'].append(
f'字段 {left} 可能存在类型不匹配,建议检查索引字段类型'
)
except ValueError:
pass
# 检查是否有ORDER BY without LIMIT
order_by = self._extract_order_by(tokens)
if order_by and 'LIMIT' not in str(tokens).upper():
results['warnings'].append('ORDER BY查询建议添加LIMIT限制')
# 检查查询是否可能全表扫描
if 'WHERE' not in str(tokens).upper():
results['errors'].append('查询缺少WHERE条件,可能导致全表扫描')
results['valid'] = False
return results
def _extract_where(self, tokens):
"""提取WHERE子句"""
in_where = False
where_tokens = []
for token in tokens:
if 'WHERE' in str(token).upper():
in_where = True
continue
if in_where:
if 'ORDER' in str(token).upper() or 'LIMIT' in str(token).upper():
break
where_tokens.append(token)
return where_tokens
def _extract_order_by(self, tokens):
"""提取ORDER BY子句"""
for i, token in enumerate(tokens):
if 'ORDER BY' in str(token).upper():
return tokens[i:]
return []
# 使用示例
audit_tool = SQLAuditTool()
sql = "SELECT * FROM orders WHERE user_id = 123456 ORDER BY create_time"
result = audit_tool.audit(sql)
print(f"审核结果: {result}")
6.3 第三个月:建立性能基线和容量规划
有了监控数据,他们开始建立性能基线,为未来的容量规划提供依据。
# 性能基线分析工具
import pymysql
import numpy as np
from datetime import datetime, timedelta
class PerformanceBaseline:
"""MySQL性能基线分析"""
def __init__(self, db_config):
self.db_config = db_config
def collect_metrics(self, hours=24):
"""收集过去N小时的性能指标"""
conn = pymysql.connect(**self.db_config)
cursor = conn.cursor()
# 收集QPS
cursor.execute("""
SELECT
UNIX_TIMESTAMP(time) as ts,
SUM(if(var_name='Questions', var_value,0)) as questions,
SUM(if(var_name='Slow_queries', var_value,0)) as slow_queries
FROM mysql.global_status
GROUP BY DATE_FORMAT(FROM_UNIXTIME(UNIX_TIMESTAMP(time)), '%Y-%m-%d %H:00:00')
ORDER BY ts DESC
LIMIT %s
""", (hours,))
rows = cursor.fetchall()
cursor.close()
conn.close()
return rows
def calculate_baseline(self, metrics):
"""计算性能基线"""
qps_values = [row[1] for row in metrics]
slow_values = [row[2] for row in metrics]
baseline = {
'qps': {
'avg': np.mean(qps_values),
'p50': np.percentile(qps_values, 50),
'p95': np.percentile(qps_values, 95),
'p99': np.percentile(qps_values, 99),
'max': np.max(qps_values),
'min': np.min(qps_values)
},
'slow_queries': {
'avg': np.mean(slow_values),
'p50': np.percentile(slow_values, 50),
'p95': np.percentile(slow_values, 95),
'max': np.max(slow_values)
}
}
return baseline
def generate_report(self, baseline):
"""生成基线报告"""
report = f"""
MySQL性能基线报告
生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
=== QPS基线 ===
平均QPS: {baseline['qps']['avg']:.2f}
P50 QPS: {baseline['qps']['p50']:.2f}
P95 QPS: {baseline['qps']['p95']:.2f}
P99 QPS: {baseline['qps']['p99']:.2f}
最大QPS: {baseline['qps']['max']:.2f}
最小QPS: {baseline['qps']['min']:.2f}
=== 慢查询基线 ===
平均慢查询数: {baseline['slow_queries']['avg']:.2f}
P50慢查询数: {baseline['slow_queries']['p50']:.2f}
P95慢查询数: {baseline['slow_queries']['p95']:.2f}
最大慢查询数: {baseline['slow_queries']['max']:.2f}
=== 告警建议 ===
建议将QPS告警阈值设为: P95 QPS的1.2倍 = {baseline['qps']['p95'] * 1.2:.0f}
建议将慢查询告警阈值设为: P95慢查询数的1.5倍 = {baseline['slow_queries']['p95'] * 1.5:.0f}
"""
return report
# 使用示例
config = {
'host': 'localhost',
'port': 3306,
'user': 'root',
'password': 'your_password',
'database': 'mysql'
}
baseline = PerformanceBaseline(config)
metrics = baseline.collect_metrics(hours=168) # 过去7天
baseline_data = baseline.calculate_baseline(metrics)
report = baseline.generate_report(baseline_data)
print(report)
七、给不同规模团队的建议
最后,我想根据你团队的实际情况,给出一些针对性的建议。
小型团队(1-2个MySQL实例)
如果你们只有1-2个MySQL实例,业务量不大,我建议:
最简单方案:Percona PMM
- 一键安装,开箱即用
- 内置了MySQL专用的dashboard
- 不需要自己维护复杂的基础设施
次简单方案:MySQL Enterprise Monitor
- 官方工具,稳定性好
- 但需要商业许可
代码示例:PMM一键部署
# 部署PMM Server
docker run -d \
--name pmm-server \
--restart=always \
-p 443:443 \
-v /opt/prometheus/data:/prometheus \
-v /opt/Graphite/data:/opt/graphite/storage/whisper \
percona/pmm-server:latest
# 部署PMM Client(在MySQL服务器上)
docker run -d \
--name pmm-client \
--restart=always \
--network host \
percona/pmm-client:latest \
add mysql \
--username=root \
--password=your_password \
--host=localhost
中型团队(3-10个MySQL实例)
如果你们有3-10个MySQL实例,建议使用Prometheus + Grafana方案:
- 部署Prometheus集群(保证高可用)
- 每个MySQL实例部署一个mysqld_exporter
- 使用Grafana Cloud或自建Grafana
- 配置Alertmanager,接入钉钉/企业微信/邮件
- 每周用pt-query-digest分析慢查询日志
大型团队(10个以上MySQL实例,或分布式数据库)
如果你们有大量的MySQL实例,或者使用了分库分表:
- 考虑使用商业APM方案(如Dynatrace、New Relic)
- 或者自建监控平台,基于Prometheus做二次开发
- 引入分布式追踪(如Jaeger、SkyWalking)
- 建立统一的监控规范和流程
八、总结:监控不是一蹴而就的事情
回过头来看那家电商公司的故事,他们的监控体系不是一蹴而就的。
第一个月,他们搭起了Prometheus + mysqld_exporter + Grafana的基础框架,看到了数据但不知道怎么解读。
第二个月,他们引入了pt-query-digest,开始深入分析慢查询,找到并优化了几个关键的SQL问题。
第三个月,他们完善了告警规则,建立了性能基线,开始用数据驱动决策。
到现在,他们的系统已经稳定运行了大半年,平均响应时间从3秒降到了200毫秒,故障发现时间从2小时缩短到了5分钟。
监控的本质是什么?
监控不是目的,而是手段。最终的目标是让系统更稳定、性能更好、用户体验更优。
一个好的监控体系,应该做到:
- 看得见:能够看到系统的所有关键指标
- 看得懂:能够通过指标判断系统的健康状态
- 看得远:能够通过历史数据预测未来的趋势
- 反应快:出现问题时能够快速发现和响应
希望这篇文章能帮助你建立起自己的MySQL监控体系。如果你有具体的问题,或者需要针对特定场景的方案,欢迎继续交流。
最后分享老王的一句话,这句话被他写在团队的博客首页:
“监控不是为了证明系统没问题,而是为了在系统出问题时,让你知道问题出在哪里。”
