KaiwuDB 征文AI 创新

把 KWDB SampleDB 数据接到 Prometheus/Grafana:监控全家桶怎么串

原创cherry白2026-10-19
4

开始写这篇文章之前,我一直在想拿什么场景来讲 KWDB 的监控接入。纯讲 API、讲配置也能写,但读者看完大概只会记住几个路径,最终我还是选定了园区物业的能耗管理场景,原因很简单:有真实的业务数据(电表、区域、功率、能耗),也有真实的"看不出来"的时刻——数据都在库里,可一旦大屏上的曲线不对劲,光靠数据库自身的指标根本说不清问题出在哪。

想象下某个场景,园区物业的能耗大屏上,某天总功率曲线突然抬高。值班同事第一反应是去看 KWDB 节点——CPU、内存、SQL 延迟,全都平稳得不像有事。那就只能确认一件事:电量确实变高了。但到底是哪片区域、哪批电表先出的偏差?变化是出在上报、聚合,还是查询链路?没人说得清。

其实 SampleDB 里这些数据都有。tsdb.meter_data 存着电压、电流、功率和能耗,rdb.meter_info、rdb.area_info 描述电表和区域。缺的不是数据,而是一条把业务数据变成监控指标的链路——数据躺在库里,监控看不见,等于没有。这次接入最后拆成了两条路径:KWDB 自身的指标走官方 /_status/vars,SampleDB 的业务指标则由一个小 Python Exporter 查询聚合后暴露成 /metrics。Prometheus 负责采集和告警,Grafana 把两类指标摆在一起看。说白了,就是让"库里的数据"和"监控的眼睛"接上线。

先把两类监控指标分开

 

1. 场景和数据量

 

为了让演示不依赖 SampleDB 自带的 10,100 条时序样例数据,测试环境将同一套表结构扩展到一个月的园区数据:

 

 

数据对象规模
电表300 个
区域100 个
采样间隔5 分钟
时间范围2026-07-01 00:00 至 2026-07-31 23:55
时序记录约 267.8 万条
监控事件2026-07-15 14:20 功率异常


SampleDB 原始表结构不需要改动。关系表仍然是:

rdb.meter_info
rdb.user_info
rdb.area_info
rdb.alarm_rules


时序表仍然是:

tsdb.meter_data(
    ts,
    voltage,
    current,
    power,
    energy,
    meter_id
)

2. 扩展一批带日期的数据

 

SampleDB 仓库提供了 generate_series 的插入示例。这里把时间固定到 2026 年 7 月,方便演示告警时刻和回放查询:

 

INSERT INTO tsdb.meter_data
    (ts, voltage, current, power, energy, meter_id)
SELECT
    TIMESTAMPTZ '2026-07-01 00:00:00+00:00'
        + s * INTERVAL '5 minutes',
    220.0 + ((m + s) % 10)::FLOAT8,
    5.0 + ((m + s) % 15)::FLOAT8 * 0.1,
    1000.0 + ((m + s) % 20)::FLOAT8 * 50,
    5000.0 + s::FLOAT8 * 10,
    'M' || m::TEXT
FROM generate_series(0, 8927) AS s
CROSS JOIN generate_series(1, 300) AS m;


写入后先确认行数和时间范围:

SELECT COUNT(*) AS rows,
       MIN(ts) AS first_ts,
       MAX(ts) AS last_ts
FROM tsdb.meter_data
WHERE ts >= TIMESTAMPTZ '2026-07-01 08:00:00+00:00'
  AND ts <  TIMESTAMPTZ '2026-08-01 09:00:00+00:00';
  

3. 两条监控路径

 

KWDB 集群指标使用官方配置:

 

KWDB node:8080/8081/8082
       |
       | GET /_status/vars
       v
Prometheus job=kaiwudb
       |
       v
官方 Grafana Dashboard


SampleDB 业务指标单独走 Exporter:

tsdb.meter_data + rdb.meter_info + rdb.area_info
       |
       | SQL 聚合
       v
Python Exporter :9105/metrics
       |
       v
Prometheus job=sampledb-exporter
       |
       v
业务能耗 Dashboard


这一步不能省略。/_status/vars 暴露的是 KWDB 节点指标,不会自动把 meter_data.power 变成 Prometheus 指标。

先写一个只导出聚合数据的 Exporter

 

1. 指标不要按电表无限展开

 

如果把 meter_id 作为 Prometheus 标签,每个电表都会产生一组新的时间序列。电表数量一大,Prometheus 的基数和存储压力会跟着上升。

 

