某电商大促半夜宕机损失百万MySQL性能监控工具帮你守住底线:从慢查询到实时告警一键搞定
那个凌晨三点,客服铃声把全公司叫醒
三年前,某头部生鲜电商的双十一大促,凌晨2:47分,主数据库突然响应超时,订单接口全部告急。等到DBA被电话叫醒冲进公司时,已经过去了23分钟——这23分钟里,系统吞掉了整整127万笔待支付订单,最终结算下来,直接损失加品牌赔偿超过380万。
事后复盘,问题出在一个看似不起眼的慢查询:SELECT * FROM products WHERE category_id IN (SELECT category_id FROM hot_categories WHERE activity_id=8821) —— 这个子查询在大促期间返回了180万行数据,而主表没有加索引,直接导致全表扫描拖垮了主库。
如果当时有一套完善的MySQL性能监控体系,这个坑完全可以提前规避。
今天我们就来聊聊,如何用监控工具守住数据库这条生命线。
为什么电商大促对MySQL是”生死考验”?
先别急着看工具,咱们先把场景搞清楚。普通人可能觉得数据库不就是存数据的地方吗?但对电商大促来说,数据库承受的是指数级压力。
假设平时你的日活是10万人,并发请求2000 QPS。大促期间,这个数据可能直接变成100万日活、20000 QPS,甚至更高。这种10倍以上的流量突增,会让平时运行正常的SQL突然变成性能杀手。
我遇到过不少案例,平时跑在10毫秒内的查询,大促期间飙到3秒以上。原因很简单:
- 连接数打满,新请求排队
- 缓冲池命中率下降,磁盘IO暴增
- 慢查询积累,形成雪崩效应
- 主从延迟拉大,读取数据不一致
所以,监控不是锦上添花,而是保命符。
慢查询:找出那些”隐形杀手”
慢查询是MySQL性能问题的”第一现场”。很多DBA都知道要开慢查询日志,但真正做到的不多。我们先来配置,再来看分析。
第一步:开启慢查询日志
-- 查看当前慢查询配置
SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';
-- 开启慢查询日志(动态修改,无需重启)
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';
SET GLOBAL long_query_time = 1; -- 超过1秒的查询才记录
-- 如果某些查询本身就很耗时,可以单独记录
SET GLOBAL log_queries_not_using_indexes = ON;
第二步:用mysqldumpslow分析慢查询
# 安装percona工具包(强烈推荐)
sudo apt-get install percona-toolkit
# 分析慢查询日志,按查询次数排序
mysqldumpslow -s c -t 10 /var/log/mysql/slow.log
# 分析慢查询日志,按平均耗时排序
mysqldumpslow -s t -t 10 /var/log/mysql/slow.log
# 只看包含特定关键字的慢查询
mysqldumpslow -s t -t 10 -g "category_id" /var/log/mysql/slow.log
第三步:实战分析一个真实案例
这是某电商平台大促期间抓到的典型慢查询:
-- 订单详情页查询(大促期间平均耗时4.2秒!)
SELECT
o.order_id,
o.user_id,
o.order_amount,
o.status,
oi.product_id,
p.product_name,
p.stock,
u.username,
u.phone,
a.address,
a.city
FROM orders o
JOIN order_items oi ON o.order_id = oi.order_id
JOIN products p ON oi.product_id = p.product_id
LEFT JOIN users u ON o.user_id = u.user_id
LEFT JOIN addresses a ON u.default_address_id = a.address_id
WHERE o.order_id = 98765432
AND o.status IN (1, 2, 3, 4, 5);
用 EXPLAIN 分析一下:
EXPLAIN
SELECT
o.order_id,
o.user_id,
o.order_amount,
o.status,
oi.product_id,
p.product_name,
p.stock,
u.username,
u.phone,
a.address,
a.city
FROM orders o
JOIN order_items oi ON o.order_id = oi.order_id
JOIN products p ON oi.product_id = p.product_id
LEFT JOIN users u ON o.user_id = u.user_id
LEFT JOIN addresses a ON u.default_address_id = a.address_id
WHERE o.order_id = 98765432
AND o.status IN (1, 2, 3, 4, 5);
结果发现:orders 表虽然对 order_id 有索引,但 status IN (...) 这部分没有走索引,导致回表扫描量过大。
优化方案:
-- 方案1:添加联合索引
ALTER TABLE orders ADD INDEX idx_order_status (order_id, status);
-- 方案2:拆分查询,先查主表,再按需关联
-- 业务层改造:先获取订单基本信息,再单独查商品和用户详情
实时监控:别再等出事了才发现问题
慢查询日志是”事后诸葛亮”,真正的运维需要实时监控。下面介绍几个主流方案。
方案一:Prometheus + Grafana(开源首选)
# prometheus.yml 配置
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'mysql'
static_configs:
- targets: ['mysql-exporter:9104']
metrics_path: /metrics
# 启动MySQL exporter
docker run -d \
--name mysql_exporter \
-p 9104:9104 \
-e DATA_SOURCE_NAME="root:password@tcp(db_host:3306)/" \
prom/mysqld-exporter
Grafana面板推荐导入模板:ID 7362(MySQL Overview)和 ID 893(MySQL Dashboard)。
方案二:Percona Monitoring and Management(PMM)
PMM是Percona公司出的免费监控方案,功能非常强大:
# 一键部署PMM Server
docker run -d \
--name pmm-server \
--restart always \
-p 443:443 \
-v /opt/monitordata:/var/lib/mysql \
percona/pmm-server:latest
# 注册MySQL实例
docker run -d \
--name pmm-client \
--restart always \
-v /sys:/host/sys:ro \
-v /proc:/host/proc:ro \
percona/pmm-client:latest \
add mysql \
--username=root \
--password=your_password \
--host=db-host \
--port=3306
PMM的优势在于它内置了查询分析(Query Analytics)功能,可以实时看到每条SQL的耗时、调用次数、扫描行数等关键指标。
方案三:阿里云RDS监控 / AWS CloudWatch
如果你用的是云数据库,那监控配置就简单多了。但要注意:云监控的延迟通常在1-5分钟,对于大促这种分秒必争的场景,可能来不及反应。
实时告警:让问题在萌芽阶段就被发现
监控了不告警,等于没监控。下面介绍如何配置有效的告警体系。
告警阈值设计原则
不要一上来就配100个告警规则,那样会把你淹死在”告警风暴”里。正确的做法是分层告警:
| 级别 | 触发条件示例 | 响应时间 |
|---|---|---|
| P0(紧急) | 主库宕机、从库延迟>30秒 | 5分钟内响应 |
| P1(重要) | 慢查询>100条/分钟、连接数>80% | 15分钟内响应 |
| P2(一般) | CPU使用率>70%、磁盘使用率>80% | 2小时内处理 |
| P3(提示) | 备份失败、配置变更 | 下一个工作日 |
基于Alertmanager配置告警
# alertmanager.yml
global:
resolve_timeout: 5m
route:
group_by: ['alertname', 'instance']
group_wait: 10s
group_interval: 10s
repeat_interval: 4h
receiver: 'wechat_notify'
routes:
- match:
severity: 'p0'
receiver: 'phone_call'
repeat_interval: 1h
- match:
severity: 'p1'
receiver: 'dingtalk'
repeat_interval: 2h
receivers:
- name: 'wechat_notify'
wechat_configs:
- corp_id: 'xxx'
agent_id: 'xxx'
to_user: '@all'
secret: 'xxx'
api_url: 'https://qyapi.weixin.qq.com/cgi-bin/'
- name: 'dingtalk'
dingtalk_configs:
- send_resolved: true
msg_type: 'action_card'
title: 'MySQL告警'
url: 'http://xxx'
single_title: '查看详情'
btns:
- text: '立即处理'
url: 'http://xxx'
- name: 'phone_call'
webhook_configs:
- url: 'http://call-api:8080/alert'
send_resolved: true
核心告警规则示例
# prometheus告警规则
groups:
- name: mysql_critical
rules:
- alert: MySQL主库宕机
expr: mysql_up == 0
for: 1m
labels:
severity: 'p0'
annotations:
summary: "MySQL主库 {{ $labels.instance }} 宕机"
description: "MySQL实例在 {{ $labels.instance }} 已经宕机超过1分钟,请立即处理!"
runbook_url: "https://wiki.example.com/runbooks/mysql-down"
- alert: MySQL从库延迟过高
expr: mysql_slave_status_seconds_behind_master > 30
for: 5m
labels:
severity: 'p0'
annotations:
summary: "MySQL从库 {{ $labels.instance }} 延迟超过30秒"
description: "当前延迟: {{ $value }} 秒"
- alert: MySQL连接数使用率过高
expr: mysql_global_status_threads_connected / mysql_global_variables_max_connections * 100 > 80
for: 5m
labels:
severity: 'p1'
annotations:
summary: "MySQL连接数使用率超过80%"
description: "当前使用率: {{ $value }}%"
- alert: MySQL慢查询激增
expr: increase(mysql_global_status_slow_queries[5m]) > 50
for: 5m
labels:
severity: 'p1'
annotations:
summary: "MySQL慢查询数量激增"
description: "过去5分钟新增慢查询: {{ $value }} 条"
- alert: MySQL缓冲池命中率过低
expr: mysql_global_status_innodb_buffer_pool_read_requests > 0 and
(1 - mysql_global_status_innodb_buffer_pool_reads /
mysql_global_status_innodb_buffer_pool_read_requests) * 100 < 95
for: 10m
labels:
severity: 'p2'
annotations:
summary: "MySQL缓冲池命中率低于95%"
description: "当前命中率: {{ $value | printf \"%.2f\" }}%"
- alert: MySQL主从复制中断
expr: mysql_slave_status_slave_io_running == 0 or
mysql_slave_status_slave_sql_running == 0
for: 1m
labels:
severity: 'p0'
annotations:
summary: "MySQL主从复制中断"
description: "IO线程或SQL线程已停止,请立即检查!"
大促前的”健康体检”清单
监控和告警配好了,大促前你还需要做一份全面的健康检查。这是我的实战清单:
1. 索引健康检查
-- 查找没有主键的表
SELECT TABLE_SCHEMA, TABLE_NAME
FROM information_schema.TABLES
WHERE TABLE_TYPE = 'BASE TABLE'
AND TABLE_SCHEMA NOT IN ('information_schema', 'performance_schema', 'mysql')
AND TABLE_NAME NOT IN (
SELECT TABLE_NAME
FROM information_schema.TABLE_CONSTRAINTS
WHERE CONSTRAINT_TYPE = 'PRIMARY KEY'
AND TABLE_SCHEMA = information_schema.TABLES.TABLE_SCHEMA
);
-- 查找重复索引
SELECT
t.TABLE_SCHEMA,
t.TABLE_NAME,
idx.INDEX_NAME,
GROUP_CONCAT(idx.COLUMN_NAME ORDER BY idx.SEQ_IN_INDEX) AS columns
FROM information_schema.STATISTICS idx
JOIN information_schema.TABLES t ON idx.TABLE_SCHEMA = t.TABLE_SCHEMA
AND idx.TABLE_NAME = t.TABLE_NAME
GROUP BY t.TABLE_SCHEMA, t.TABLE_NAME, idx.INDEX_NAME
HAVING COUNT(*) > 1;
-- 查找从未使用的索引(需要开启性能_schema)
SELECT
OBJECT_SCHEMA,
OBJECT_NAME,
INDEX_NAME
FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE OBJECT_SCHEMA NOT IN ('mysql', 'performance_schema', 'information_schema')
AND INDEX_NAME IS NOT NULL
AND COUNT_STAR = 0
AND last_used IS NOT NULL
AND last_used < DATE_SUB(NOW(), INTERVAL 7 DAY);
2. 表空间清理
-- 查看大表碎片情况
SELECT
table_name,
data_length,
data_free,
ROUND(data_free / (data_length + data_free) * 100, 2) AS fragment_percent
FROM information_schema.tables
WHERE table_schema = 'your_database'
AND data_free > 10 * 1024 * 1024 -- 碎片超过10MB
ORDER BY data_free DESC;
-- 清理碎片(注意:大表在线操作有风险,建议低峰期执行)
OPTIMIZE TABLE your_large_table;
3. 配置参数检查
-- 关键参数检查
SELECT
variable_name,
variable_value,
CASE
WHEN variable_name = 'innodb_buffer_pool_size' THEN
CONCAT(ROUND(variable_value/1024/1024/1024, 2), ' GB')
WHEN variable_name = 'innodb_log_file_size' THEN
CONCAT(ROUND(variable_value/1024/1024, 2), ' MB')
WHEN variable_name = 'max_connections' THEN
CONCAT(variable_value, ' (建议: 当前连接数/这个值 < 0.8)')
ELSE variable_value
END AS formatted_value
FROM information_schema.global_variables
WHERE variable_name IN (
'innodb_buffer_pool_size',
'innodb_log_file_size',
'innodb_flush_log_at_trx_commit',
'max_connections',
'slow_query_log',
'long_query_time',
'query_cache_type',
'thread_cache_size',
'table_open_cache'
);
大促期间的”战时状态”管理
大促当天,监控团队需要切换到”战时状态”:
值班安排
不要指望一个DBA能扛住大促。我推荐的配置是:
- 主DBA:负责整体协调和重大决策
- 副DBA:负责日常监控和告警处理
- 运维工程师:负责服务器资源监控
- 开发负责人:负责应用层问题排查
实时监控看板
准备一个大屏看板,展示关键指标:
┌─────────────────────────────────────────────────────┐
│ 🚨 大促实时监控大屏 - 2024双11 │
├─────────────────────────────────────────────────────┤
│ MySQL主库状态: ✅ 正常 连接数: 847/2000 (42%) │
│ 慢查询/分钟: 23 QPS: 12,847 │
│ 从库延迟: 0.3秒 CPU使用率: 67% │
│ │
│ ⚠️ 近期告警: │
│ [14:23] 慢查询激增: SELECT * FROM orders WHERE... │
│ [14:18] 连接数使用率达85% │
│ [13:52] 从库延迟0.8秒(已恢复) │
└─────────────────────────────────────────────────────┘
应急预案
每个团队都应该有应急预案卡,内容包括:
【MySQL应急预案卡】
问题1: 主库CPU打满
├─ 第一步: 查看top SQL: pt-query-digest slow.log
├─ 第二步: 杀掉异常会话: KILL [process_id]
├─ 第三步: 如果无法快速定位,考虑主从切换
└─ 升级阈值: CPU > 90% 持续5分钟
问题2: 从库延迟过高
├─ 第一步: 检查主从复制状态: SHOW SLAVE STATUS
├─ 第二步: 检查是否有大事务阻塞
├─ 第三步: 临时提升从库性能(减少binlog刷盘频率)
└─ 升级阈值: 延迟 > 60秒
问题3: 连接数打满
├─ 第一步: 查看连接来源: SHOW PROCESSLIST
├─ 第二步: 杀掉空闲连接: KILL [idle_process_id]
├─ 第三步: 临时调大max_connections
└─ 升级阈值: 连接数 > 90% 持续3分钟
工具推荐:从轻量级到企业级
根据不同的预算和规模,我推荐以下几套方案:
轻量级方案(适合中小团队)
mysqld-exporter + Prometheus + Grafana
- 成本:免费
- 部署难度:低
- 功能:基础监控 + 自定义告警
- 适合:日活100万以下的电商
标准方案(适合成长型企业)
Percona Monitoring and Management (PMM)
- 成本:免费(开源)
- 部署难度:中等
- 功能:全面的监控 + 查询分析 + 性能诊断
- 适合:日活100-500万的电商
企业级方案
Datadog / New Relic / 阿里云云监控
- 成本:按量付费,较高
- 部署难度:低
- 功能:全栈监控 + AI智能告警 + 根因分析
- 适合:日活500万以上的头部电商
国产替代方案
腾讯云CLS / 阿里云SLS + ARMS
- 成本:中等
- 部署难度:低
- 功能:日志分析 + 链路追踪 + 应用监控
- 适合:使用对应云服务的电商
最后说一句:监控是”预防医学”,不是”重症监护”
回到开头那个案例,如果那个电商团队在大促前做了完善的监控和演练,127万笔订单的损失完全可以避免。
监控的核心价值不是”出事后的抢救”,而是”出问题前的预防”。
这套体系建立起来需要时间,但绝对值得。建议从现在开始:
- 本周:开启慢查询日志,收集一周的慢查询数据
- 下周:部署基础监控(Prometheus + Grafana)
- 下个月:配置告警规则,完成第一次压力测试
- 大促前:进行全流程应急演练
数据库是电商的”心脏”,心脏出问题,全系统都停摆。把监控做好,就是给心脏装上起搏器——平时看不出什么,关键时刻能救命。
如果你正在筹备大促,现在就开始行动吧。等出了问题再补救,黄花菜都凉了。
