第7篇 业务数据监控:SQL Exporter 与自定义业务指标

8 阅读19分钟

业务数据躺在关系型数据库里,Prometheus 拉不到它,中间必须有人做翻译。而这一层该不该独立部署、该不该在抓取时查库,决定了它是"监控"还是"事故源"。

前几篇已铺满监控体系的外围,但有一个共同点:描述的都是「系统跑得好不好」,而不是「业务现在是什么状态」。

真正的业务真相在数据库里:今天新增了多少订单、还有多少工单压着没人处理、账务对不平的笔数有几条、库存逼近零的商品有几个。而这类问题,都不在 node_exporter、JVM、Blackbox 的探针范围。

本篇讨论的就是这一层:如何把数据库里的业务数据,稳定、低成本地变成 Prometheus 指标。由于业务数据都有其独特性,本篇只做粗线条的方案性介绍,不追求面面俱到。


一、为什么必须有中间层

Prometheus 只认一种数据形态:HTTP 端点返回的文本指标

metric_name{label="value"} 数值

而数据库吐出来的是结果集(行 × 列)。两者的格式不兼容,中间必须有个 Exporter 翻译层。它只做三件事:

  • 按一定节奏向数据库发 SQL;
  • 把结果集的「行 → 标签」「列 → 数值」映射成 Prometheus 样本;
  • 在 /metrics 上以文本格式暴露,等 Prometheus 来抓。

因此采集链路变成:

数据库 ──SQL──▶ Exporter ──/metrics(文本)──▶ Prometheus ──▶ Grafana / Alertmanager

但需注意:该 Exporter 不是无状态的旁路组件。每一次采数,都要真实消耗数据库的连接、CPU 和 IO。


二、调研:现成的三方 Exporter 够不够用

结论:绝大多数场景不需要自己写 Exporter,三方方案已经很成熟。

方案数据库支持能否采集业务数据(自定义 SQL)说明
sql_exporter(burningalchemist / prometheus-community)MySQL、PostgreSQL、SQL Server、Oracle、ClickHouse、Snowflake、Vertica✅ 完全配置驱动,任意 SQL → 任意指标free/sql_exporter 的活跃继任者。collector 用 YAML 定义,无需写代码。业务数据监控的首选
mysqld_exporter / postgres_exporterMySQL / PostgreSQL⚠️ 以数据库自身指标为主(连接数、慢查询、缓存命中),不提供通用业务 SQL 能力解决的是"数据库监控",不是"业务数据监控",两者互补而非替代
oracledb_exporterOracle✅ 支持 TOML + SQL 定义自定义指标Oracle 环境可用;比 sql_exporter 更偏向 Oracle 专有视图
dameng_exporter达梦 DM8(信创)✅ 30+ 内置指标,并支持通过 SQL 定义自定义指标国产生态适配较好,配套 Grafana 面板
Prometheus 官方 mysqld/node 系列—❌ 不涉及与本篇主题无关,仅作对照

选型建议与自研边界

  • 优先直接落地 sql_exporter 这一类配置驱动的方案:新加一个业务指标 = 加一段 YAML + 一条 SQL,不用发版、不用重启业务。
  • 只有下面三种情况才值得自研:
    1. 要求抓取链路绝对不触发数据库查询(安全或性能红线);
    2. 需要在多个数据源之间做跨库聚合与计算,而不是单条 SQL 出结果;
    3. 取数来源不只是 SQL(还要读 HTTP 接口、文件、消息队列)后统一成一个指标。

三、Prometheus 四种数据类型速览

Prometheus 只有四种数据类型,选错了类型,后面的 PromQL 和告警全是歪的。

维度CounterGaugeHistogramSummary
值的方向只增(进程重启才归零)可增可减观测值按桶累计观测值按分位统计
语义累计总量瞬时快照分布(延迟/尺寸)分布(延迟/尺寸)
暴露的序列单条单条_bucket{le}、_sum、_count_sum、_count、φ 分位
典型 PromQLrate()、increase()直接读、delta()、deriv()histogram_quantile()、rate()直接读分位值
跨实例聚合✅ sum 有意义✅ sum / avg 有意义✅ 同桶可聚合❌ 分位数不可聚合
从 SQL 直接生成⚠️ 困难✅ 天然契合⚠️ 需 GROUP BY 手工造桶❌ 基本不可行
业务数据监控中的使用频率低~中高(主力)低极低
业务侧典型例子累计注册用户数待支付订单数、库存水位支付耗时分布单实例接口 P99