这次只暴露区域级指标:

 

sampledb_area_power_watts{region,area}
sampledb_meter_data_lag_seconds{region,area}
sampledb_meter_status_total{status}
sampledb_exporter_scrape_duration_seconds


需要查看某个电表的原始点位时,仍然回到 KWDB 查询,不把明细全部搬进 Prometheus。

2. 安装依赖

 

Psycopg2 按 KWDB 的 Python 连接示例安装,Exporter 指标使用 Prometheus 官方 Python 客户端:

 

python3.10 -m venv .venv
source .venv/bin/activate
pip install psycopg2-binary prometheus-client


连接密码放在环境变量里:

export KWDB_HOST=127.0.0.1
export KWDB_PORT=26257
export KWDB_USER=monitor_reader
export KWDB_PASSWORD='change-me'
export KWDB_DATABASE=defaultdb
export EXPORTER_PORT=9105

3. Exporter 代码

 

下面的代码使用 5 分钟窗口聚合区域功率,并计算每个区域最后一条数据的延迟。MONITOR_NOW 可用于回放 2026 年 7 月 15 日的场景;不设置时使用当前时间。

 

import os
import time
from datetime import datetime, timezone

import psycopg2
from prometheus_client import Gauge, Histogram, start_http_server


AREA_SQL = """
WITH power_5m AS (
    SELECT
        mi.area_id,
        SUM(md.power)::FLOAT8 AS total_power
    FROM tsdb.meter_data AS md
    JOIN rdb.meter_info AS mi
      ON md.meter_id = mi.meter_id
    WHERE md.ts >= %s::TIMESTAMPTZ - INTERVAL '5 minutes'
    GROUP BY mi.area_id
),
last_seen AS (
    SELECT
        mi.area_id,
        MAX(md.ts) AS last_ts
    FROM tsdb.meter_data AS md
    JOIN rdb.meter_info AS mi
      ON md.meter_id = mi.meter_id
    WHERE md.ts >= %s::TIMESTAMPTZ - INTERVAL '24 hours'
    GROUP BY mi.area_id
)
SELECT
    a.region,
    a.area_name,
    COALESCE(p.total_power, 0),
    s.last_ts
FROM rdb.area_info AS a
LEFT JOIN power_5m AS p
  ON p.area_id = a.area_id
LEFT JOIN last_seen AS s
  ON s.area_id = a.area_id
"""

STATUS_SQL = """
SELECT status, COUNT(*)
FROM rdb.meter_info
GROUP BY status
"""

area_power = Gauge(
    "sampledb_area_power_watts",
    "Aggregated power of an area",
    ["region", "area"],
)
data_lag = Gauge(
    "sampledb_meter_data_lag_seconds",
    "Seconds since the latest meter sample in an area",
    ["region", "area"],
)
meter_status = Gauge(
    "sampledb_meter_status_total",
    "Number of meters by status",
    ["status"],
)
scrape_duration = Histogram(
    "sampledb_exporter_scrape_duration_seconds",
    "Time spent querying KWDB",
)
scrape_failures = Gauge(
    "sampledb_exporter_last_scrape_success",
    "Whether the last SampleDB scrape succeeded",
)


def monitor_now():
    value = os.getenv("MONITOR_NOW")
    if value:
        return datetime.fromisoformat(value).astimezone(timezone.utc)
    return datetime.now(timezone.utc)


def collect():
    started = time.perf_counter()
    conn = None
    try:
        conn = psycopg2.connect(
            host=os.getenv("KWDB_HOST", "127.0.0.1"),
            port=int(os.getenv("KWDB_PORT", "26257")),
            user=os.getenv("KWDB_USER", "monitor_reader"),
            password=os.environ["KWDB_PASSWORD"],
            dbname=os.getenv("KWDB_DATABASE", "defaultdb"),
            connect_timeout=3,
            options="-c statement_timeout=4000",
        )
        at = monitor_now()
        with conn.cursor() as cur:
            cur.execute(AREA_SQL, (at, at))
            for region, area, power, last_ts in cur.fetchall():
                area_power.labels(region, area).set(float(power or 0))
                if last_ts is None:
                    lag = 86400.0
                else:
                    if last_ts.tzinfo is None:
                        last_ts = last_ts.replace(tzinfo=timezone.utc)
                    lag = max(0.0, (at - last_ts).total_seconds())
                data_lag.labels(region, area).set(lag)

            cur.execute(STATUS_SQL)
            for status, count in cur.fetchall():
                meter_status.labels(status).set(int(count))

        scrape_failures.set(1)
    except Exception:
        scrape_failures.set(0)
        raise
    finally:
        if conn is not None:
            conn.close()
        scrape_duration.observe(time.perf_counter() - started)


