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 行/批;
- v3 :10,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):
- 旧 API :
timescaledb.compress
+add_compression_policy()
;用chunk_compression_stats
评估。 - 2.18+ :
enable_columnstore / orderby / segmentby
+add_columnstore_policy()
,列存滚动转换,常见可达**>90%**降容(视数据而定)。📦
- 旧 API :
-
冷热分层 :表空间 +
move_chunk()
/show_chunks()
迁移旧 chunk 到冷盘,并配合保留策略清理。🧊
3. ABP 架构与模块 🧩
- TS.Gateway(ABP Module) :统一 REST
/api/ts/write
、/api/ts/query
- Provider 抽象 :
ITimeSeriesProvider
(InfluxProvider / TimescaleProvider 可切换) - Ingestor :
Channel<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):
sqlCREATE 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 >= 5m
→ Timescale:ca_5m
/ Influx:agg_5m
interval >= 1h
→ca_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 CA :v2.13+ 默认 materialized_only=true(仅物化);必要时手动打开实时聚合。
-
Timescale 压缩/列存:
- 旧 API:
timescaledb.compress + add_compression_policy
; - 新 API(2.18+):
enable_columnstore + add_columnstore_policy
(用 SELECT 调用)。
- 旧 API:
-
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 大小建议 🛠️
-
查询/聚合友好索引:
sqlCREATE INDEX IF NOT EXISTS ix_readings_tenant_device_ts_desc ON readings (tenant_id, device_id, ts DESC);
-
chunk 大小经验法则 :让当前活跃 chunk(含索引)≈ 内存的 25% ;默认 7d,可按写入速率调为 1d (
set_chunk_time_interval('readings', INTERVAL '1 day')
)。