业务数据监控一般以 Gauge 为主,Counter 慎用,Histogram / Summary 基本留给应用侧。

类型在业务数据监控中的定位关键注意点
Gauge绝对主力。订单存量、待处理工单、金额、库存、对账差异笔数——业务数据绝大多数是"某一刻的快照",天然就是 Gauge用 delta() / deriv() 看变化趋势,用窗口值(近 1h 新增)表达"速率"
Counter谨慎使用坑点:从 SQL 查出来的"累计值"不等于 Counter。Counter 的语义是「由本进程累加、单调递增」,而库里的累计值可能被数据清理、归档、回滚而下降,rate() 会把下降误判为"重置",算出来的速率完全失真。只有在确定该值严格单调时才用 Counter,否则一律用 Gauge
Histogram少见Prometheus 的直方图是客户端分桶,而 SQL 侧拿不到分桶。若一定要做,只能在 SQL 里按耗时区间 GROUP BY 手工拼出 le 桶,成本高、收益低。延迟分布这类需求,应该在第4篇的应用侧埋点里解决
Summary基本不用分位数由客户端计算且不可跨实例聚合,与"从数据库取数"的模型天然冲突

四、可实施方案

1、总体架构与部署位置

                    ┌──────────── 每 1~5 分钟 ────────────┐
                    ▼                                    │
        [ Exporter 独立进程 ] ──SQL──▶ [ 只读从库 / 只读账号 ]
                    │
          内存缓存(最近一次采集结果)
                    │
        /metrics ──抓取 15~30s──▶ Prometheus

Exporter 放在哪里,有三种选择:

部署方式说明评价
❌ 内嵌进业务应用在业务进程里加定时任务查库并暴露 /metrics不推荐。监控代码与业务共享线程池、连接池和 CPU;SQL 慢查询、内存泄漏会直接拖垮业务;监控的本意是保障稳定性,结果成了不稳定源
✅ 独立 Exporter 进程(旁挂)单独部署一个 exporter,用独立连接与独立账号推荐。故障域隔离:Exporter 挂掉只丢指标,不影响业务
✅ 独立 Exporter + 只读从库同上,且 SQL 走从库/只读实例最优。把采数压力从主库彻底剥离,避免影响在线事务

2、四条硬性原则

  • 业务数据指标不集成进应用。监控采集是"额外负载",绝不能和业务抢资源、共用失败边界。要独立进程、独立账号、独立连接池。
  • 不在 Prometheus 抓取时同步查库。抓取间隔通常是 15~30 秒,如果每次抓取都跑一遍 SQL,等于给数据库挂了一个 15 秒一次的压力源。正确做法是后台定时采集 + 内存缓存,/metrics 只读缓存、秒回。
  • 查询走只读从库、只读账号、带超时。最小权限 SELECT,禁止 DDL/DML;SQL 必须设执行超时。
  • 标签基数必须受控。把 user_id、order_id 放进标签,时间序列数量会随数据量线性膨胀,几小时内就能吃光 Prometheus 的内存。

3、取数时机:三种方式对比

注意:「什么时候查库」比「查什么」更重要。

方式查询时机优点缺点适用
sql_exporter + min_interval抓取时触发,但受最小间隔限制,间隔内返回缓存零开发、配置即用首次抓取仍是同步执行;对 DB 的节奏仍由抓取驱动绝大多数场景的默认选择
自研 Exporter(后台采集 + 内存缓存)后台 goroutine / 定时器驱动,与抓取完全解耦抓取链路零查库;可跨源聚合、可自定义计算需要开发与维护抓取必须零查库、多数据源聚合、复杂计算
定时任务 + Pushgateway定时任务采集后主动 push无需常驻进程无 up 语义,任务挂掉后指标停留在旧值而不自知,易产生"陈旧数据当正常"的误判离线批处理类指标,且必须配套新鲜度告警