if __name__ == "__main__":
    start_http_server(int(os.getenv("EXPORTER_PORT", "9105")))
    while True:
        try:
            collect()
        except Exception as exc:
            print(f"sampledb scrape failed: {exc}", flush=True)
        time.sleep(30)
        

代码里有几个刻意的限制:查询窗口只有 5 分钟,SQL 设置了 4 秒超时,Prometheus 标签只保留区域维度。Exporter 自己也输出成功状态和查询耗时,出了问题不会只剩下一块空白的 Grafana 面板。


启动并检查:

export MONITOR_NOW='2026-07-15T14:25:00+00:00'
python exporter.py
curl http://127.0.0.1:9105/metrics | grep sampledb_

把 KWDB 原生指标和业务指标写进 Prometheus

 

本文按 KWDB 3.2.0 的部署方式组织,Prometheus 和 Grafana 版本沿用文档示例中的 Prometheus 2.53.0 与 Grafana 11.1.0。

 

1. Prometheus 配置

 

KWDB 官方示例使用 /_status/vars,不是常见的 /metrics。自定义 Exporter 才使用 /metrics:

 

global:
  scrape_interval: 10s
  evaluation_interval: 10s

rule_files:
  - "rules/alerts.rules.yml"
  - "rules/aggregation.rules.yml"
  - "rules/sampledb.rules.yml"

scrape_configs:
  - job_name: "kaiwudb"
    metrics_path: "/_status/vars"
    scheme: "http"
    static_configs:
      - targets:
          - "127.0.0.1:8080"
          - "127.0.0.1:8081"
          - "127.0.0.1:8082"
        labels:
          cluster: "my-kaiwudb-cluster"

  - job_name: "sampledb-exporter"
    metrics_path: "/metrics"
    static_configs:
      - targets:
          - "127.0.0.1:9105"
        labels:
          source: "sampledb"
          

启动 Prometheus:

./prometheus --config.file=prometheus.yml


页面默认在 http://127.0.0.1:9090。第一次配置时先检查两个 Job 都是 UP,再去看 Grafana。否则后面的面板问题很难区分是 PromQL 写错还是根本没有采集到数据。

2. 增加业务告警规则

 

KWDB 自带的 alerts.rules.yml 继续保留,用来检测节点不可达、磁盘空间、Range、Raft 和证书等问题。SampleDB 业务规则单独放在 sampledb.rules.yml:

 

groups:
  - name: sampledb.business
    rules:
      - alert: SampleDBAreaPowerHigh
        expr: sampledb_area_power_watts > 7000
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "区域功率超过阈值"
          description: "{{ $labels.region }}/{{ $labels.area }} 当前功率 {{ $value }} W"

      - alert: SampleDBMeterDataLag
        expr: sampledb_meter_data_lag_seconds > 180
        for: 3m
        labels:
          severity: critical
        annotations:
          summary: "电表数据延迟超过 3 分钟"
          description: "{{ $labels.region }}/{{ $labels.area }} 没有及时上报数据"

      - alert: SampleDBExporterDown
        expr: up{job="sampledb-exporter"} == 0
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "SampleDB Exporter 不可用"
          

SampleDBAreaPowerHigh 是业务告警,InstanceDead 是数据库节点告警。两者同时出现时,排查顺序不同:先确认 Exporter 是否还能查询,再看 KWDB 节点本身是否异常。

3. 通过 API 验证采集结果

 

curl 'http://127.0.0.1:9090/api/v1/query?query=up%7Bjob%3D%22sampledb-exporter%22%7D'


也可以直接查询原生 SQL 延迟指标:

curl --get 'http://127.0.0.1:9090/api/v1/query' \
  --data-urlencode 'query=histogram_quantile(0.99, rate(sql_service_latency_bucket{job="kaiwudb"}[5m]))'
  

在 Grafana 里合并两套监控视图

 

1. 添加 Prometheus 数据源

 

Grafana 默认地址是 http://127.0.0.1:3000。在 Connections > Data sources 中添加 Prometheus,URL 填:

 

http://127.0.0.1:9090


点击 Save & test,确认数据源可用。


生产环境不要继续使用默认的 admin/admin,也不要直接照搬 Prometheus 配置里的 insecure_skip_verify: true。启用 HTTPS 时应配置受信任的 CA,而不是跳过证书校验。

