4. SCADA实时数据库选型与集成

SCADA系统采集的遥测数据具有典型的时序特征:按固定周期产生、带时间戳、写多读少、按时间范围聚合查询。传统关系库在每秒数万点写入时很快遇到瓶颈,因此本项目在采集层与业务层之间引入时序数据库(TSDB)作为实时/历史数据存储。本节围绕InfluxDB与TDengine两条主流路线,讲清选型依据、部署方式、数据模型设计,以及源码中写入与查询接口的实现。

一、选型:InfluxDB 与 TDengine 的取舍

两者都能满足SCADA场景,但定位不同。InfluxDB 2.x 采用 measurement + tag + field 模型,生态成熟、Flux/InfluxQL 查询灵活、Grafana 原生支持好,适合测点种类多、标签维度丰富的项目;其单机写入能力约数十万点/秒,集群版为商业授权。TDengine 采用“一个设备一张表”的模型,超级表(STable)定义schema,子表按设备自动扩展,内置降采样、流式计算和缓存,压缩率高、单机写入可达百万点/秒,且开源版即可用集群,适合测点数量大、以设备为单位的电力/能源监控。

本项目的选型判断可归纳为:若现场设备数在千级以内、需要复杂标签过滤与灵活聚合,选 InfluxDB;若设备数上万、以“设备+测点”固定结构为主、追求写入吞吐与存储成本,选 TDengine。示例项目默认采用 TDengine 3.x,同时保留 InfluxDB 适配层,通过统一接口屏蔽差异。

SCADA 时序数据链路与双库适配架构 现场设备 PLC / RTU / 电表 Modbus / IEC104 采集服务 点位轮询 / 订阅 质量码 + 时间戳 写入适配层 批量缓冲 / 重试 统一 TSDB 接口 InfluxDB 2.x bucket / measurement TDengine 3.x database / STable 查询接口层 时间范围 / 降采样 / 聚合 / 最新值 统一返回 Point{ts, value, quality} SCADA 应用 实时画面 / 趋势 报警 / 报表 关键参数(示例): 采集周期 1s | 批量写入 500 点/批 | 保留策略 90 天原始 + 1 年降采样 | 时间精度 ms | 时区统一 UTC 存储、本地展示

二、部署:单机起步,容器化落地

示例项目采用 Docker Compose 部署,便于开发与现场一致。TDengine 3.x 单机部署命令如下,映射 6030(原生连接)与 6041(REST/taosAdapter)端口:

docker run -d --name tdengine \
  -p 6030:6030 -p 6041:6041 \
  -v /data/taos:/var/lib/taos \
  -e TAOS_FQDN=tdengine \
  tdengine/tdengine:3.2.0.0

InfluxDB 2.x 则需初始化组织、bucket 与 token:

docker run -d --name influxdb \
  -p 8086:8086 \
  -v /data/influx:/var/lib/influxdb2 \
  -e DOCKER_INFLUXDB_INIT_MODE=setup \
  -e DOCKER_INFLUXDB_INIT_USERNAME=scada \
  -e DOCKER_INFLUXDB_INIT_PASSWORD=Scada@2024 \
  -e DOCKER_INFLUXDB_INIT_ORG=energy \
  -e DOCKER_INFLUXDB_INIT_BUCKET=scada_raw \
  influxdb:2.7

部署后需验证:TDengine 用 taos -h tdengine -s "show databases;" 确认连通;InfluxDB 用 curl http://localhost:8086/health 返回 {"status":"pass"}。生产环境还应配置数据目录独立磁盘、开启 WAL、设置合理的 maxRows 与缓存参数。

三、数据模型设计

SCADA点位可抽象为“设备—测点—时间—值—质量码”。TDengine 中,用超级表定义一类设备的公共schema,子表按设备实例创建。示例:

CREATE DATABASE scada KEEP 90d PRECISION 'ms';
USE scada;