关于第一种,sql_exporter 的默认行为需要特别说明:

min_interval 默认为 0s,表示每次 /metrics 被访问都会执行全部查询。要避免"抓取即查库",必须显式设置 min_interval(如 1m),让高频抓取命中缓存。

4、落地方案(以 sql_exporter 为例)

第一步:数据库侧准备只读账号

CREATE USER 'monitor'@'%' IDENTIFIED BY '***';
GRANT SELECT ON order_db.* TO 'monitor'@'%';
-- 限制并发连接,避免监控账号挤占业务连接
ALTER USER 'monitor'@'%' WITH MAX_USER_CONNECTIONS 5;

第二步:Exporter 主配置

1)主配置字段说明(0.24)

global 段

字段默认值说明
scrape_timeout10s单次抓取总超时。实际生效值 = min(scrape_timeout, 请求头 X-Prometheus-Scrape-Timeout-Seconds − scrape_timeout_offset);≤0 表示不设
scrape_timeout_offset500ms从 Prometheus 的超时里扣掉的余量,保证是 exporter 先返回而不是 Prometheus 先超时。必须为正数
min_interval0s最关键的一项:collector 结果的缓存 TTL。0s 表示每次抓取都执行全部查询,线上必须改
warmup_delay0s启动后首次填充缓存时,各 collector 之间错开的延迟,避免同时压库(惊群)
max_connections3到单个 target 的最大连接数(查询会并发跑在多个连接上)
max_idle_connections3最大空闲连接数,通常与 max_connections 相同
max_connection_lifetime0(无限)连接最长复用时间
scrape_error_drop_interval0s多久清一次累计的 scrape_errors_total,0 表示不清
enable_query_metricsfalse开启后额外暴露 per-query 的 query_duration_seconds / query_rows_returned,做自监控强烈建议打开

target / jobs 段(二选一)

字段位置说明
nametarget可选。设置后额外暴露带 target 标签的 up / scrape_duration_seconds,且始终返回 HTTP 200
data_source_nametargetURL 格式 DSN;特殊字符需 URL 编码
collectorstarget / jobs[]在该目标上执行的 collector 名,支持 glob(如 biz_*)
enable_pingtarget / jobs[]采集前是否 ping,默认 true
job_namejobs[]任务名,会作为 job 标签
static_configs[].targetsjobs[]键值对:实例名: DSN,键会成为 instance 标签
static_configs[].labelsjobs[]该组所有目标的附加标签(env、svc 等)
collector_files顶层collector 文件 glob,一个文件一个 collector
2)配置示例
# sql_exporter.yml
global:
  scrape_timeout: 10s              # 单次抓取上限;<=0 表示不设
  scrape_timeout_offset: 500ms     # 从 Prometheus 超时里预留的余量,必须为正
  min_interval: 1m                 # 【必改】collector 结果缓存 TTL。0s = 每次抓取都查库
  warmup_delay: 200ms              # 启动预热时各 collector 错开的间隔,防惊群
  max_connections: 3               # 到单个 target 的最大连接数
  max_idle_connections: 3          # 通常与 max_connections 一致
  max_connection_lifetime: 30m     # 0 表示无限复用
  scrape_error_drop_interval: 1h   # 0 表示 scrape_errors_total 只增不清
  enable_query_metrics: true       # 暴露 per-query 耗时/行数,用于自监控

# ── 单目标模式:与 jobs 二选一 ───────────────────────────────
target:
  name: order_db                                    # 可选;设置后会自带 up / scrape_duration_seconds
  data_source_name: 'mysql://monitor:p%40ssw0rd@order-db-read:3306/order_db'
  collectors: [biz_order]                           # 支持 glob,如 biz_*
  enable_ping: true                                 # pgbouncer / 数仓 建议 false

