跳到主内容

从故障滞后 10 分钟到 30 秒发现:MySQL exporter 慢查询与主从延迟告警实战 ​

问题背景:数据库故障为什么总是业务先发现 ​

很多团队接入 MySQL 监控后,仍然会遇到两类典型问题:慢查询已经把 CPU 打满,但告警只停留在主机资源层;主从延迟持续升高,读写分离场景开始读到旧数据,业务侧先出现投诉。根本原因不是没有 exporter,而是监控指标、采集账号、告警阈值和故障场景没有打通。

本文以 mysqld_exporter + Prometheus + Alertmanager 为例,搭建一套可落地的 MySQL 监控告警体系,重点覆盖:

  • 慢查询:基于慢查询日志状态、运行时间过长的 SQL、性能诊断表进行发现;
  • 主从延迟:基于 SHOW REPLICA STATUS 暴露的 Seconds_Behind_Source 和 GTID 相关指标;
  • 告警闭环:PromQL 规则、抑制策略、分级通知和故障复盘。

方案对比:外置脚本、云监控与 exporter 怎么选 ​

常见方案有三种。外置脚本灵活,可以直接解析慢查询日志或执行 SHOW PROCESSLIST,但脚本维护成本高,指标格式不统一,容易变成新的故障点。云监控开箱即用,适合快速接入,但自定义 PromQL、多环境聚合和开源生态联动能力有限。

mysqld_exporter 的优势在于标准化:它将 MySQL 状态、复制状态、InnoDB 指标和可选的性能 Schema 指标转换为 Prometheus 时序数据,方便与 Grafana、Alertmanager、Kubernetes 和自建告警平台集成。对于中大型团队,推荐 exporter 作为统一采集层,慢日志分析工具作为补充,而不是用单一工具解决所有问题。

监控架构与数据链路 ​

下图展示 exporter 场景下从 MySQL 实例到告警通知的完整链路。需要注意,exporter 只负责采集和暴露指标,真正的阈值判断发生在 Prometheus,通知路由发生在 Alertmanager。

MySQL 主从实例慢查询/复制状态mysqld_exporter/metrics 指标暴露Prometheus抓取与 PromQL告警规则Rule EvaluationAlertmanager分组/抑制/静默值班群/短信/电话分级通知

实操步骤一:准备可复现的慢查询表和数据 ​

先构造一张订单明细表。该表数据量增长快,常见问题是只对订单号建索引,却按买家、状态和创建时间范围查询,导致扫描行数过大。

