时序数据与流处理
物联网平台层真正的考验,是怎么把源源不断的位号值存下来、算出来、查得动。这一层既不是设备,也不是业务应用,而是夹在中间的" 数据骨架":每秒成千上万条带时间戳的读数涌入,既要写得进、留得住,又要随时被仪表盘、告警、AI 拉出来用。读完这一章,你会理解时序数据为什么需要专门的存储与管线,知道批处理与流处理各自适合什么,并能把这套通用范式对应到 IoT DC3 的数据平面——位号值经 RabbitMQ 异步投递、落进 TimescaleDB 超表、再进最新值缓存的那条链路。
这一层是什么 / 为什么存在
在四层参考架构里,平台层的职责是"存起来、管起来、算出来"。感知层产出物理量、网络层把它们送达之后,平台层要面对一个和传统业务系统截然不同的负载: 时序数据(time-series data)。
时序数据有几个共同的脾气,理解它们就理解了为什么不能拿一张普通关系表硬扛:
- 高写入、低更新:数据几乎只追加(append-only),一条采集落库后基本不再改。写入吞吐是主要压力,事务、行级更新、外键这些传统数据库的强项在这里几乎用不上。
- 天然按时间索引:每条记录都带一个时间戳,绝大多数查询都是"某设备某位号在某段时间内的值"。时间是第一查询维度,不是可有可无的列。
- 近期热、远期冷:刚采到的值被频繁读取(实时大屏、当前告警判定),几个月前的值只在偶尔的趋势分析里被扫到。访问热度随时间快速衰减。
- 价值随精度衰减:一年前的数据没人关心毫秒级细节,按小时或按天的均值/极值就够了。这给了**降采样(downsampling)和 保留策略(retention)**用武之地。
如果用普通关系表存这些数据,问题很快暴露:单表行数膨胀到上亿后,时间范围扫描越来越慢;B-tree 索引在持续追加下不断分裂、膨胀;没有内建的过期机制,老数据只能靠人写脚本清理。平台层需要的是**为"时间 + 追加 + 冷热分层"量身定制 **的存储与处理范式——这正是时序数据库与流处理管线存在的理由。
关键技术与权衡
把时序数据用好,靠的是一条管线,而不是单一组件。典型的物联网数据管线由四段串成:采集端把读数发往消息总线解耦,消费端把消息 落库进时序存储,存储之上再提供查询给应用与算法。
这条管线上有几处关键技术与权衡:
时序数据库:超表与分区思想。 通用方案(如 TimescaleDB、InfluxDB、TDengine)的共同思路是把一张逻辑大表自动切成许多小块 。以 TimescaleDB 的**超表(hypertable)**为例:对用户它就是一张普通的表,可以用标准 SQL 读写;底层却按时间维(再加上某个设备/标签维)自动把数据切成一个个 分块(chunk)。好处是显而易见的——查询带上时间范围时,规划器只扫相关的几个块(chunk pruning),而不是整张表;写入永远落在" 最新"那个块上,索引局部、不必在亿级数据里到处插入;过期数据可以整块(chunk)丢弃,比逐行 DELETE 快几个数量级。分区是时序存储一切性能的根基。
降采样与保留策略:用精度换成本。 既然数据价值随时间衰减,就没必要为远期数据付高精度的存储代价。两条互补的策略:保留策略 自动 drop 掉超过某个年龄的原始数据;降采样/连续聚合则把高频原始值预先卷成低频摘要(如每分钟原始值 → 每小时均值/最值),既保留长期趋势又大幅压缩体积。再叠加列式压缩,冷块通常能压到原体积的零头。这三者一起,让"留三年数据" 在经济上变得可行。
消息总线:解耦与背压。 采集端和落库端如果直连,任何一端的抖动都会拖垮另一端——设备一波突发会瞬间打爆数据库,数据库一次慢查询又会把采集线程堵住。中间插一条 消息总线(message bus)(RabbitMQ、Kafka、MQTT broker 等)就把两端解耦:采集端只管发,落库端按自己的节奏消费。这同时引出* *背压(backpressure):当下游消费跟不上时,消息在队列里堆积而不是丢弃,并通过预取限流(prefetch)**、消费者并发、死信与 TTL 等手段控制风险——堆积有上限、超时进死信、消费可水平扩展。总线让管线在流量尖峰下"弹性吸收"而非"硬碰硬"。
批处理 vs 流处理:两种计算姿势。 同一份时序数据,有两种算法:
- 批处理(batch):攒一批再一次性算/写。吞吐高、单位开销低,但有延迟——适合"每小时出报表""每天算趋势" 这类对时延不敏感的场景,也适合写入侧的批量落库以摊薄 I/O 成本。
- 流处理(stream):数据一到就算。延迟低,能在值落库的同时(甚至之前)就完成告警判定、滑动窗口聚合——适合"温度超限立刻报警"" 实时大屏"这类要求秒级响应的场景。
二者不是二选一,而是同一管线里的两条岔路:热路径走流处理保实时,冷路径走批处理保吞吐。实践中常见" 写入用批量摊薄、告警用流式即时"的组合。
工程要点
把上面的范式落到生产,有几条经验值得记住:
- 写入永远是第一约束。时序系统的容量先看"每秒能写多少点"。批量写入、合适的分块大小、避免写入时维护重索引,都是为写吞吐让路。读优化排第二。
- 时间戳要分清"采集时刻"与"落库时刻"。设备采到值的时间和它被写进库的时间是两回事,二者之差就是管线延迟。把两个时间都存下来,既能正确排序,又能用来监控链路时延。
- 聚合要警惕可空值。时序表里常混有字符串型、JSON 型的非数值载荷,对应的数值列为空。做
AVG/SUM/MAX时若不显式过滤空值,结果会被悄悄带偏——这是个反复踩的坑。 - 冷热分层要尽早规划。压缩与保留策略最好在建表时就定,等数据涨到亿级再补救会很被动。压缩后的块通常只读,要确认查询路径仍然可用。
- 背压参数要按负载调。预取数、消费者并发、队列 TTL 这些不是拍脑袋的常数:预取太大占内存、太小压不满吞吐;并发太高争数据库连接、太低消费不过来。先用保守默认值,再按实测流量调。
- 死信不是垃圾桶。超时、格式错误、反复处理失败的消息进死信队列后要有人看、有据可查,而不是任其堆积或静默丢弃。
在 IoT DC3 中如何落地
IoT DC3 的数据平面就是上面这条通用管线的一个具体实现。一条位号值从设备到可查,正好走过" 采集 → 消息总线 → 消费落库 → 缓存/查询"四段:
消息总线:RabbitMQ 异步投递 + 7 天 TTL + 死信。 驱动采到的位号值不直写数据库,而是发往 RabbitMQ 的 topic 交换机 dc3.e.value,数据中心 dc3-center-data 的持久队列 dc3.q.value.point 以通配 dc3.r.value.point.* 收下全部驱动的值。这条队列声明了 7 天 TTL(604800000 ms)并挂了死信交换机 dc3.e.point_value_dead:消息在队列里最多停 7 天,超时或被 reject 即进死信,不会静默丢失。这正是"消息总线解耦 + 背压 + 死信兜底"在 DC3 的落地。
消费者并发是默认档,非高吞吐档
位号队列的消费者 PointValueReceiver 没有显式指定 containerFactory,因此跑在默认监听容器工厂上: concurrentConsumers=2、maxConcurrentConsumers=8、prefetchCount=10、手动 ack。RabbitConfig 另外提供了一个高吞吐工厂 highThroughputRabbitListenerContainerFactory(concurrent=4、max=32、prefetch=100),但当前没有监听器 opt-in ——高吞吐工厂存在,默认未启用。高吞吐工厂需要时给 @RabbitListener 显式加 containerFactory。
批量落库:按入站速率二选一。 数据中心消费时不是无脑逐条写库。它按入站速率分流——速率低于 POINT_BATCH_SPEED(默认 100 )时即时落库;超过阈值时改交 Quartz 定时任务批处理,由 POINT_BATCH_INTERVAL(默认 5,单位秒)参与速率计算。这正是" 写入侧用批处理摊薄 I/O、低峰时用即时写保实时"的工程取舍。
时序存储:TimescaleDB 超表 dc3_point_value。 位号值落在 TimescaleDB 超表 dc3_point_value(位于 dc3_history schema)。它实践了上文的分区思想——按时间维 create_time 每 1 天一个 chunk、按设备维 device_id 16 个哈希桶 双维分区;并配了两条数据生命周期策略:7 天前的 chunk 自动列式压缩、超过 180 天的数据自动清理 。冷热分层、压缩、保留,在这里都是开箱即用的硬事实。
num_value 可空:聚合查询必须 num_value IS NOT NULL
dc3_point_value.num_value(DOUBLE PRECISION)对非数值或 JSON 载荷为 NULL。任何 AVG/SUM/MAX/MIN 聚合都必须 加 WHERE num_value IS NOT NULL,否则字符串型位号的空值会混进来、把结果带偏;超表上还有一条只覆盖 num_value IS NOT NULL 的部分索引,不加这个谓词连索引都会错过。这正是"工程要点"里那条"聚合警惕可空值"在 DC3 的具体体现。
最新值缓存:Caffeine 热路径。 写路径在落库的同时把最新值塞进本地 Caffeine 最新值缓存;读最新值(point_value/latest )时先吃这层缓存、未命中再回源 TimescaleDB 补齐。历史区间查询(point_value/list)则不走缓存、直接扫超表。这就是" 近期热、远期冷"在读路径上的落地:热点最新值走内存缓存,冷数据走时序库的时间范围扫描。
链路的可靠性由三处叠加保证:消息发布前被统一标 PERSISTENT(配合 durable 队列扛 broker 重启)、消费者手动 ack(成功才确认、异常重回队列、校验失败进死信)、publisher confirms(确认回调追踪投递)。值一旦落库,还会同步 交告警引擎评估——这是流处理"数据一到即算"在 DC3 的体现。完整链路、模型变换与读接口示例见数据平面。
延伸阅读
- 边缘与云架构 — 这条管线的上游:采集在边缘还是在云、驱动如何下沉
- 数据智能与 AIoT — 这条管线的下游:落库后的数据如何被分析与大模型消费
- 物联网技术总览 — 四层参考架构全景与 DC3 的逐层映射
- 数据平面 — DC3 里一条位号值从设备落库的完整链路与硬约束
- 服务与拓扑 — 数据中心、消息总线、时序库在 DC3 服务图里的位置