# ── 多目标模式:一个 exporter 监控多个库,与 target 二选一 ────
# jobs:
#   - job_name: biz_order
#     collectors: [biz_order]
#     enable_ping: true
#     static_configs:
#       - targets:
#           # 左侧是 instance 名(会作为 instance 标签),右侧是 DSN
#           'order-db-read:3306': 'mysql://monitor:pwd@order-db-read:3306/order_db'
#         labels:
#           env: prod
#           svc: order
#   - job_name: biz_payment
#     collectors: [biz_payment]
#     static_configs:
#       - targets:
#           'pay-db-read:5432': 'postgresql://monitor:pwd@pay-db-read:5432/pay_db?sslmode=disable'
#         labels:
#           env: prod
#           svc: payment

# collector 定义文件:一个文件一个 collector
collector_files:
  - "/etc/sql_exporter/collectors/*.collector.yml"

关于 0.24 的两个细节:

  • 启动预热:进程启动后缓存为空,会先把所有 collector 跑一遍(warmup)。预热期间 /metrics 请求会被阻塞直到预热完成,warmup_delay 就是用来错开这一轮采集、避免瞬间把所有查询压到数据库上的。
  • min_interval 同样作用于启动后的第一次采集,因此它不只是"缓存 TTL",同时也是控制单次压库节奏的手段。

第三步:指标定义(collector)

# biz_order.collector.yml
collector_name: biz_order
metrics:
  # 用于判断数据是否能正常查询
  # 网上有文章介绍有 last_success_timestamp_seconds 指标,但在0.24版本里并没有,需自建
  - metric_name: last_success_timestamp_seconds
    type: gauge
    help: '该查询最近一次真实执行成功的时刻(Unix 秒)'
    values: [ts]
    query: |
      SELECT UNIX_TIMESTAMP() AS ts
  - metric_name: biz_order_total
    type: gauge                 # 业务数据统一用 gauge
    help: '各状态订单存量(近 7 天)'
    key_labels: [status]        # 结果集的 status 列 → 标签
    values: [cnt]               # 结果集的 cnt 列 → 数值
    query: |
      SELECT status, COUNT(*) AS cnt
      FROM t_order
      WHERE create_time >= NOW() - INTERVAL 7 DAY
      GROUP BY status;

一行结果 status=pending, cnt=1820 会被翻译成:

biz_order_total{status="pending",job="biz_order",instance="order-db:3306",env="prod",svc="order"} 1820

第四步:启动 sql_exporter 服务

1)Docker 启动服务
docker run -d --name sql_exporter \
  --restart=no \
  --network my-bridge \
  -p 9399:9399 \
  -v /data/volumes/sql_exporter:/etc/sql_exporter:ro \
  burningalchemist/sql_exporter:0.24 \
    --config.file=/etc/sql_exporter/sql_exporter.yml \
    --web.enable-reload

坑点:collector_files 是相对于配置文件所在目录的 glob。容器内必须把 sql_exporter.yml 与所有 *.collector.yml 放在同一目录整体挂载,否则 collector 会静默加载不到,不报错,只是"没有指标"。

2)命令行参数速查(0.24)
参数默认值说明
--config.filesql_exporter.yml主配置文件路径。也可用环境变量 SQLEXPORTER_CONFIG 覆盖
--config.checkfalse只校验配置并退出。变更前必跑
--config.data-source-name空用命令行覆盖配置里的 DSN,多环境复用同一份配置文件
--config.enable-pingtrue采集前是否 ping 数据库。pgbouncer / 按需付费数仓要设 false
--config.target-labeltarget多目标模式下标识目标的标签名
--config.ignore-missing-valuesfalse[实验] 忽略结果集中缺失列的行
--web.listen-address:9399监听地址
--web.metrics-path/metrics业务指标暴露路径
--web.config.file空TLS / BasicAuth / 限流配置文件(exporter-toolkit 格式)
--web.enable-reloadfalse开启 /reload 端点。默认关闭,不开启时用 SIGHUP 重载
--log.levelinfo日志级别
--log.formatlogfmt日志格式
--log.file空(stderr)日志文件路径
--versionfalse打印版本并退出

第五步:接入 Prometheus

- job_name: 'sql_exporter'
  scrape_interval: 30s
  honor_labels: true        # 关键:保留 exporter 打的 instance/job,不被抓取侧覆盖
  static_configs:
    - targets: ['sql_exporter:9399']

