数据库集群自动化巡检与推送报告系统 —— 项目记录
脱敏声明:本文为技术复盘,文中所有 IP、主机名、域名、账号、邮箱、数据源 UID、大盘 UID、
集群业务名称、部署路径均已替换为占位符(如db-mysql-01、10.20.x.y、grafana.example.com、业务集群A)。PromQL、指标名、判据、代码结构均保持原样,可直接参考。
不涉及任何真实个人或组织信息。
目录
- 为什么做这件事
- 两条并行的战线
- 巡检覆盖范围
- 整体设计
- 指标口径定义
- 核心代码
- 效果展示
- 踩过的坑与解决方式
- 工程经验总结
- 项目产出清单
1. 为什么做这件事
1.1 从「人去查」到「系统告诉人」
数据库集群的容量与健康看板早就有了 —— Prometheus + Grafana,指标齐全。
但现实是:没人会每天早上主动把 N 个大盘翻一遍。
- 大盘有几十个面板,真正要看的只有 4 个指标(CPU、内存、磁盘 IO、磁盘使用率)
- 4 个指标分散在不同的大盘上,还要手动切主机变量
- 「配置了告警」不等于「看懂了告警」—— 阈值告警只告诉你"某个指标越线了",
而值班的人想知道的是"现在整个集群什么状况、哪几台有问题、问题有多严重" - 周期性容量趋势(哪个盘在涨、涨多快)没人做
所以我们做了一个推送到邮箱 + 企微群的巡检报告:不等人来看,每天定时把结论送到眼前。
1.2 原有的巡检脚本有哪些问题
在改造之前,公司内部已经有一套脚本在跑,但暴露出几类典型问题:
| 问题类别 | 具体表现 |
|---|---|
| 口径不统一 | 5 个数据库类型 5 套采集脚本,同一个"内存使用率"有 2 种算法,"磁盘 IO"有 3 种聚合方式,彼此差 1~2 个百分点 |
| 指标被平均掩盖 | 磁盘 IO 把主机上所有盘平均,一块盘跑到 99% 而另一块空闲 → 报出来只有 49%,直接漏报 |
| 采集漏项 | MongoDB 脚本"只要有非 sda 的盘就无条件忽略 sda",某些主机真正忙的恰恰是 sda,一直报 0.05% |
| 物理不可能的值 | irate对外推/多路径设备算出 105.45% 这种 >100% 的磁盘使用率,直接进邮件,没人发现 |
| 峰值没有上下文 | 只报时段峰值,不报持续多久。"尖峰 2 分钟"和"持续 203 分钟"在报表里长得一模一样 |
| 可解释性差 | 表格列名写「磁盘IO使用率峰值」,但实际是"最忙那一分钟的盘间平均",读邮件的人理解不了 |
| 运维性缺失 | 推送脚本退出码永远是 0、没有数据新鲜度校验、SMTP 无超时(卡住就一直挂)、日志无限增长 |
1.3 目标
- 把「数据口径」对齐,并在报表里写清楚每一列是怎么算的
- 把「被平均掩盖的问题」暴露出来
- 让报表能直接用 —— 值班的人看一眼就知道该处理哪几台
- 采集与推送解耦、可测试、可回滚
- 不改动正在运行的老脚本,新老并行、可随时退回
2. 两条并行的战线
项目实际覆盖了两套环境,需求差异不小:
| 线下(内网离线环境) | 线上(生产环境) | |
|---|---|---|
| 主机数 | 60 台 | 40 台 |
| 数据库 | MySQL / StarRocks / Greenplum / MongoDB / Oracle / Redis | MySQL / StarRocks / Greenplum / MongoDB / Oracle |
| 数据源 | 自建 Grafana(三套 Prometheus 联邦) | 生产 Grafana(默认数据源 + 容器指标源) |
| 采集方式 | Grafana 为主,SSH + sar/iostat 兜底(部分主机没有监控探针) | 纯 Grafana |
| 推送 | 邮件 + 企业微信群 | 邮件 + 企业微信群 |
| 调度 | cron 每天 2 次 | cron 每天 2 次 |
两条线的指标口径、判定逻辑、报告结构、推送模块是共用一套设计的,
只有"取数"这一层不同 —— 这也是后来能把线上 6 个脚本合并成 1 个的基础。
3. 巡检覆盖范围
3.1 主机与集群
| 数据库类型 | 集群数 | 主机数 | 备注 |
|---|---|---|---|
| MySQL | 6 个业务集群 | 18 台 | 主从架构,含域名主备切换检测 |
| StarRocks | 1 | 6 台 | 数仓 |
| Greenplum | 1 | 6 台 | 数仓 |
| MongoDB | 2 | 6 台 | 副本集 |
| Oracle | 2 | 4 台 | 含表空间、备库延迟采集 |
| Redis | — | 4 台 | 仅线下 |
3.2 采集指标
| 指标 | 含义 | 单位 |
|---|---|---|
| 主机可达性 / 数据库连通性 | 有没有数据 / 端口通不通 | 可达 / 不可达 / 未探测 |
| CPU 使用率峰值 | 主机整体 CPU(或容器 CPU 配额使用率,视集群而定) | % |
| 内存使用率峰值 | 按MemAvailable计算,不含可回收缓存 | % |
| 磁盘 IO 使用率峰值 | 每块盘"忙的时间占比",取窗口内峰值后按口径聚合 | % |
| 磁盘使用率峰值 | 该主机最满的挂载点 | % |
| 综合状态 | 上述指标的最差状态 | 严重 / 告警 / 正常 |
3.3 时间窗口
采用连续窗口模式:上次运行时刻 → 本次运行时刻。
老版本用固定班次(上午 01:30–09:30 / 下午 09:30–17:30),
结果17:30 到次日 01:30 这 8 小时是盲区—— 夜间出问题第二天早上才知道。
改成连续窗口后盲区消除,代价是要维护一个"上次成功运行时刻"的状态文件,
并处理"状态文件丢失 / 长时间没跑"的兜底(限制最长回溯 24 小时)。
4. 整体设计
4.1 数据流
┌─────────────────────────────────┐ │ Prometheus / Grafana 数据源 │ │ (默认源 / 容器指标源 / 多联邦) │ └───────────────┬─────────────────┘ │ Datasource Proxy API │ GET /api/datasources/proxy/uid/<uid>/api/v1/query_range ▼ ┌────────────────────────────────────────────────────────────┐ │ 采集层(Collector) │ │ · 按集群生成 PromQL(instance/ip 正则、设备正则) │ │ · 批量查询 + 按主机聚合 │ │ · 拿不到就降级到 SSH(sar / iostat / free / top) │ └───────────────────────────┬────────────────────────────────┘ │ 结构化数据 ▼ ┌────────────────────────────────────────────────────────────┐ │ 判定层(Judge) │ │ · 峰值 vs 阈值(警告 / 严重) │ │ · 缺数据 → 「无数据」而非「严重」 │ │ · 综合状态 = 各项最差 │ └───────────────────────────┬────────────────────────────────┘ │ ┌──────────────┴──────────────┐ ▼ ▼ ┌──────────────────────┐ ┌──────────────────────┐ │ 渲染层(Render) │ │ 推送层(Push) │ │ · HTML 邮件 │ │ · SMTP_SSL 发信 │ │ · 企微 markdown │ │ · 群机器人 webhook │ └──────────────────────┘ └──────────────────────┘4.2 采集层:Prometheus 优先,SSH 兜底
不是所有主机都装了 node_exporter。所以采集层做了优先级降级:
Grafana 有数据 → 用 Grafana(快、准、带标签) Grafana 没数据 → SSH 上去跑 sar/iostat/free/top(慢,但不会漏) 两边都没有 → 标记「未探测」,不算「不可达」,避免误报SSH 兜底的关键工程点是一次连接拿完所有数据:把远程要执行的命令拼成一个脚本,
用@@@分隔各段输出,一次 SSH 会话全部取回。60 台主机 10 并发,整体几十秒。
4.3 判定层:缺数据不等于故障
老脚本的judge_status(None)直接返回「严重」,导致监控探针没装的主机天天报严重。
改成三态:
有值且越线 → 严重 / 告警 有值未越线 → 正常 压根没取到值 → 无数据(独立状态,不参与"最差"计算,也不会让整机变红)同时在报告顶部显示数据质量:完整 N 台 / 部分缺失 N 台 / 无数据 N 台(缺失 x%)。
这样"报表说没问题"和"报表没看到"能被区分开。
5. 指标口径定义
这一节是整个项目最花时间的部分。同一句话「CPU 使用率」在不同集群里可能是两个完全不同的东西。
5.1 CPU
| 场景 | 口径 | 含义 |
|---|---|---|
| 容器化 MySQL | 容器实际用量 / 容器 CPU limit × 100 | 容器 CPU 配额使用率—— 反映业务 pod 用了多少配额 |
| 物理机 / 虚拟机 | 100 × (1 − avg(irate(node_cpu_seconds_total{mode="idle"}[5m]))) | 主机 CPU 使用率—— 反映整机所有核的平均繁忙度 |
⚠️ 这两者量级完全不同(一个是"用了配额的百分之几",一个是"整机有多忙"),
放在同一张表里必须把列名区分开,否则读表的人一定会误判。
5.2 内存
内存使用率 = (1 − MemAvailable / MemTotal) × 100为什么不用MemTotal − MemFree − Buffers − Cached?
MemAvailable是内核给出的"在不触发 swap 的前提下还能被新应用用掉多少内存"的估算值,
它已经把可回收的 page cache / slab算作可用。而Free + Buffers + Cached是手工拼的,
口径随内核版本和具体场景漂移,通常明显高估内存压力。
实测同一台机器两种算法差30~45 个百分点(一个报 72%,一个报 42%)。
统一成MemAvailable后,内存告警的准确性提升非常明显。
5.3 磁盘 IO 使用率
irate(node_disk_io_time_seconds_total{..., device=~"sd[a-z]"}[5m]) * 100node_disk_io_time_seconds_total是"这块盘累计有多少秒在处理 IO",
它的一阶导数就是该盘每秒里有多大比例的时间在忙。×100 就是百分比。
100% = 这块盘一直在处理 IO。
要注意它的三个特性:
- 反映"忙不忙",不反映吞吐和延迟。一块慢机械盘在很低 IOPS 下也能 100% 忙;
一块 NVMe 100% 忙时吞吐可能很高。所以它适合发现"盘被打满了",
不适合判断"性能好不好"。 - 必须逐盘算,不能先聚合(见 8.2)。
irate需要区间内至少 2 个采样点,采集间隔必须小于区间长度(见 8.1)。
5.4 磁盘使用率
max by(instance) ((1 - ( node_filesystem_avail_bytes{fstype=~"xfs|ext4"} / node_filesystem_size_bytes{fstype=~"xfs|ext4"} )) * 100)取该主机最满的挂载点。fstype过滤掉tmpfs/devtmpfs这类假文件系统。
某些集群额外加了挂载点白名单(mountpoint=~"/log|/|/data*"),避免/etc/hosts、/usr/share/zoneinfo这类固定大小、天然 100% 满的只读挂载点污染结果 ——
这是踩过坑才知道的(见 8.10)。
6. 核心代码
6.1 查询 Grafana 数据源
classGrafana(object):def__init__(self,base_url,username,password,logger)