CREATE STABLE meters (
  ts TIMESTAMP,
  value DOUBLE,
  quality TINYINT
) TAGS (
  station BINARY(32),
  device  BINARY(32),
  point   BINARY(32),
  unit    BINARY(16)
);

CREATE TABLE dev_001 USING meters
  TAGS ('变电站A', 'METER_001', 'Ua', 'V');

InfluxDB 对应模型为 measurement=meter,tag 为 station/device/point/unit,field 为 value/quality。两者共同原则:标签(tag)用于过滤,字段(field)用于计算;时间戳统一毫秒精度、UTC 存储;质量码 0=正常、1=可疑、2=无效,便于查询时过滤坏点。

四、源码中的写入接口

写入适配层定义统一接口,屏蔽底层差异。以 Go 为例:

type Point struct {
    Ts      int64   // 毫秒时间戳
    Device  string
    Point   string
    Value   float64
    Quality int8
}

type TSDBWriter interface {
    WriteBatch(ctx context.Context, pts []Point) error
    Close() error
}

TDengine 实现使用参数绑定与批量插入,避免SQL拼接注入并提升性能:

func (w *TaosWriter) WriteBatch(ctx context.Context, pts []Point) error {
    tx, err := w.db.Begin()
    if err != nil { return err }
    stmt, _ := tx.Prepare("INSERT INTO ? USING meters TAGS(?,?,?,?) VALUES(?,?,?)")
    for _, p := range pts {
        stmt.SetTableName("dev_" + p.Device)
        stmt.SetTags("变电站A", p.Device, p.Point, "V")
        stmt.SetTimestamp(0, time.UnixMilli(p.Ts))
        stmt.SetFloat(1, p.Value)
        stmt.SetInt8(2, p.Quality)
        if _, err := stmt.Exec(); err != nil { return err }
    }
    return tx.Commit()
}

InfluxDB 实现使用 Line Protocol 批量写入:

func (w *InfluxWriter) WriteBatch(ctx context.Context, pts []Point) error {
    var buf bytes.Buffer
    for _, p := range pts {
        fmt.Fprintf(&buf, "meter,station=变电站A,device=%s,point=%s value=%f,quality=%di %d\n",
            p.Device, p.Point, p.Value, p.Quality, p.Ts*1e6)
    }
    _, err := w.api.WriteWithResponse(ctx, w.org, w.bucket, buf.String())
    return err
}

写入策略上,采集服务将点位放入环形缓冲,每 500 点或每 200ms 触发一次批量写入;失败时指数退避重试,超过阈值写入本地磁盘队列,恢复后补传,保证断网不丢数据。

五、查询接口与结果判读

查询接口需支持三类典型需求:最新值(实时画面)、时间范围原始值(趋势)、降采样聚合(报表)。TDengine 示例:

-- 最新值
SELECT LAST_ROW(value) FROM meters WHERE device='METER_001' AND point='Ua';

-- 5分钟平均,过滤坏点
SELECT _wstart, AVG(value) FROM meters
WHERE point='Ua' AND quality=0 AND ts >= NOW-1h
INTERVAL(5m);

InfluxDB 对应 Flux 查询:

from(bucket:"scada_raw")
  |> range(start: -1h)
  |> filter(fn:(r) => r._measurement=="meter" and r.point=="Ua" and r.quality==0)
  |> aggregateWindow(every: 5m, fn: mean)

结果判读要点:若最新值查询返回空,先确认设备是否在写、时间戳是否落在保留期内;若聚合曲线出现断点,检查 quality 过滤是否过严或采集是否丢点;若查询延迟高,应确认是否命中时间索引、是否对 tag 做了高基数设计。建议在接口层统一返回 {ts, value, quality} 数组,前端不感知底层库差异。

实践建议:先用 TDengine 单机跑通写入与查询,再通过适配层接入 InfluxDB 做对比压测。压测时关注写入吞吐(点/秒)、P99 查询延迟、磁盘日增量三个指标,用真实点位规模而非几十个点验证,才能得到有参考价值的选型结论。