第六步:验证

$ curl -s localhost:9399/metrics | grep biz_order_total
biz_order_total{env="prod",instance="order-db:3306",job="biz_order",status="pending",svc="order"} 1820
biz_order_total{env="prod",instance="order-db:3306",job="biz_order",status="paid",svc="order"} 9642

注意:当数据库不可达时,sql_exporter 的 /metrics 会返回 HTTP 500,Prometheus 侧会记为 up=0。

5、数据库侧风险控制

监控引入的每一次查询,都是对生产数据库的一次访问。

风险后果控制措施
连接数占用监控账号与业务争抢连接池,连接打满后业务不可用单实例连接数固定(max_connections: 3);用独立只读账号并限制 MAX_USER_CONNECTIONS;复用长连接而非每次新建
大表全量扫描千万级大表上 COUNT(*) 直接把 DB CPU 打到 100%,从库延迟飙升查询必须先 EXPLAIN 验证走索引;禁止 SELECT *;为筛选/分组字段建索引;尽量用时间窗口限定范围
查询频率过高15 秒一次抓取 = 15 秒一次全量 SQL,DB 承受持续脉冲压力设置 min_interval: 1m~5m;业务数据变化以分钟计,秒级刷新没有意义
结果集基数爆炸时间序列数量失控,Prometheus 内存/磁盘耗尽标签基数 ≤ 10³;绝不把 user_id / order_id 放进标签;SQL 内先 GROUP BY 收敛维度再返回
查询无超时慢查询堆积,Exporter 卡死、抓取超时、级联告警SQL 侧设 statement_timeout / max_execution_time;配置 scrape_timeout_offset 留余量
数据陈旧却无人知面板显示的是 1 小时前的数据,被误判为"业务正常"必须暴露 last_success_timestamp_seconds,并对"数据过期"单独告警。但默认没有该指标,需参考前文自建。
主库被反复打扰影响在线事务,引发用户可感知的抖动走只读从库 / 只读实例;确需读主库时,把查询降到最低频率并选在低峰执行

一句话总结风险控制原则:把监控当成一个不受信任的、需要限流的外部客户端来对待。


五、一个业务数据监控会产生哪些指标

很多人以为"业务数据监控"就是那几条业务指标,实际上一个可用的落地需要三层指标同时到位。少了后两层,业务数据的可信度无从判断。

层指标类型作用
① 业务数据biz_order_pending_countGauge待处理订单数——核心业务水位
​biz_order_total{status}Gauge各状态订单存量
​biz_order_amount_yuan{channel}Gauge各渠道订单金额
​biz_order_timeout_countGauge超时未支付订单数
​biz_order_created_1hGauge近 1 小时新增(窗口值,表达"速率")
② 采集健康度up{job="sql_exporter"}Gauge抓取链路是否通(Prometheus 自动生成)
​..._query_duration_seconds{query}Gauge单条 SQL 执行耗时(enable_query_metrics 开启后由 exporter 暴露)
​..._query_rows_returned{query}Gauge单条 SQL 返回行数,异常跳变往往先于业务异常
​last_success_timestamp_seconds{query}Gauge自建指标,最后一次成功采集的时间戳。数据新鲜度,最该配的一条告警。
③ 数据库与连接..._db_connections_open / in_use / idleGaugeExporter 到 DB 的连接池水位
​biz_query_errors_total{reason}CounterSQL 报错累计(权限、超时、语法、连接拒绝)
​数据库自身指标(连接数、慢查询、从库延迟)—由 mysqld_exporter / 内置视图提供,与本篇互补

注意:

  • last_success_timestamp_seconds:用 time() - biz_last_success_timestamp_seconds > 600 直接告警"数据已过期",是防止"看着旧数据做决策"的唯一手段。但需注意,数据库和 Prometheus 之间要有 NTP 同步,否则新鲜度会带着固定偏移。
  • query_rows_returned:返回行数突然从 20 涨到 2000,通常意味着数据异常或维度失控,比业务阈值更早报警。