sql
-- 创建订单明细表
CREATE TABLE IF NOT EXISTS order_item (
  id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  order_no VARCHAR(32) NOT NULL COMMENT '订单号',
  buyer_id BIGINT UNSIGNED NOT NULL COMMENT '买家 ID',
  status TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0待支付 1已支付 2已完成',
  amount DECIMAL(12,2) NOT NULL DEFAULT 0.00,
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (id),
  UNIQUE KEY uk_order_no (order_no)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单明细表';

-- 写入 10 万行测试数据,借助递归 CTE 生成序号
INSERT INTO order_item (order_no, buyer_id, status, amount, created_at)
WITH RECURSIVE seq AS (
  SELECT 1 AS n
  UNION ALL
  SELECT n + 1 FROM seq WHERE n < 100000
)
SELECT
  CONCAT('O', LPAD(n, 10, '0')),
  100000 + (n % 5000),
  n % 3,
  ROUND(50 + RAND() * 500, 2),
  DATE_SUB(NOW(), INTERVAL (n % 720) HOUR)
FROM seq;\n
-- 典型慢查询:缺少 buyer_id + status + created_at 组合索引
SELECT buyer_id, COUNT(*) AS cnt, SUM(amount) AS total_amount
FROM order_item
WHERE buyer_id BETWEEN 100000 AND 104999
  AND status = 2
  AND created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY buyer_id
ORDER BY total_amount DESC
LIMIT 20;

使用 EXPLAIN ANALYZE 可以看到实际执行耗时和扫描行数。如果 rows examined 远大于返回行数,就需要调整索引。

sql
-- 查看真实执行情况,MySQL 8.0 支持
EXPLAIN ANALYZE
SELECT buyer_id, COUNT(*) AS cnt, SUM(amount) AS total_amount
FROM order_item
WHERE buyer_id BETWEEN 100000 AND 104999
  AND status = 2
  AND created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY buyer_id
ORDER BY total_amount DESC
LIMIT 20;

-- 增加适配过滤条件和分组字段的组合索引
ALTER TABLE order_item
  ADD INDEX idx_buyer_status_created (buyer_id, status, created_at);

索引字段顺序遵循高频等值过滤优先、范围字段靠后的原则。这里 buyer_id 虽然是范围查询,但业务也可能按固定买家查询,实际设计应结合多组 SQL 的谓词分布,不应机械套用。

实操步骤二:开启慢查询日志并部署 exporter ​

在 MySQL 中先确认慢查询配置。生产环境建议将 long_query_time 设置为 1 秒左右,核心交易链路可以逐步收紧到 300 毫秒。

sql
-- 查看慢查询相关参数
SHOW VARIABLES LIKE 'slow_query_log';
SHOW VARIABLES LIKE 'long_query_time';
SHOW VARIABLES LIKE 'log_output';

-- 动态开启慢查询日志,无需重启
SET GLOBAL slow_query_log = ON;
SET GLOBAL log_output = 'FILE';
SET GLOBAL long_query_time = 1;

-- 查看慢查询累计计数
SHOW GLOBAL STATUS LIKE 'Slow_queries';

exporter 需要最小权限的监控账号。不要直接使用 root,也不要授予业务库写权限。

sql
-- 创建 Prometheus 监控账号
CREATE USER 'exporter'@'10.10.%' IDENTIFIED BY 'ChangeMe_Strong_Password';
GRANT PROCESS, REPLICATION CLIENT, SELECT ON *.* TO 'exporter'@'10.10.%';
FLUSH PRIVILEGES;

启动 exporter 时建议显式指定采集器。复制状态依赖 REPLICATION CLIENT 权限;如果需要采集 information_schema.processlist 中的长连接 SQL,需要开启对应采集器,但要评估标签基数和性能开销。

bash
# 启动 mysqld_exporter,示例版本为 0.15.x
./mysqld_exporter \
  --config.my-cnf=/etc/mysqld_exporter/.my.cnf \
  --collect.info_schema.processlist \
  --collect.global_status \
  --collect.slave_status \
  --web.listen-address=:9104

.my.cnf 中不要把密码写入命令行参数,避免出现在进程列表和 Shell 历史中。

ini
[client]
user=exporter
password=ChangeMe_Strong_Password
host=127.0.0.1
port=3306

Prometheus 抓取配置中通过 instance 区分实例,并通过业务、环境、角色等标签支持告警路由。

yaml
global:
  scrape_interval: 15s
  evaluation_interval: 15s

scrape_configs:
  - job_name: mysql
    static_configs:
      - targets: ['10.10.1.11:9104']
        labels:
          env: prod
          role: mysql-master
          service: trade
      - targets: ['10.10.1.12:9104']
        labels:
          env: prod
          role: mysql-replica
          service: trade

实操步骤三:慢查询与主从延迟 PromQL 告警 ​

慢查询告警不能只看累计值,因为 Slow_queries 是单调递增计数器,应该使用 increase() 观察时间窗口内的增量。

yaml
groups:
  - name: mysql-critical
    rules:
      # 5 分钟内新增慢查询超过 30 次,警告
      - alert: MySQLSlowQueriesHigh
        expr: increase(mysql_global_status_slow_queries[5m]) > 30
        for: 2m
        labels:
          severity: warning
        annotations:
          summary: "MySQL 慢查询持续增多"
          description: "实例 {{ $labels.instance }} 在 5 分钟内新增 {{ $value }} 次慢查询,请检查索引和执行计划。"

      # 当前存在执行超过 60 秒的 SQL,严重告警
      - alert: MySQLLongRunningQuery
        expr: mysql_processlist_time_seconds{state!="Sleep"} > 60
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "MySQL 存在长时间运行 SQL"
          description: "实例 {{ $labels.instance }} 用户 {{ $labels.user }} 的 SQL 已运行超过 60 秒。"

主从延迟要同时判断复制线程状态和延迟秒数。只看延迟可能漏掉 IO 线程中断;只看线程状态又无法发现大事务导致的持续追数。

yaml
      # 从库复制 SQL/IO 线程异常
      - alert: MySQLReplicationThreadDown
        expr: mysql_slave_status_slave_io_running != 1 or mysql_slave_status_slave_sql_running != 1
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "MySQL 复制线程异常"
          description: "实例 {{ $labels.instance }} 复制线程停止,请立刻检查错误号和主库网络、GTID、DDL 状态。"

      # 主从延迟超过 30 秒
      - alert: MySQLReplicationLagHigh
        expr: mysql_slave_status_seconds_behind_master > 30
        for: 2m
        labels:
          severity: warning
        annotations:
          summary: "MySQL 主从延迟过高"
          description: "实例 {{ $labels.instance }} 延迟 {{ $value }} 秒,读写分离可能读到旧数据。"

不同 exporter 版本对 MySQL 8.0 的 source/replica 命名可能存在差异。上线前应在 Prometheus 页面执行 mysql_slave_status_ 或 mysql_replication_ 前缀查询,以实际指标名为准。

告警分级与通知抑制 ​

主库故障时,从库延迟、连接数升高、慢查询增多会同时触发,值班人员容易被告警风暴淹没。Alertmanager 应按 env、service、instance 分组,并配置主库严重故障抑制从库延迟告警。

yaml
route:
  group_by: ['alertname', 'env', 'service', 'instance']
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 1h
  receiver: 'dba-oncall'

inhibit_rules:
  - source_matchers:
      - alertname = MySQLReplicationThreadDown
    target_matchers:
      - severity = warning
    equal: ['env', 'service', 'instance']

避坑总结:监控上线前必须检查的细节 ​

  1. 不要把累计计数器当成当前值告警。 常见错误是直接写 mysql_global_status_slow_queries > 100,实例运行时间越长越必然触发。修正方法是使用 increase(mysql_global_status_slow_queries[5m]) 或 rate() 观察增量。

  2. 不要只告警 Seconds_Behind_Master。 常见现象是复制线程已经断开,延迟值保持旧值或为空,告警却没有触发。修正方法是同时检查 IO/SQL 线程状态、延迟秒数和 GTID 差距,并在从库端持续抓取 exporter。

  3. 不要让 SQL 摘要成为高基数字段。 如果把完整 SQL 文本、SQL ID、用户会话 ID 直接做成 Prometheus 标签,一次批量任务就可能产生大量时间序列,导致 Prometheus 内存上涨。正确做法是 Prometheus 只告警实例和用户维度,详细 SQL 通过慢日志平台或 processlist 下钻。

  4. 慢查询阈值不要长期设置过大。 long_query_time=10 只能发现严重故障,无法提前发现核心接口退化。建议先从 1 秒开始,结合业务 SLA 逐步收紧,并通过索引优化、SQL 改写和归档降低噪音。

  5. 主从延迟告警要考虑大事务和在线 DDL。 单表大批量更新、无主键事务、DDL 回放都会让延迟突增。处理时不要盲目重启复制线程,应先查看当前回放语句、未提交事务和磁盘 IO,再决定限流、拆分事务或切换读写流量。

通过以上配置,团队可以把数据库问题从“业务投诉后排查”前移到“指标异常即通知”。真正稳定的监控体系不是规则越多越好,而是每个告警都能对应明确影响、处理人和排查路径。

免责声明

本工具与文章内容仅用于学习测试场景,优化建议仅供参考,请勿直接用于生产环境,由此产生的风险自行承担。