263 lines
12 KiB
Markdown
263 lines
12 KiB
Markdown
---
|
||
tags: [MySQL, 监控指标, QPS, TPS, Buffer Pool Hit Rate, Prometheus, Grafana, InnoDB]
|
||
create time: 2026-05-16 00:00
|
||
---
|
||
|
||
# 监控指标
|
||
|
||
## 概述
|
||
|
||
有效的监控体系能让你在问题爆发前发现问题。MySQL 的监控涵盖 **性能**、**可用性**、**容量** 和 **安全** 四个维度。
|
||
|
||
> [!TIP] 核心思想
|
||
> 监控的目的不是「收集更多数据」,而是 **「用最少的关键指标发现最多的问题」**。初学者常犯的错误是把所有 SHOW STATUS 变量都记录下来——但真正需要告警的可能不到 20 个。先定义你的 SLA/SLO,再决定监控什么。
|
||
|
||
一个典型的 MySQL 监控栈自下而上:
|
||
|
||
```mermaid
|
||
graph TD
|
||
A["MySQL Server"] -->|"SHOW GLOBAL STATUS / performance_schema"| B["mysqld_exporter"]
|
||
B -->|"HTTP scrape"| C["Prometheus"]
|
||
C -->|"查询 + 告警规则"| D["Grafana"]
|
||
C -->|"Alertmanager"| E["告警通道\n钉钉/飞书/PagerDuty"]
|
||
D -->|"Dashboard"| F["运维 & 开发"]
|
||
|
||
style A fill:#f9d,stroke:#333
|
||
style C fill:#bee,stroke:#333
|
||
style D fill:#ddf,stroke:#333
|
||
style E fill:#fdb,stroke:#333
|
||
```
|
||
|
||
理解了这个架构,你就能定位任何一个环节出问题时该检查哪里。
|
||
|
||
## 核心指标速查
|
||
|
||
### 吞吐量类
|
||
|
||
| 指标 | SHOW STATUS 名称 | 健康范围 | 说明 |
|
||
|------|-----------------|---------|------|
|
||
| **QPS** | `Queries` | 视硬件而定 | 包含内部重写(如 INSERT → multiple row),非客户端请求数 |
|
||
| **TPS** | `Com_commit` + `Com_rollback` | 视硬件而定 | 只统计 InnoDB 事务型操作 |
|
||
| **Threads Connected** | `Threads_connected` | < MaxConnections × 0.8 | 当前已建立的连接总数 |
|
||
| **Threads Running** | `Threads_running` | < CPU 核心数 | 正在执行查询的线程数(瞬时高峰可短暂超限) |
|
||
|
||
> [!WARNING] QPS ≠ 客户端请求数
|
||
> `Queries` 计数的是服务器内部处理的查询数,而非客户端发送的请求数。一条 `INSERT INTO t VALUES(1),(2),(3)` 会被计数为 1 次 Client Query,但可能被优化器展开后计为多次 Internal Queries。如果需要精确的客户端请求量,应看 `Com_select` + `Com_insert` + `Com_update` + `Com_delete` 的总和。
|
||
|
||
```sql
|
||
-- 计算实时 QPS 和 TPS
|
||
SHOW GLOBAL STATUS LIKE 'Queries'; -- 累计查询数
|
||
SHOW GLOBAL STATUS LIKE 'Com_commit'; -- 累计 commit 数
|
||
SHOW GLOBAL STATUS LIKE 'Com_rollback'; -- 累计 rollback 数
|
||
|
||
-- QPS = Queries / Uptime
|
||
-- TPS = (Com_commit + Com_rollback) / Uptime
|
||
```
|
||
|
||
```go
|
||
// Go 中采集指标的推荐方式:直接查询并返回 map
|
||
func collectStatus(db *sql.DB) (map[string]string, error) {
|
||
rows, err := db.Query("SHOW GLOBAL STATUS")
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
|
||
metrics := make(map[string]string)
|
||
for rows.Next() {
|
||
var name, value string
|
||
rows.Scan(&name, &value) // value 可能是数字或字符串,统一读取为 string
|
||
metrics[name] = value
|
||
}
|
||
return metrics, rows.Err()
|
||
}
|
||
```
|
||
|
||
这段代码的核心思路是 **一次查询拿全部指标**,避免 N+1 次网络往返。实际生产环境中,你会在每个指标之间做差值运算得到速率,详见下方公式推导。
|
||
|
||
**速率计算公式:**
|
||
|
||
```
|
||
ΔQPS = (Queries_now - Queries_prev) / (Timestamp_now - Timestamp_prev)
|
||
ΔTPS = ((Com_commit_now + Com_rollback_now) - (Com_commit_prev + Com_rollback_prev)) / ΔSeconds
|
||
```
|
||
|
||
Prometheus 的 `mysqld_exporter` 会自动帮你完成这个差值计算,如果用原生 SQL 采样则需自行实现。
|
||
|
||
### InnoDB 引擎指标
|
||
|
||
InnoDB 是绝大多数 MySQL 业务场景使用的存储引擎,其内部指标决定了数据库的整体健康状况。
|
||
|
||
| 指标 | SHOW STATUS 名称 | 健康阈值 | 说明 |
|
||
|------|-----------------|---------|------|
|
||
| **Buffer Pool 读请求** | `Innodb_buffer_pool_read_requests` | — | 从 Buffer Pool 中读取的逻辑请求总数 |
|
||
| **Buffer Pool 磁盘读取** | `Innodb_buffer_pool_reads` | 命中率 ≥ 99% | 需回磁盘读取的次数(未命中) |
|
||
| **Redo Log 写入量** | `Innodb_data_written` | 突增意味着写入密集 | 累计写入 Redo Log 的数据字节数 |
|
||
| **Redo Log flush 等待** | `Innodb_log_waits` | 应接近 0 | Redo Log 缓冲区满导致刷盘等待 |
|
||
| **死锁次数** | `Innodb_deadlocks` | 不应持续增加 | 每次死锁回滚一个事务 |
|
||
| **平均行锁等待** | `Innodb_row_lock_time_avg` | < 10ms | 行级锁平均等待时间 |
|
||
| **行锁总等待时间** | `Innodb_row_lock_time` | 监控趋势 | 所有行锁等待累积耗时(ms) |
|
||
| **DML 计数** | `Innodb_rows_deleted/inserted/updated` | 监控比例变化 | 各类 DML 操作的累计次数 |
|
||
|
||
> [!NOTE] 如何判断 Buffer Pool 大小是否合理?
|
||
> 只看命中率是不够的。更可靠的方法是观察 `Innodb_buffer_pool_reads` 的增长斜率:如果它几乎是一条水平线(单位时间内增长极少),说明当前 Buffer Pool 够用;如果斜率持续走高,就是增大 `innodb_buffer_pool_size` 的信号。一般建议设为物理内存的 **50%~70%**。
|
||
|
||
```sql
|
||
-- Buffer Pool 命中率(推荐写法)
|
||
SELECT
|
||
(1 - A.value / B.value) * 100 AS hit_rate_pct
|
||
FROM (SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME = 'Innodb_buffer_pool_reads') A,
|
||
(SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME = 'Innodb_buffer_pool_read_requests') B;
|
||
|
||
-- 为什么不用 INFORMATION_SCHEMA.INNODB_METRICS?
|
||
-- 该表需要额外启用,且部分版本中字段名不一致,推荐使用 global_status 视图
|
||
```
|
||
|
||
> [!QUESTION] 思考
|
||
> 假设你的 Buffer Pool 命中率为 99.5%,但这个值是从服务启动至今累计计算的。如果最近一小时内出现了缓存穿透,累计比率会显示出来吗?
|
||
> **答案**:不会——累计值会稀释短期异常。正确的做法是用定时采集的 **差值** 来计算窗口内的实时命中率。
|
||
|
||
### 复制延迟指标
|
||
|
||
主从复制是 MySQL 高可用的基石。延迟超过容忍度,读写分离就会带来数据一致性问题。
|
||
|
||
| 指标 | SHOW REPLICA / SLAVE STATUS 字段 | 告警阈值 | 说明 |
|
||
|------|--------------------------------|---------|------|
|
||
| **Seconds_Behind_Master** | `Seconds_Behind_Master` | > 5s 警告,> 30s 严重 | 从库落后主库的秒数(NULL 表示连接断开) |
|
||
| **IO Thread** | `Slave_IO_Running` (5.7) / `Replica_IO_Running` (8.0) | 必须 = Yes | IO 线程负责拉取 binlog |
|
||
| **SQL Thread** | `Slave_SQL_Running` (5.7) / `Replica_SQL_Running` (8.0) | 必须 = Yes | SQL 线程负责重放事件 |
|
||
| **Relay Log 空间** | `Relay_Log_Space` | 突增预警 | 中继日志积压通常意味着大事务 |
|
||
|
||
```sql
|
||
-- 标准查看方式(MySQL 8.0.22+ 使用 REPLICA 关键字)
|
||
SHOW REPLICA STATUS\G
|
||
|
||
-- MySQL 8.0+: 基于 GTID 的更精确延迟检测
|
||
SELECT
|
||
PROCESSLIST_TIME AS delay_seconds,
|
||
INFO
|
||
FROM performance_schema.threads t
|
||
JOIN performance_schema.events_stages_current ess
|
||
ON t.THREAD_ID = ess.THREAD_ID
|
||
WHERE NAME = 'stage/replication_sql_waiting_for_master_to_send_event';
|
||
```
|
||
|
||
> [!CAUTION] Seconds_Behind_Master 的陷阱
|
||
> 当从库停止时,这个值会变成 NULL 而非一个巨大的数字。因此告警逻辑应同时检查 `Slave_IO_Running = No`、`Slave_SQL_Running = No` 和 `Seconds_Behind_Master IS NULL` 三种情况。
|
||
|
||
## 慢查询趋势
|
||
|
||
慢查询日志不只是排障工具,更是 **长期性能趋势** 的风向标。每天慢查询数量的突然增加,往往预示着即将发生的系统性问题。
|
||
|
||
```sql
|
||
-- 每日慢查询统计
|
||
SELECT DATE_FORMAT(event_time, '%Y-%m-%d') AS day,
|
||
COUNT(*) AS slow_count,
|
||
ROUND(AVG(query_time), 3) AS avg_time,
|
||
ROUND(MAX(query_time), 3) AS max_time
|
||
FROM mysql.slow_log
|
||
GROUP BY day
|
||
ORDER BY day DESC
|
||
LIMIT 30;
|
||
```
|
||
|
||
> [!TIP] 实操建议
|
||
> 在生产环境不建议直接查 `mysql.slow_log`(它是表格式存储,数据量大时性能差)。推荐使用 Percona 的 **[pt-query-digest](https://www.percona.com/doc/percona-toolkit/LATEST/pt-query-digest.html)**,它能聚合相似查询并按类型统计,还能输出 HTML 报告。
|
||
|
||
一个实用的告警规则设计:
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
A["Slow Query 阈值\nquery_time > 1s"] --> B{"单条超时 > 10s?"}
|
||
B -->|是| C["立即告警\nP1 级别"]
|
||
B -->|否| D{"当日累计 > 昨日 2x?"}
|
||
D -->|是| E["趋势告警\nP2 级别"]
|
||
D -->|否| F["正常记录"]
|
||
|
||
style C fill:#f99,stroke:#c00
|
||
style E fill:#fd9,stroke:#ca0
|
||
style F fill:#9f9,stroke:#0a0
|
||
```
|
||
|
||
这种分级告警能帮你区分「偶发异常」和「系统性恶化」,避免告警疲劳。
|
||
|
||
## 告警分级策略
|
||
|
||
好的监控应该告诉你 **现在该做什么**,而不是让你从一堆告警里自己判断。
|
||
|
||
| 级别 | 触发条件示例 | 响应时效 | 通知渠道 |
|
||
|------|-------------|---------|---------|
|
||
| **P0 致命** | Slave_SQL_Running = No、磁盘空间 < 5%、Innodb_deadlocks > 0/min | 5 分钟内 | 电话 + 短信 + IM |
|
||
| **P1 严重** | 延迟 > 30s、Threads_running > CPU×2、QPS 骤降 50%+ | 15 分钟内 | 短信 + IM |
|
||
| **P2 警告** | 延迟 > 5s、命中率 < 95%、慢查询激增 | 1 小时内 | IM 群 |
|
||
| **P3 提示** | Buffer Pool 利用率持续下降、表空间增长过快 | 当天处理 | 日报/周报 |
|
||
|
||
> [!QUESTION] 你的 SLA 是什么?
|
||
> 不同的业务对可用性的要求差异巨大:金融系统可能要求 99.999%,而内部工具 99.9% 就足够了。**先明确业务目标,再反过来设计告警阈值**。不要盲目套用别人的配置。
|
||
|
||
## 监控工具选型
|
||
|
||
| 工具 | 定位 | 优点 | 缺点 |
|
||
|------|------|------|------|
|
||
| **Prometheus + mysqld_exporter** | 指标采集 | 云原生标准、告警丰富 | 需自建 Grafana Dashboard |
|
||
| **PMM (Percona)** | 完整监控平台 | 开箱即用、包含 QAN 分析 | 较重,占用资源 |
|
||
| **Zabbix** | 传统监控 | 成熟稳定、报警灵活 | 缺少查询级洞察 |
|
||
| **Datadog/NewRelic** | SaaS 监控 | 零运维、可视化优秀 | 付费、数据出网 |
|
||
| **阿里云 RDS 监控** | 云服务内置 | 免费、深度集成 | 锁定云厂商 |
|
||
|
||
### Prometheus + mysqld_exporter 快速搭建
|
||
|
||
```yaml
|
||
# prometheus.yml 追加
|
||
scrape_configs:
|
||
- job_name: 'mysql'
|
||
static_configs:
|
||
- targets: ['localhost:9104'] # mysqld_exporter 端口
|
||
```
|
||
|
||
```bash
|
||
# 启动 exporter(MySQL 5.7)
|
||
mysqld_exporter \
|
||
--config.my-cnf=/etc/mysql/debian.cnf \
|
||
--collect.global_status \
|
||
--collect.engine_innodb_status \
|
||
--collect.perf_schema.tableiowaits
|
||
|
||
# MySQL 8.0 推荐额外开启 perf_schema 指标
|
||
mysqld_exporter \
|
||
--config.my-cnf=/etc/mysql/debian.cnf \
|
||
--collect.global_status \
|
||
--collect.info_schema.innodb_metrics \
|
||
--collect.perf_schema.eventswaitssummary
|
||
```
|
||
|
||
> [!NOTE] mysqld_exporter 权限
|
||
> exporter 需要专门的只读账号,至少授予 `REPLICATION CLIENT`, `PROCESS`, `SUPER` 权限即可。切勿用 root 账号运行。
|
||
|
||
## Grafana 关键 Dashboard
|
||
|
||
以下面板组合作为基线监控,可根据自身需求增减:
|
||
|
||
```
|
||
1. Overview: QPS, TPS, Connections, Slow Queries
|
||
2. InnoDB Buffer Pool: Hit Rate, Dirty Pages, Free Pages
|
||
3. Replication: Seconds Behind Master, IO/SQL Thread Lag
|
||
4. Disk I/O: Read/Write Latency, Throughput
|
||
5. Network: Bytes Sent/Received per Second
|
||
6. Memory: Used vs Available
|
||
```
|
||
|
||
社区推荐的 Dashboard ID(导入到 Grafana 即可):
|
||
|
||
| 主题 | Dashboard ID | 作者 |
|
||
|------|-------------|------|
|
||
| MySQL Overview | [3131](https://grafana.com/grafana/dashboards/3131-mysql-database/) | Google |
|
||
| Percona MySQL | [9967](https://grafana.com/grafana/dashboards/9967-percona-server-mysql-dashboard/) | Percona |
|
||
| mysqld_exporter | [12234](https://grafana.com/grafana/dashboards/12234-mysqld-exporter-overview/) | Prometheus Community |
|
||
|
||
## 关联笔记
|
||
|
||
- [[hhs/MySQL/03-索引与查询优化/16-慢查询日志分析]] — pt-query-digest 配合监控系统
|
||
- [[hhs/Redis/09-高级特性]] — Redis 监控指标的对比
|
||
- [[hhs/GORM/16-日志与调试]] — GORM 级别的慢查询追踪
|