第③ 层数据库与连接,默认无该层指标,需通过编写自定义的 collector 配置文件来实现:

  • 连接池水位指标 (db_connections_open / in_use / idle):对于 MySQL,可以查询 information_schema.PROCESSLIST 或 performance_schema 来获取当前连接数、活跃连接数等;对于 PostgreSQL,可以查询 pg_stat_activity 视图,如 SELECT COUNT(*) as connections FROM pg_stat_activity 。
  • 业务错误计数器 ( biz_query_errors_total{reason}):需要应用程序或数据库中间件在发生特定错误时进行记录。sql_exporter 无法自动感知业务层面的错误。如在数据库中创建一个用于记录错误日志的表。

六、一个 Exporter 配多少条 SQL

这是直接影响稳定性上限的问题。

核心约束:所有 SQL 都要在一次采集周期内跑完。

维度建议值理由超限怎么办
单 Exporter 的 SQL 条数10~20 条为宜,上限 30 条条数越多,单次采集耗时与 DB 压力线性增长按业务域拆分多个 Exporter 实例(订单 / 支付 / 库存各一个),各自独立端口与 job
单条 SQL 执行时间P95 < 1s采集耗时需远小于采集间隔加索引、缩小时间窗口、改走从库;仍慢则改为预聚合中间表
单次采集总耗时< min_interval 的 1/3避免"上一次还没跑完,下一次又来了"拆分实例;不同 Exporter 错峰设置 min_interval
单条 SQL 返回行数≤ 1000 行行数 × 标签组合 = 时间序列数SQL 内先 GROUP BY 收敛维度,只返回真正需要的分类
单指标的标签基数≤ 10³,多标签乘积 ≤ 10⁴时间序列爆炸的临界点剔除 id 类标签;或改为"多个指标"而不是"一个指标多个标签"
单实例连接数3~5控制对 DB 的连接占用提高前先核算数据库剩余连接额度
采集频率 min_interval1~5 分钟业务数据变化以分钟计与 SQL 耗时、DB 负载联动调整,不做低于 30s 的采集

拆分阈值(任一条命中就该拆)

  • SQL 条数 > 30 条;
  • 单次采集总耗时 > 5s;
  • 单个 Exporter 产生的总时间序列数 > 5 万;
  • 涉及两个以上互不相关的业务域。

拆分的成本很低——多跑一个容器、多加一个 job;而不拆的代价是:一条慢查询拖慢整个 Exporter 的采集,所有业务指标一起失真。


七、与应用内埋点的边界

维度应用内埋点(第4篇)数据库侧采集(本篇)
数据来源代码里主动打点从库里查出来的既成事实
典型指标接口耗时、业务动作次数、异常计数订单存量、待处理量、对账差异、库存水位
是否改代码需要不需要(只加配置)
精度高(事件级)低(快照级,受采集间隔限制)
对业务的影响同在进程内,需谨慎完全隔离

互补关系:应用埋点回答"这个动作发生了多少次、花了多久",数据库采集回答"现在还剩多少、积压了多少"。存量类、外部系统写库类、历史遗留系统类指标,都只能靠本篇的方式拿到。


八、小结

  • 需要中间层,但不必造轮子。 业务数据 → Prometheus 必须有一个 Exporter 做翻译;优先选 sql_exporter 这类配置驱动方案,配置即指标,改指标不用发版。
  • 独立部署是底线。 业务数据指标不要集成进业务应用,共享资源、共享故障域,监控反而会拖垮被监控的对象。独立进程 + 独立只读账号 + 天然隔离。
  • 抓取时不查库。 Prometheus 的抓取节奏(15~30s)不适合驱动数据库查询。要么给 sql_exporter 配好 min_interval 走缓存,要么自研带后台采集与内存缓存的 Exporter。
  • 业务数据几乎都用 Gauge。 库里查出的"累计值"不满足 Counter 的单调语义,rate() 会算错;Histogram / Summary 是客户端能力,留给应用侧埋点。
  • 风险控制先于指标设计。 连接数、查询性能、结果集基数、查询超时、数据新鲜度,这五件事在写第一条 SQL 之前就应定好规矩。监控的价值是"早知道",但前提是它自己没有变成新的故障源。

​