2. 导入 KWDB 官方面板

 

先导入官方面板,再新建一个业务 Dashboard。比较有用的三个面板是:

 

1.KaiwuDB_Console_Overview.json
4.KaiwuDB_Console_SQL.json
9.KaiwuDB_Console_Slow_Query.json


官方 SQL 面板已经使用了以下指标:

sql_conns
sql_failure_count
sql_service_latency_bucket
exec_latency_bucket
sql_distsql_queries_active


慢查询面板关注:

requests_slow_raft
requests_slow_distsender
requests_slow_lease
requests_slow_latch

3. 增加 SampleDB 业务面板

 

园区总功率:

 

sum(sampledb_area_power_watts)


按区域排行:

topk(10, sampledb_area_power_watts)


数据延迟:

max(sampledb_meter_data_lag_seconds)


Exporter 查询耗时 P95:

histogram_quantile(
  0.95,
  rate(sampledb_exporter_scrape_duration_seconds_bucket[5m])
)


KWDB SQL 服务延迟 P99:

histogram_quantile(
  0.99,
  rate(sql_service_latency_bucket{job="kaiwudb"}[5m])
)


这几块面板放在同一行时,能看出一个很实用的关系:如果业务功率图异常,但 KWDB SQL 延迟正常,问题更可能在数据本身或采集链路;如果两者一起升高,再去检查 SQL、连接数和节点资源。

把异常完整回放一遍

 

1. 先制造业务侧功率升高

 

为了模拟 14:20 的尖峰,在几块电表上追加一小段高功率数据:

 

INSERT INTO tsdb.meter_data
    (ts, voltage, current, power, energy, meter_id)
SELECT
    TIMESTAMPTZ '2026-07-15 14:20:00+00:00'
        + s * INTERVAL '5 minutes',
    228.0,
    8.2,
    3200.0,
    9000.0 + s * 20,
    'M' || (s + 1)::TEXT
FROM generate_series(0, 5) AS s;


Exporter 下一轮采集后,Prometheus 中这些电表所属区域的 sampledb_area_power_watts 会升高。持续 5 分钟后,SampleDBAreaPowerHigh 进入 firing 状态。

2. 再模拟数据上报中断

 

暂时停止写入某个区域的电表数据,或者把 MONITOR_NOW 调到数据集之后:

 

export MONITOR_NOW='2026-08-01T00:10:00+00:00'


Exporter 仍然能访问 KWDB,但 sampledb_meter_data_lag_seconds 会持续增加。这个状态和数据库节点掉线不同:

up{job="kaiwudb"}


仍然是 1,而业务延迟指标已经超过 180 秒。监控系统因此能区分“数据库活着,但业务数据没有继续到达”。

3. 最后模拟 KWDB 节点不可达

 

停止一个节点或临时阻断其 HTTP 监控端口,官方规则中的:

 

up{job="kaiwudb"} == 0


会触发 InstanceDead。Grafana 的 Overview、SQL 和 Slow Query 面板也会少一个实例的数据,这时不应该继续把业务延迟全部归因于电表写入。

这套监控链路留下的几个结论

 

这次接入后,Prometheus 中保存的是两类不同的指标:

 

 

指标代表什么入口
sql_conns、sql_service_latency_bucketKWDB SQL 服务状态/_status/vars
requests_slow_raft、requests_slow_latch节点内部慢请求/_status/vars
sampledb_area_power_watts区域业务功率Exporter /metrics
sampledb_meter_data_lag_seconds电表数据延迟Exporter /metrics


几条规则最终固定下来:

  1. 原始电表数据留在 KWDB,Prometheus 只接收低基数的聚合指标。
  2. KWDB 的 /_status/vars 和 Exporter 的 /metrics 分开配置、分开排障。
  3. 所有业务指标都要有对应的 Exporter 健康指标,不能只监控业务值。
  4. Grafana 先导入 KWDB 官方面板,再增加 SampleDB 业务面板,不修改官方面板中的指标语义。
  5. 告警规则按数据库层和业务层分组,避免收到一条告警后不知道应该查节点还是查数据采集。
  6. 这样串起来之后,监控的作用不再只是“看见曲线”。7 月 15 日的功率异常可以继续追到具体区域,数据延迟可以和数据库健康状态分开判断,SQL 延迟升高时也能在同一块 Grafana 页面上看到。SampleDB 负责保存明细,Prometheus 负责时间序列指标和告警,Grafana 负责把两者放到一个排查入口里。

 

-- 坚持原创,转载请注明出处 --

评论(0)

Me