ABP vNext 的工业时间序列治理:InfluxDB vs TimescaleDB 落地对比

ABP vNext 的工业时间序列治理:InfluxDB vs TimescaleDB 落地对比 🎯


📚 目录

  • [ABP vNext 的工业时间序列治理:InfluxDB vs TimescaleDB 落地对比 🎯](#ABP vNext 的工业时间序列治理:InfluxDB vs TimescaleDB 落地对比 🎯)
    • [TL;DR ✨](#TL;DR ✨)
    • [1. 业务背景与问题定义 🏭](#1. 业务背景与问题定义 🏭)
    • [2. 技术选型概览:InfluxDB vs TimescaleDB 🥊](#2. 技术选型概览:InfluxDB vs TimescaleDB 🥊)
      • [InfluxDB(v2 / v3)](#InfluxDB(v2 / v3))
      • [TimescaleDB(PostgreSQL 扩展)](#TimescaleDB(PostgreSQL 扩展))
    • [3. ABP 架构与模块 🧩](#3. ABP 架构与模块 🧩)
      • [Mermaid:TS Gateway 端到端(写入/查询 + 决策/降级)](#Mermaid:TS Gateway 端到端(写入/查询 + 决策/降级))
    • [4. 数据建模(统一 → Provider 映射)🧱](#4. 数据建模(统一 → Provider 映射)🧱)
    • [5. 写入通道(高吞吐 + 背压)⚡](#5. 写入通道(高吞吐 + 背压)⚡)
      • [5.1 InfluxProvider:**严格转义** + **Options/Factory 启用 Gzip** + **复用 WriteApiAsync**](#5.1 InfluxProvider:严格转义 + Options/Factory 启用 Gzip + 复用 WriteApiAsync)
      • [5.2 TimescaleProvider:**Npgsql Binary COPY**(高吞吐标准姿势)](#5.2 TimescaleProvider:Npgsql Binary COPY(高吞吐标准姿势))
    • [6. 压缩 / 列存策略(降本优先)💾](#6. 压缩 / 列存策略(降本优先)💾)
    • [7. 连续聚合(1m/5m/1h/1d)⏱️](#7. 连续聚合(1m/5m/1h/1d)⏱️)
      • [TimescaleDB(Continuous Aggregates)](#TimescaleDB(Continuous Aggregates))
      • [InfluxDB(Tasks 降采样)](#InfluxDB(Tasks 降采样))
    • [8. 冷热分层与生命周期(ILM)🧊🔥](#8. 冷热分层与生命周期(ILM)🧊🔥)
    • [9. 统一查询 API(面向前端/报表)🧠](#9. 统一查询 API(面向前端/报表)🧠)
    • [10. 可靠性与一致性 🛡️](#10. 可靠性与一致性 🛡️)
    • [11. 选型建议清单 📝](#11. 选型建议清单 📝)
    • [12. 参数速查表 🧾](#12. 参数速查表 🧾)
    • [13. Docker Compose(含 Influx **secrets** 版本)🐳](#13. Docker Compose(含 Influx secrets 版本)🐳)
    • [14. ABP 关键接口与注册 🧩](#14. ABP 关键接口与注册 🧩)
    • [15. Timescale 索引与 chunk 大小建议 🛠️](#15. Timescale 索引与 chunk 大小建议 🛠️)

TL;DR ✨

  • 指标/事件型,高写入 + 时间窗 + 标签过滤 → 选 InfluxDB
    Tasks + 多 bucket(保留期) 实现降采样与冷热分层;严格遵守行协议转义 ;批量写入随版本调优v2 ≈ 5000 行/批v3 ≈ 10,000 行或 10MB(先到为准)启用 Gzip。🧪📈
  • 强 SQL/生态(JOIN/子查询/权限/BI/ETL) → 选 TimescaleDB
    Hypertable + Continuous Aggregates(CA) + 压缩/列存(2.18+) + 表空间迁移 实现 ILM。v2.13+ 默认仅物化(实时关闭),需要时再开启。🧱🧊
  • 通用最佳实践先聚合后查询冷热分层 + 压缩/列存多租户隔离 + 限流/降级迟到数据用 offset/end_offset 留刷新窗口。🧭

1. 业务背景与问题定义 🏭

工业场景(PLC/OPC UA 点位、能耗、设备健康)往往写多读多 、强依赖时间窗口聚合 ,且要求长期留存 。目标是在 成本(压缩/列存) × 性能(写/查) × 可维护(分层/迁移) 三角中取得平衡。⚖️


2. 技术选型概览:InfluxDB vs TimescaleDB 🥊

InfluxDB(v2 / v3)

  • 行协议(Line Protocol)measurement,tag_set field_set timestamp

    转义规则:measurement 仅转义空格/逗号 ;tag/field-key 还需转义等号 ;string field 转义 "\。✍️

  • 降采样与分层Tasks 把高分辨率数据聚合写到目标 bucket(热/温/冷各自保留期)。Task offset 用于容纳迟到数据,避免清理竞态。🧹

  • 批量写最佳实践

    • v2 :起点 ~5000 行/批
    • v310,000 行或 10MB(先到为准)
      同时建议启用 Gzip。🧵
  • v3 SQL/Flight SQL:可用 SQL / Flight SQL 客户端(Java/JS/Python/C#)直接查询。🛫

TimescaleDB(PostgreSQL 扩展)

  • Hypertable/Chunks:基于时间(可选空间)自动分片。⏱️🧩

  • 连续聚合(CA)CREATE MATERIALIZED VIEW ... WITH (timescaledb.continuous)v2.13+ 默认关闭实时聚合materialized_only=true),可按需开启。⏳

  • 压缩 vs 2.18+ 列存(Hypercore/Columnstore)

    • 旧 APItimescaledb.compress + add_compression_policy();用 chunk_compression_stats 评估。
    • 2.18+enable_columnstore / orderby / segmentby + add_columnstore_policy(),列存滚动转换,常见可达**>90%**降容(视数据而定)。📦
  • 冷热分层表空间 + move_chunk() / show_chunks() 迁移旧 chunk 到冷盘,并配合保留策略清理。🧊


3. ABP 架构与模块 🧩

  • TS.Gateway(ABP Module) :统一 REST /api/ts/write/api/ts/query
  • Provider 抽象ITimeSeriesProvider(InfluxProvider / TimescaleProvider 可切换)
  • IngestorChannel<T> 聚合批写 + Polly 重试/熔断(失败批落盘回补)
  • 多租户:ABP Multi-Tenancy + DataFilter 默认按租户过滤与限流
  • 可观测:OTel Trace & Metrics(写/查/聚合/压缩阶段 p95/错误率/落后度)

Mermaid:TS Gateway 端到端(写入/查询 + 决策/降级)

/api/ts/write /api/ts/query Influx Timescale Line Protocol 批量 Tasks/offset Binary COPY CA 刷新 end_offset >=5m >=1h 其他 失败->落盘 失败->落盘 采集器/前端 TS Gateway (ABP) 报表/看板 TS_PROVIDER? Ingestor
Channel 批聚合
Polly 重试/熔断 Ingestor
Channel 批聚合
Polly 重试/熔断 (InfluxDB HOT) Influx 降采样 agg_5m Influx 降采样 agg_1h (Timescale HOT Hypertable) CA_5m(materialized_only) CA_1h interval/agg? 命中 CA_5m / agg_5m 命中 CA_1h / agg_1h 回源明细 + 限流/分页 (重放 LP 脚本) (COPY 回补脚本)


4. 数据建模(统一 → Provider 映射)🧱

统一点位模型
metric, tags: tenantId, deviceId, pointId, site, fields: value, status, timestamp

  • Influx 映射measurement = metric高基数 属性尽量放 fields ,常用过滤维度放 tags(控制 series cardinality)。⚠️

  • Timescale 映射(PG → Hypertable):

    sql 复制代码
    CREATE TABLE readings(
      tenant_id text NOT NULL,
      device_id text NOT NULL,
      point_id  text NOT NULL,
      ts timestamptz NOT NULL,
      value double precision NULL,
      status text NULL,
      tags jsonb NULL,
      PRIMARY KEY (tenant_id, device_id, point_id, ts)
    );
    SELECT create_hypertable('readings','ts', chunk_time_interval => interval '1 day');

WARNING(Influx 高基数) :强唯一且高速变化的维度(如随机实例 ID、UUID)放入 tags 会造成 series 爆炸;请改放 fields 或仅在明细保留。🧨


5. 写入通道(高吞吐 + 背压)⚡

5.1 InfluxProvider:严格转义 + Options/Factory 启用 Gzip + 复用 WriteApiAsync

csharp 复制代码
using InfluxDB.Client;
using InfluxDB.Client.Api.Domain;

public record TsPoint(string Metric, string TenantId, string DeviceId,
                      string PointId, DateTimeOffset Ts, double Value, string? Status = null);

// 转义:measurement(空格/逗号);tag/field-key(空格/逗号/等号);string field(\" 和 \\)
static string EscMeas(string s) => s.Replace(" ", "\\ ").Replace(",", "\\,");
static string EscTag(string s)  => s.Replace(" ", "\\ ").Replace(",", "\\,").Replace("=", "\\=");
static string EscFieldKey(string s) => EscTag(s);
static string EscFieldString(string s) => s.Replace("\\", "\\\\").Replace("\"", "\\\"");

static string ToLine(TsPoint p)
{
    var m = EscMeas(p.Metric);
    var tags = $"tenantId={EscTag(p.TenantId)},deviceId={EscTag(p.DeviceId)},pointId={EscTag(p.PointId)}";
    var fields = $"value={p.Value}" + (string.IsNullOrEmpty(p.Status) ? "" : $",status=\"{EscFieldString(p.Status)}\"");
    var tsNs = p.Ts.ToUnixTimeMilliseconds() * 1_000_000L; // ns
    return $"{m},{tags} {fields} {tsNs}";
}

public sealed class InfluxProvider : ITimeSeriesProvider
{
    private readonly InfluxDBClient _client;
    private readonly WriteApiAsync _writer;
    private readonly string _org, _bucket;

    public InfluxProvider(IConfiguration cfg)
    {
        var options = new InfluxDBClientOptions.Builder()
            .Url(cfg["INFLUX__Url"])
            .AuthenticateToken(cfg["INFLUX__Token"])
            .EnableGzip()                    // ✅更通用的 Gzip 启用方式
            .Build();

        _client = InfluxDBClientFactory.Create(options);
        _writer = _client.GetWriteApiAsync(); // ✅复用长生命周期,内部批处理/缓冲
        _org = cfg["INFLUX__Org"];
        _bucket = cfg["INFLUX__Bucket"];
    }

    public async Task WriteAsync(IEnumerable<TsPoint> batch, CancellationToken ct)
    {
        var lines = batch.Select(ToLine).ToList();
        await _writer.WriteRecordsAsync(lines, WritePrecision.Ns, _bucket, _org, ct);
        // v2:~5000 行/批;v3:~10k 行或 10MB(先到为准),可配置化
    }
}

5.2 TimescaleProvider:Npgsql Binary COPY(高吞吐标准姿势)

csharp 复制代码
using Npgsql;

public sealed class TimescaleProvider : ITimeSeriesProvider
{
    private readonly NpgsqlDataSource _ds;
    public TimescaleProvider(IConfiguration cfg) => _ds = NpgsqlDataSource.Create(cfg["PG__Conn"]);

    public async Task WriteAsync(IEnumerable<TsPoint> batch, CancellationToken ct)
    {
        await using var conn = await _ds.OpenConnectionAsync(ct);
        await using var writer = await conn.BeginBinaryImportAsync(
          "COPY readings (tenant_id, device_id, point_id, ts, value, status) FROM STDIN (FORMAT BINARY)", ct);

        foreach (var p in batch)
        {
            await writer.StartRowAsync(ct);
            await writer.WriteAsync(p.TenantId, NpgsqlTypes.NpgsqlDbType.Text);
            await writer.WriteAsync(p.DeviceId, NpgsqlTypes.NpgsqlDbType.Text);
            await writer.WriteAsync(p.PointId,  NpgsqlTypes.NpgsqlDbType.Text);
            await writer.WriteAsync(p.Ts.UtcDateTime, NpgsqlTypes.NpgsqlDbType.TimestampTz);
            await writer.WriteAsync(p.Value, NpgsqlTypes.NpgsqlDbType.Double);
            await writer.WriteAsync(p.Status, NpgsqlTypes.NpgsqlDbType.Text);
        }
        await writer.CompleteAsync(ct); // ✅必须提交,否则 COPY 回滚
    }
}

6. 压缩 / 列存策略(降本优先)💾

TimescaleDB

旧 API(≤2.17 & 向后兼容)

sql 复制代码
ALTER TABLE readings SET (
  timescaledb.compress,
  timescaledb.compress_orderby = 'ts DESC',
  timescaledb.compress_segmentby = 'tenant_id,device_id'
);
SELECT add_compression_policy('readings', INTERVAL '7 days');
SELECT * FROM chunk_compression_stats('readings'); -- 评估压缩率

2.18+(Hypercore/Columnstore,推荐)

sql 复制代码
-- 列存:把旧 chunk 滚动转换为 Columnstore
ALTER TABLE readings SET (
  timescaledb.enable_columnstore,
  timescaledb.orderby   = 'ts DESC',
  timescaledb.segmentby = 'tenant_id,device_id'
);

-- ✅统一用 SELECT 形式,兼容性更好
SELECT add_columnstore_policy('readings', INTERVAL '7 days');

InfluxDB

  • Tasks 将 1s 明细降采到 1m/5m/1h,分别写入不同保留期的 bucket(热/温/冷);使用 offset 处理迟到数据与保留清理竞态。

7. 连续聚合(1m/5m/1h/1d)⏱️

TimescaleDB(Continuous Aggregates)

sql 复制代码
-- 仅物化(v2.13+ 默认),稳定延迟且不回源明细
CREATE MATERIALIZED VIEW ca_5m
WITH (timescaledb.continuous, timescaledb.materialized_only = true) AS
SELECT time_bucket('5 minutes', ts) AS bucket,
       tenant_id, device_id, point_id,
       avg(value) AS avg_v, max(value) AS max_v, min(value) AS min_v
FROM readings
GROUP BY 1,2,3,4
WITH NO DATA;

-- 刷新策略:排除最近 1 小时(迟到保护),每 15 分钟刷新
SELECT add_continuous_aggregate_policy('ca_5m',
  start_offset => INTERVAL '30 days',
  end_offset   => INTERVAL '1 hour',
  schedule_interval => INTERVAL '15 minutes');

-- 需要"读到最新"时,可临时开启实时聚合
ALTER MATERIALIZED VIEW ca_5m SET (timescaledb.materialized_only = false);

-- ✅为 CA 增加查询友好索引(按你的查询模式微调列)
CREATE INDEX IF NOT EXISTS ix_ca5m_tenant_device_bucket
  ON ca_5m (tenant_id, device_id, bucket DESC);

InfluxDB(Tasks 降采样)

flux 复制代码
option task = {name: "downsample_5m", every: 5m, offset: 2m}
from(bucket: "hot")
  |> range(start: -task.every)
  |> aggregateWindow(every: 5m, fn: mean)
  |> to(bucket: "agg_5m")

8. 冷热分层与生命周期(ILM)🧊🔥

Mermaid:冷热分层/生命周期策略(含窗口与迁移)

Tasks/CA + offset/end_offset retention/move_chunk 到期 HOT 0~7d
明细/未压缩 WARM 7~90d
降采/压缩或列存 COLD 90d+
聚合/只读 (归档/删除)

  • Timescale :建表空间 (冷盘/NAS),show_chunks 找旧 chunk,move_chunk 迁移,配 add_retention_policy 清理更老数据。
  • Influx :多 bucket + 各自保留期;Tasks 负责把数据从热层逐步搬到温/冷层。

9. 统一查询 API(面向前端/报表)🧠

请求metric, tenantId, deviceId[], from/to, agg: raw|avg|min|max|pXX, interval
路由策略(伪代码):

  • interval >= 5mTimescale: ca_5m / Influx: agg_5m
  • interval >= 1hca_1h / agg_1h
  • 否则回源明细(配限流/分页)。
    Timescale 可通过 materialized_only=true 强制不回源;Influx 通过时间范围选定 bucket 避免扫热层全集。

Timescale 查询示例(数组参数显式类型)

csharp 复制代码
await using var cmd = conn.CreateCommand();
cmd.CommandText = @"
SELECT time_bucket(@iv, ts) AS bucket, device_id, avg(value) AS avg_v
FROM readings
WHERE tenant_id = @t AND device_id = ANY(@d) AND ts BETWEEN @f AND @to
GROUP BY 1,2
ORDER BY 1";
cmd.Parameters.AddWithValue("t", tenantId);
cmd.Parameters.Add("d", NpgsqlTypes.NpgsqlDbType.Array | NpgsqlTypes.NpgsqlDbType.Text).Value = deviceIds;
cmd.Parameters.AddWithValue("f", from.UtcDateTime);
cmd.Parameters.AddWithValue("to", to.UtcDateTime);
cmd.Parameters.Add("iv", NpgsqlTypes.NpgsqlDbType.Interval).Value = interval ?? TimeSpan.FromMinutes(5);

10. 可靠性与一致性 🛡️

  • 幂等键(tenantId, deviceId, pointId, ts),网关层去重。
  • 迟到与重算 :Timescale 用 end_offset、Influx 任务用 offset;必要时做二次回补。
  • 降级与切换:Provider 可切换(单库可用);失败批落盘 + 二次导入(PG 用 COPY;Influx 重放 LP)。

11. 选型建议清单 📝

  • InfluxDB 优先 :写入压力大、以时间窗 + tag 过滤为主、链路简单(Telegraf→Influx→Grafana)。遵循行协议转义批量写 (v2 ~5k;v3 ~10k/10MB)、Tasks+bucket 分层。
  • TimescaleDB 优先 :强 SQL/BI/权限/JOIN;用 CA + 压缩/列存 + 表空间迁移 做 ILM;v2.13+ 默认仅物化,延迟更稳。
  • 通用先聚合后查询冷热分层多租户隔离(ABP DataFilter)限流/降级

12. 参数速查表 🧾

  • Influx v2 写入 :批量 ≈ 5000 行;Gzip=ON ;时间精度尽量用 ms(除非业务要求 ns)。

  • Influx v3 写入 :批量 ≈ 10000 行或 10MB 上限(先到为准);Gzip=ON

  • Timescale CAv2.13+ 默认 materialized_only=true(仅物化);必要时手动打开实时聚合。

  • Timescale 压缩/列存

    • 旧 API:timescaledb.compress + add_compression_policy
    • 新 API(2.18+):enable_columnstore + add_columnstore_policy用 SELECT 调用)。
  • Chunk 大小 :活跃 chunk(含索引)≈ 内存 25% ;高写入建议把 chunk_time_interval 从 7d 调整为 1d


13. Docker Compose(含 Influx secrets 版本)🐳

yaml 复制代码
version: "3.8"
services:
  influxdb:
    image: influxdb:2
    ports: ["8086:8086"]
    environment:
      DOCKER_INFLUXDB_INIT_MODE: setup
      DOCKER_INFLUXDB_INIT_ORG: acme
      DOCKER_INFLUXDB_INIT_BUCKET: hot
      DOCKER_INFLUXDB_INIT_USERNAME_FILE: /run/secrets/influx_user
      DOCKER_INFLUXDB_INIT_PASSWORD_FILE: /run/secrets/influx_pass
      DOCKER_INFLUXDB_INIT_ADMIN_TOKEN_FILE: /run/secrets/influx_token
    secrets: [influx_user, influx_pass, influx_token]
    volumes: [ "./influx2:/var/lib/influxdb2" ]
  timescaledb:
    image: timescale/timescaledb:latest-pg16
    environment: ["POSTGRES_PASSWORD=pass"]
    ports: ["5432:5432"]
    command: ["-c","shared_buffers=1GB","-c","timescaledb.telemetry_level=off"]
    volumes: [ "./pg:/var/lib/postgresql/data" ]
  ts-gateway:
    build: .
    depends_on: [influxdb, timescaledb]
    environment:
      - TS_PROVIDER=Influx # or Timescale
      - INFLUX__Url=http://influxdb:8086
      - INFLUX__Org=acme
      - INFLUX__Bucket=hot
      - INFLUX__Token=@use-secrets@
      - PG__Conn=Host=timescaledb;Username=postgres;Password=pass;Database=ts
    ports: ["5000:8080"]
secrets:
  influx_user:  { file: ./.secrets/influx_user }
  influx_pass:  { file: ./.secrets/influx_pass }
  influx_token: { file: ./.secrets/influx_token }

14. ABP 关键接口与注册 🧩

csharp 复制代码
public interface ITimeSeriesProvider
{
    Task WriteAsync(IEnumerable<TsPoint> batch, CancellationToken ct);
    Task<IReadOnlyList<(DateTimeOffset Ts, double Value)>> 
      QueryAsync(string metric, string tenantId, string[] deviceIds,
                 DateTimeOffset from, DateTimeOffset to,
                 string agg /* raw|avg|min|max|p95 */, TimeSpan? interval, CancellationToken ct);
}

[DependsOn(typeof(AbpAspNetCoreModule))]
public class TsModule : AbpModule
{
  public override void ConfigureServices(ServiceConfigurationContext ctx)
  {
    ctx.Services.AddHttpClient();
    // 通过配置切换 Provider
    ctx.Services.AddSingleton<ITimeSeriesProvider, InfluxProvider>();
    // 或:ctx.Services.AddSingleton<ITimeSeriesProvider, TimescaleProvider>();
  }
}

15. Timescale 索引与 chunk 大小建议 🛠️

  • 查询/聚合友好索引

    sql 复制代码
    CREATE INDEX IF NOT EXISTS ix_readings_tenant_device_ts_desc
      ON readings (tenant_id, device_id, ts DESC);
  • chunk 大小经验法则 :让当前活跃 chunk(含索引)≈ 内存的 25% ;默认 7d,可按写入速率调为 1dset_chunk_time_interval('readings', INTERVAL '1 day'))。