物联网数据存储架构:从时序数据库到时序+关系型混合存储的选型复盘
·
物联网数据存储架构:从时序数据库到时序+关系型混合存储的选型复盘
物联网存储的挑战不是"存得下",而是"查得快"——当你需要在1秒内从万亿条传感器数据中找到某个设备过去7天的所有异常温度点时,存储架构的选择就决定了答案。
一、问题的分层
物联网平台的数据按属性天然分为两类:
| 数据类型 | 特征 | 写入模式 | 查询模式 | 数据量级 |
|---|---|---|---|---|
| 时序数据 | 时间戳+数值,不可变 | 高频追加写 | 时间范围+设备ID | 每天TB级 |
| 元数据 | 设备属性/关系,可变 | 低频随机写 | 关联查询/点查 | 百万级记录 |
这个划分是架构决策的基石。试图用MySQL同时解决两类问题,就是大多数物联网项目性能崩塌的根源。
二、时序数据库的深度对比
我们在同一硬件环境(3节点×32C64G×NVMe SSD)上做了全面的基准测试。
2.1 写入性能
测试数据:1000万设备,每设备每10秒上报1条(10字段),持续1小时
TDengine 3.2: ████████████████████████████████ 12,800,000 points/sec
InfluxDB 2.7: ██████████ 3,200,000 points/sec
TimescaleDB: ██████ 1,900,000 points/sec
ClickHouse: ██████████████████████ 8,500,000 points/sec
2.2 查询延迟对比(10亿条数据规模)
| 查询场景 | TDengine | InfluxDB | TimescaleDB | ClickHouse |
|---|---|---|---|---|
| 单设备24h数据 | 8ms | 95ms | 120ms | 45ms |
| 1000设备最新值 | 5ms | 220ms | 180ms | 35ms |
| 时间范围聚合(1h→1min降采样) | 120ms | 2800ms | 3400ms | 850ms |
| 全表扫描(max值) | 3200ms | 超时(30s) | 超时(30s) | 5800ms |
| 磁盘占用(压缩后) | 42GB | 128GB | 105GB | 68GB |
2.3 为什么选择TDengine
5个决定性因素:
- "一个设备一张表"的存储模型——同一设备的数据物理连续存储,时间范围查询本质上是顺序读,这是8ms延迟的物理基础。
- 列式存储+两级压缩——delta-of-delta时间戳压缩+类Gorilla浮点压缩,8:1的压缩比是实测数据。
- 超级表(STable)抽象——既保留了每设备独立存储的优势,又提供了跨设备的聚合查询能力。
- 内置降采样——滚动窗口聚合不需要再写Flink作业。
- 极低运维成本——3节点集群即可支撑每天TB级写入,不像ClickHouse需要更多节点。
三、关系型存储的MySQL建模
设备元数据虽然数据量不大,但模型复杂度不低:
-- 产品定义(设备模板)
CREATE TABLE product_definition (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
product_key VARCHAR(64) NOT NULL UNIQUE COMMENT '产品标识',
product_name VARCHAR(128) NOT NULL,
device_type ENUM('sensor','actuator','gateway','camera') NOT NULL,
protocol_type ENUM('mqtt','coap','http','modbus','opcua') NOT NULL,
data_format ENUM('json','cbor','binary','protobuf') NOT NULL DEFAULT 'json',
-- TSL(Thing Specification Language) - 物模型定义
thing_model JSON NOT NULL COMMENT '属性/服务/事件定义',
status TINYINT DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) COMMENT '产品定义表';
-- 设备实例
CREATE TABLE device_instance (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
device_id VARCHAR(64) NOT NULL UNIQUE COMMENT '设备唯一标识',
device_name VARCHAR(128),
product_id BIGINT NOT NULL,
-- 设备密钥(一机一密)
device_secret VARCHAR(128) NOT NULL,
-- 激活状态
activation_status ENUM('inactive','active','disabled','deleted') DEFAULT 'inactive',
first_online_time DATETIME,
last_online_time DATETIME,
-- 设备属性(冗余物模型中的关键字段,避免JSON解析)
firmware_version VARCHAR(32),
ip_address VARCHAR(45),
rssi INT COMMENT '信号强度',
-- 地理位置
location POINT SRID 4326 COMMENT 'GPS坐标',
geo_hash VARCHAR(12) COMMENT 'GeoHash(用于空间查询)',
-- 租户/分组
tenant_id BIGINT NOT NULL,
group_id BIGINT,
-- 标签(JSON,支持动态扩展)
tags JSON,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_product (product_id),
INDEX idx_tenant (tenant_id),
INDEX idx_status (activation_status),
SPATIAL INDEX idx_location (location),
INDEX idx_geo_hash (geo_hash)
) COMMENT '设备实例表';
-- 设备关系(如:传感器属于某个网关)
CREATE TABLE device_relation (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
parent_device_id VARCHAR(64) NOT NULL COMMENT '父设备(网关)',
child_device_id VARCHAR(64) NOT NULL COMMENT '子设备(传感器)',
relation_type ENUM('topology','group','shadow') NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_relation (parent_device_id, child_device_id, relation_type),
INDEX idx_child (child_device_id)
) COMMENT '设备拓扑关系表';
四、冷热数据分层存储
这是控制成本的核心策略。不加以分层,时序数据的存储成本会随设备数量线性增长,2年内吃掉所有利润。
@Service
public class TieredStorageManager {
// 分层策略
private static final Duration HOT_RETENTION = Duration.ofDays(7); // TDengine保留7天
private static final Duration WARM_RETENTION = Duration.ofDays(90); // Parquet保留90天
// >90天自动归档到冷存储(S3 Glacier类型)
/**
* 每日执行的数据迁移
*/
@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点
public void migrateData() {
// 1. TDengine → Parquet(7天→90天)
migrateHotToWarm();
// 2. Parquet → S3 Glacier(90天→永久)
migrateWarmToCold();
// 3. 降采样:原始数据 → 聚合摘要
downsampleOldData();
}
private void migrateHotToWarm() {
// 从TDengine导出第8天数据,写入Parquet
String sql = """
SELECT ts, device_id, temperature, humidity, vibration_rms
FROM sensor_data
WHERE ts >= NOW - 8d AND ts < NOW - 7d
""";
// 按设备ID分区写入Parquet,每个文件10000行
try (ResultSet rs = tdengineQuery(sql)) {
ParquetWriter writer = new ParquetWriter(
"/data/warm/date=" + LocalDate.now().minusDays(8) + "/",
CompressionCodecName.ZSTD // ZSTD压缩比最好
);
List<SensorRecord> batch = new ArrayList<>(10000);
while (rs.next()) {
batch.add(mapToRecord(rs));
if (batch.size() >= 10000) {
writer.writeBatch(batch);
batch.clear();
}
}
writer.close();
}
// 迁移完成后删除TDengine热数据(TDengine RETENTION自动处理)
}
/**
* 降采样策略
* 原始数据10s采样 → 1分钟聚合 → 1小时聚合 → 1天聚合
*/
private void downsampleOldData() {
// 30天以上数据:从1分钟聚合到1小时粒度
downsampleService.aggregate(
sourceTable = "sensor_data_1m",
targetTable = "sensor_data_1h",
windowDuration = Duration.ofHours(1),
aggregations = List.of("AVG", "MIN", "MAX", "STDDEV"),
startDate = LocalDate.now().minusDays(90),
endDate = LocalDate.now().minusDays(30)
);
}
}
4.1 查询路由
查询层需要感知数据在哪一层:
@Service
public class QueryRouter {
public QueryResult query(QueryRequest request) {
Instant queryStart = request.getStartTime();
if (queryStart.isAfter(Instant.now().minus(HOT_RETENTION))) {
// 全部在热存储:直接查TDengine
return tdengineQuery(request);
} else if (queryStart.isAfter(Instant.now().minus(WARM_RETENTION))) {
// 跨热温存储:并行查询,结果合并
CompletableFuture<QueryResult> hotFuture =
CompletableFuture.supplyAsync(() -> tdengineQuery(request.restrictTo(HOT_RETENTION)));
CompletableFuture<QueryResult> warmFuture =
CompletableFuture.supplyAsync(() -> parquetQuery(request.before(HOT_RETENTION)));
return hotFuture.thenCombine(warmFuture, QueryResult::merge).join();
} else {
// 涉及冷存储:返回降采样数据 + 提示
QueryResult result = parquetQuery(request);
result.setNote("查询范围超过90天,返回降采样数据(1小时粒度)");
return result;
}
}
}
五、总结
物联网数据存储架构的选型核心是"分层"——按数据特性分层、按冷热分层、按粒度分层:
-
时序数据和元数据必须分离存储。时序数据库(TDengine)处理高频写入和时间范围聚合,MySQL处理设备关系和属性查询。混用MySQL的结果就是写入瓶颈和查询超时。
-
TDengine在当前是物联网时序场景的最优解——8:1的压缩比、"一个设备一张表"的物理模型、毫秒级的单设备查询,这三个指标直接决定了系统的可行性和运营成本。
-
冷热分层不是可选的优化手段,而是架构设计的必选项。7天热数据(TDengine)+ 90天温数据(Parquet)+ 永久冷数据(S3 Glacier)+ 多级降采样,这套策略让我们的存储成本控制在每月1.2万元,而全量存TDengine的成本是8.6万元。
数据的价值与查询频率正相关,与存储成本负相关——分层的本质是让每一条数据待在它应该在的位置。
更多推荐


所有评论(0)