4. 数据存储方案设计

数据存储这块,说实话,是仿真回放系统里最容易翻车的地方。我见过不少团队,前期图省事直接上 MySQL,结果数据量一上来,查询慢得像蜗牛爬。今天咱们就聊聊时序数据库的选型、分区策略,还有冷热数据分离这些实战问题。

4.1 为什么非得用时序数据库?

仿真数据有个特点:时间戳密集、写入量大、查询多是范围扫描。普通关系型数据库处理这种场景,性能会急剧下降。我曾在项目中用 PostgreSQL 存过仿真日志,单表几千万条后,一个简单的范围查询就要十几秒。后来换了时序数据库,同样的查询,毫秒级返回。

说白了,时序数据库就是为这种场景量身定做的。它做了三件事:

  • 列式存储:按时间列组织数据,压缩率高
  • 预聚合:自动计算分钟级、小时级的统计值
  • 自动过期:可以设置数据保留策略,省心

4.2 两大主流方案:InfluxDB vs TimescaleDB

目前市面上最常用的就是 InfluxDB 和 TimescaleDB。我两个都用过,说说我的感受。

对比维度 InfluxDB TimescaleDB
底层引擎 自研 TSM 引擎 基于 PostgreSQL
写入性能 极高,单机百万点/秒 较高,依赖 PG 调优
查询语法 类 SQL(Flux/InfluxQL) 标准 SQL
数据压缩 优秀,通常 10:1 良好,依赖 PG 压缩
运维复杂度 中等,集群版需付费 低,PG 生态成熟
适用场景 高频写入、轻量查询 复杂分析、关联查询

我的建议:如果仿真数据量巨大(每天上亿条),且查询模式简单(按时间范围取原始值),选 InfluxDB。如果数据需要和业务表做关联分析,或者团队对 SQL 更熟悉,选 TimescaleDB。

4.3 数据分区策略:别让一张表扛下所有

嗯,这里要注意。不管选哪个数据库,分区都是必须做的。我见过有人把一年数据塞进一张表,结果查询时全表扫描,CPU 直接飙到 100%。

分区策略一般有两种:

  • 按时间分区:比如按天、按周分区。这是最常用的方式。查询时只扫描相关分区,性能提升明显。
  • 按标签分区:比如按仿真场景 ID、按设备 ID 分区。适合多租户场景。

以 TimescaleDB 为例,创建超表并分区:

-- 创建普通表
CREATE TABLE sim_data (
    time        TIMESTAMPTZ NOT NULL,
    device_id   INT NOT NULL,
    scenario_id INT NOT NULL,
    value       DOUBLE PRECISION,
    metadata    JSONB
);

-- 转换为超表,按天分区
SELECT create_hypertable('sim_data', 'time', 
    chunk_time_interval => INTERVAL '1 day');

-- 还可以按 device_id 做空间分区
SELECT add_dimension('sim_data', 
    by_hash('device_id', 4));

小技巧:分区粒度别太细。按小时分区的话,分区数太多,元数据管理开销反而大。我个人习惯按天分区,数据量特别大的场景按 6 小时分区。

4.4 冷热数据分离:省钱又高效

仿真数据有个特点:越新的数据越常被查。一周前的数据,访问频率可能下降 90%。这时候把冷热数据混在一起存,就是浪费钱。

我曾经在项目中踩过坑:把所有数据都放在 SSD 上,一个月后存储成本翻了三倍。后来做了冷热分离,成本直接降了 60%。

冷热分离的核心思路:

  1. 热数据:最近 7 天的数据,存在高性能存储(SSD),保留原始精度。
  2. 温数据:7 天到 3 个月的数据,存在普通 HDD,可以降采样(比如 1 秒精度降到 1 分钟精度)。
  3. 冷数据:3 个月以上的数据,归档到对象存储(如 S3、MinIO),只保留统计摘要。

InfluxDB 的保留策略配置:

-- 创建保留策略
CREATE RETENTION POLICY "hot_policy" 
    ON "simdb" DURATION 7d REPLICATION 1 SHARD DURATION 1d;

CREATE RETENTION POLICY "warm_policy" 
    ON "simdb" DURATION 90d REPLICATION 1 SHARD DURATION 7d;

-- 创建连续查询,自动降采样
CREATE CONTINUOUS QUERY "cq_1m" ON "simdb" 
BEGIN
    SELECT mean(value) INTO "simdb"."warm_policy"."sim_data_1m"
    FROM "simdb"."hot_policy"."sim_data"
    GROUP BY time(1m), device_id, scenario_id
END;

注意:降采样会丢失数据细节。如果后续需要回放原始数据,一定要保留一份原始副本。我建议在冷数据归档前,先做一次完整性校验。

4.5 整体架构图

下面这张图展示了数据存储的完整流程,从写入到冷热分离:

仿真数据存储架构 仿真数据流 时序数据库(InfluxDB / TimescaleDB) 数据分区(按天 + 按设备ID) 冷热数据分离 热数据(SSD,7天) 原始精度 温数据(HDD,90天) 降采样(1分钟) 冷数据(对象存储) 统计摘要

4.6 避坑指南

最后分享几个我踩过的坑:

  • 分区键选择不当:我曾经用时间戳做唯一分区键,结果数据分布不均匀,某些分区特别大。后来加了设备 ID 做二级分区,问题解决。
  • 忘记设置数据过期:InfluxDB 默认保留策略是永久。如果不设置,磁盘会慢慢被撑爆。我建议上线第一天就配好保留策略。
  • 冷数据恢复测试:归档到对象存储后,一定要定期做恢复测试。我遇到过归档文件损坏,结果恢复时才发现,那叫一个惨。

总结一下:选型看场景,分区按时间,冷热要分离。这套方案我在多个项目中验证过,稳定运行两年没问题。