ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

边缘时序数据库sfsDb:弱网环境下的本地存储与同步方案

边缘时序数据库sfsDb:弱网环境下的本地存储与同步方案

在做某制造车间的设备数据接入时,我遇到了一个非常头疼的问题:现场上了快三百个传感器,采集PLC电流、振动脉冲、能耗和温湿度,数据在本地网关上都好好的,一旦要求“先全部回传到中心再分析”,整个链路就变得又慢又脆。带宽撑不住,断网期间数据全丢,中心恢复后回溯还要靠人工补录。那段时间我意识到,边缘侧的时序数据处理不能再依赖“中心大数据库”,边缘自己就得有一块能存、能查、能同步的“本地数据底盘”。sfsDb就是在这个背景下折腾出来的一个示例项目,目标是做边缘计算时代的轻量级时序数据新选择:设备数据先在边缘落地,提供基础存储、聚合查询、断点续传能力,再按策略与中心库同步。

这篇文章会把sfsDb从设计动机、数据模型、写入链路、存储压缩、查询接口到弱网同步的完整思路展开,后面还会晒一份实测数据和几个让我折腾了大半夜的坑。如果你正在做边缘网关、工业数据采集、能源站本地监控这类事情,正在犹豫到底要不要在边缘侧放一个数据库,或者已经放了但不知道分寸怎么把握,这篇文章应该能给你一些有用的参考。

1. 从中心汇聚到边缘留存:时序数据为什么需要一个新落点

1.1 中心汇聚方案的三个现实瓶颈

先说我的切身体会。车间网关采集PLC和传感器数据,逻辑上非常简单:数据先规约解析,然后通过消息队列或者HTTP POST推给中心平台,由中心时序库统一落库和展示。这个模式在理想网络下没问题,但现实情况是:

第一,带宽撑不住。三百个传感器,每秒一个点,每个点带上设备ID、指标名、时间戳和浮点值,折算下来每秒大约几KB。看着不大,但加上高频采集的几个通道(振动、瞬时功率)后,峰值能到几十KB每秒。一天下来就是几GB原始数据,全链路传回中心,光流量成本就是一笔不小的开销。

第二,时延没法接受。预测性维护要求在设备异常后十几秒内报警,如果数据要先经过边缘、再进广域网、再到中心数据库、再被分析任务扫描一遍,一个来回少说几十秒,等中心算出“轴温异常”的时候,设备可能已经开始降载运行了。

第三,断网即黑盒。车间网络抖动、改造停电、光纤被施工挖断,都是常态。中心拿不到数据,本地也没留存,问题回溯只能靠设备自带日志和Excel补录,效率和准确性都很差。

这三条,尤其是第三条,让我下定决心做一个边缘侧的轻量时序数据组件,替代“只做转发、不落本地”的老方案。

1.2 sfsDb的定位:不是小号中心库,而是边缘侧数据管家

sfsDb的设计目标非常明确:它不是一个缩小版的中心时序数据库,而是边缘侧的本地数据管家。它要做的事情只有三件:

一是把设备数据稳定地落在本地磁盘,确保断网期间数据不丢;二是在本地提供时间范围查询和窗口聚合,让边缘应用能直接拿到统计结果;三是在网络恢复后,按批次把本地数据或聚合结果同步到中心库。

它刻意不去做什么?不做分布式集群,不做完整SQL语法,不做跨节点容灾。边缘盒子就是一台低配ARM或者小x86,供电都可能不稳定,需要的是一个能在一两百MB内存里跑起来的“数据库轻骑兵”,而不是一套需要运维专家伺候的数据库系统。

这种“范围克制”很重要。如果你一开始就想把所有中心库的能力塞进去,项目就很容易变成“边缘部署了一套重型数据库”,后续维护成本会吞掉所有收益。

1.3 那些不适合用sfsDb的场景(提前划边界)

也要讲讲反过来的边界。sfsDb不适合以下场景:几十万条时间线的超大规模物联网平台;需要复杂JOIN和子查询的时序业务分析;多个边缘节点之间要求强一致数据同步的场景。这些需求出现时,老老实实把数据汇到中心用成熟的分布式时序方案,边缘侧最多做一个轻量缓冲。

一句话总结我的判断:边缘时序数据库的价值点,不在“多”而在“近”——离数据源头近,离业务现场近,离断网风险近。

2. 数据骨架与写入路径:从设备协议到落盘的全过程

2.1 数据模型:measurement + tags + fields + timestamp

时序数据本质上是一条条不断追加的数值记录。sfsDb的数据模型沿用了这类数据库比较成熟的做法,用四要素定义一条记录:

  • measurement:逻辑上的指标组,相当于表名,比如machine_current、machine_vibration;
  • tags:标签维度,用于描述这条数据的归属和来源属性,比如device_id=3、position=roll_motor,它们通常是有序、不重复的键值对;
  • fields:具体的数值字段,比如current=12.5,支持整数、浮点数、布尔值和字符串;
  • timestamp:数据点发生时间,sfsDb默认使用毫秒精度。

一个经验是:tag在设计时要克制。tag的值会被放入索引并参与哈希计算,tag数量越多,内存索引膨胀越快。如果你把变化频繁的数值也塞进tag,那几乎是灾难现场。tag只放那些长期稳定、可能作为过滤条件的维度,比如设备编号、位置、线体编号。

2.2 写入链路的五个环节

sfsDb本身不直接处理设备协议,它提供一套HTTP接口和Unix Socket接口,让边缘网关把Modbus、MQTT、OPC UA等协议解析后的数据翻译成统一JSON或二进制格式再写入。写入链路完整走一遍,大概是这样:

  1. 协议解析层收到一批设备数据,完成格式转换和时间戳标准化;
  2. 数据进入批缓冲,按measurement+tags按序聚合;
  3. 先追加写WAL段,保证ACK时数据已经可靠落盘;
  4. 更新内存中的时间线索引和当前数据块;
  5. 缓冲达到阈值(比如128MB或5秒),触发落盘压缩,生成不可变数据文件。

第3步是边缘场景最容易忽略的。很多设备盒子突然断电,导致明明返回了“写入成功”的数据丢了。sfsDb的处理原则是:先写WAL再响应客户端,宁可写路径慢一点,也不能丢已确认的数据。

2.3 采用LSM风格写路径的原因

有朋友问我,边缘设备数据量不算特别大,为什么不直接用传统的B-Tree存储?我的回答是,时序数据的写入模式和B-Tree的强项正好拧着。

B-Tree适合随机读写均衡的场景,而时序数据99%都是按时间顺序追加,写入完全集中在文件尾部。如果用B-Tree,每一条点都可能触发页分裂和随机IO,SSD寿命和写放大都会受影响。相反,LSM风格的追加写、批量合并能很好匹配物联网数据流的特性。

当然LSM也有代价,最典型的是压缩合并期间会占用CPU和磁盘IO,查询可能会看到数据逻辑上存在但暂时被合并线程占用。这个问题我后面专门讲,属于边缘设备上最容易翻车的点。

3. 存储引擎的选择:列式分桶、压缩编码与自动清理

3.1 按时间分桶:查询与过期删除的最小代价

sfsDb的存储目录按照“时间桶”组织,通常一小时一个桶,目录内再按measurement和tags分组文件。这样做有几个立竿见影的好处。

查询时,任何时间范围扫描都能先定位到少数几个桶,避免全表扫描;删除过期数据时,直接整个桶目录删除,不需要逐条delete,这在磁盘空间紧张的边缘盒子上非常友好。举个例子,保留策略配置为“7天”,存储引擎每天凌晨把8天前的桶目录整个rm掉,空间立刻释放,IO开销几乎可以忽略。

分桶粒度也不是越细越好。太细会导致文件数量爆炸,太多小文件反而拉低查询效率;太粗则过期清理不够灵活。实测下来,工业场景一小时一桶是比较均衡的选择。

3.2 时间戳和数值的压缩编码

sfsDb在压缩上重点处理两个对象:时间戳和数值字段。

时间戳压缩采用“二阶差分”(delta-of-delta)方案。设备采集通常是固定周期,时间戳序列的差值往往是一个常数,二阶差分后大量值会变成0或者很小的数,再用变长整数编码,几乎可以做到每个时间戳只占1到2个字节。对比直接存储8字节时间戳,压缩收益非常可观。

数值压缩对浮点数采用异或编码,对整数采用差值编码。锯齿形变长编码可以处理正负交错的整数,工业中的计数器、累加值这类数据压缩比通常很高。

需要提醒的是,压缩算法都存在“最坏情况”。如果振动信号是高频完全随机噪声,异或编码的压缩比会很差。遇到这种场景,最好在采集侧就同步做特征抽取,原始波形干脆切块归档,而不是硬塞进时序数据库。

3.3 保留策略与合并任务

边缘盒子的存储空间不像服务器那么充裕,所以sfsDb把保留策略做成了一等公民。配置项包括“最多保留天数”和“最多占用磁盘空间”,两者同时生效:超过天数先触发过期桶删除,桶删除后空间仍不够,就对最旧的数据做降采样压缩,保留聚合结果并丢弃原始点。

为了避免存储文件碎片化,后台会有一个合并线程定期把小文件合并成较大的压缩段文件。合并操作和WAL刷盘、查询线程共享同一份CPU预算,必须做限速,否则在线查询会被拖垮。这一块的具体调优放在第7节细讲。

4. 查询与预聚合:边缘侧就能拿到可用的统计结果

4.1 轻量查询接口的设计

边缘应用对查询的需求和中心BI分析很不一样。现场看板、报警规则、边缘控制逻辑,需要的查询类型就那么几种:按设备过滤、按时间范围扫描、按固定窗口聚合。因此sfsDb没有引入完整SQL引擎,而是提供了一组精简的查询API:

  • 原始点查询:指定measurement、tags和时间范围,返回原始记录;
  • 窗口聚合查询:指定聚合函数和时间间隔,返回每个窗口的统计结果;
  • 最新值查询:每个时间线返回最新一个点,用于现场监控大屏。

为什么不引入完整SQL?一是体积太大,二是边缘场景根本用不到复杂关联查询,三是没必要的解析和优化开销会吃掉宝贵的CPU。做一个“刚好够用”的查询接口,维护成本和出bug的概率都低得多。

4.2 时间窗口聚合的计算细节

时间窗口聚合是边缘查询最核心的能力。sfsDb支持的聚合函数包括mean、sum、min、max、count、rate、delta,以及百分位近似值percentile。

窗口对齐是一个容易被忽视的细节。比如聚合间隔设为1分钟,窗口边界是按数据产生时刻对齐(整分钟对齐),还是按服务启动时刻对齐?sfsDb默认按整点对齐,这样边缘和中心分别算同一个窗口时,结果可以对应上。

聚合计算过程中,内存只保存当前窗口部分的状态值,比如计算均值只需要累计和与计数,不需要把所有原始点都留在内存里。这样就算一个窗口内有几十万个点,内存占用也能保持恒定。

4.3 数据下推:让云端只收结论

我在这个项目里体会最深的一点是:边缘侧计算的最大价值不是“算得快”,而是“少传数据”。

假设一台设备一天产生86,400个原始点。如果全部回传中心,每天都要占用一次几十MB的传输。但在边缘侧按5分钟窗口预先聚合成均值、极值,一天只需要288个统计点。对于只看趋势和异常告警的远程监控场景,这两者的业务价值几乎一样,传输成本却差了两个数量级。

所以sfsDb在同步模块里专门支持“只同步聚合结果”模式。边缘节点本地保留完整原始点用于短期回溯,回传中心的只是降采样后的统计序列。这等于把“数据变现”的动作提前到了数据源头。

5. 弱网下的同步:断点续传与数据最终一致

5.1 批同步而非实时流

边缘节点与中心库之间,网络不可能像数据中心内部那样可靠。sfsDb的同步策略从一开始就放弃了“实时流式推送”的思路,改走批同步。

批同步的核心逻辑是:本地数据先按时间桶完整落盘,同步服务只负责把已经落盘的、尚未发送到中心的数据块批量传输。网络空闲时段(比如凌晨)或者积压数据超过阈值时触发同步。这样设计的好处很明显:不依赖长连接,不会因为连接中间断了就丢数据,中心端的写入压力也平坦。

我曾经试过用实时流方式同步,结果网络抖动一次,消息队列积压,数据顺序全乱,处理逻辑复杂了好几倍。后来彻底改成批同步,一切都清爽了。

5.2 数据块编号、水位线与幂等重传

为了让断点续传真正可靠,sfsDb给每个同步数据块都分配了一个单调递增的块编号,同步服务记录每个目标中心库对应的“已确认水位线”,也就是最高成功写入中心的块编号。下一次同步直接从水位线之后的块开始重传。

这里有一个关键细节:重传必须是幂等的。中心库侧会根据数据块的ID和首尾时间戳做一个去重判断,如果同一块数据重复到达,直接丢弃重复内容。否则一旦确认机制出现超时重传,中心库就会出现重复数据。

实际部署中,我建议边缘节点至少保留3天的已确认块元数据,不要同步完成后立刻删除,否则中心侧如果要回溯校验某段时间的完整性,本地已经没有依据可查了。

5.3 时钟偏移与重复数据的处理

时序数据最容易出现的一种脏数据是“时间戳错乱”。边缘设备时钟不准是常态,如果设备本地时间比真实时间快了5分钟,上报数据的时间戳全错,同步中心后所有按时间对齐的分析都会乱掉。

sfsDb的应对策略是:数据写入时保留两个时间字段,一个来自设备自身的采集时间,一个记录网关接收时间。中心订阅方可以根据网关接收时间判断设备时钟偏移,也可以按需校正。同步模块还会做一次粗粒度的乱序整理,同一个同步批次内部先按时间戳排序,再分批传输,减少中心端乱序写入压力。

6. 实测数据与边界:性能、资源占用和可信区间

6.1 测试环境

为了搞清楚sfsDb到底能在什么硬件上跑,我在两类设备上做了压测:一块ARM架构开发板,4核1.8GHz、2GB内存;一台低功耗x86小主机,2核4GB内存。存储都是eMMC/SSD,模拟生产环境里常见的边缘盒子形态。

压测场景按车间规模设置:模拟200台设备,每台每秒产生10个数据点,总写入约2000点/秒,连续灌数据72小时。查询测试则在灌数据结束后,随机发起7天范围的时间窗口聚合查询。

6.2 写入、压缩、查询、同步的实测结果

结果整理成下面这张表,数据来自我自己的测试环境,未必代表所有硬件表现,但能反映一个基本量级:

项目实测结果说明
单实例写入能力批量写入稳定到3万点/秒以上前提是网关侧做好数据批量组织,单条HTTP请求带一批数据点
内存占用稳态运行低于500MB时间线索引数量约5万条
压缩比工业参数场景约5到10倍变化越平稳压缩越好,随机噪声场景会显著下降
聚合查询延迟覆盖7天、约1亿点,秒级返回走时间桶裁剪和窗口聚合路径
断点续传积压1GB数据,恢复网络后2小时内传完受带宽限制,传输本身开销很低

最让我意外的是ARM开发板上的表现。之前我担心小A卡扛不住压缩合并和查询同时跑,实际上把合并线程限速调到合理区间后,负载一直很平稳。

6.3 边界与可信区间

任何性能数字都有前提,sfsDb的适用边界也很清楚:日常工业采集、能耗监控、环境监测这类低频采样场景,它能做得很舒服;但面对每通道1kHz以上的高频振动信号,3万点/秒的写入能力就不够看了,高频场景更适合用归档文件+特征抽取的方式处理。

另一个边界是时间线规模。五万条时间线内存占用不到500MB,听着不错,但如果你被忽悠着把设备所有可能变化的值都建成独立时间线,几十万条甚至上百万条时间线会把内存吃光。这个问题的根源不是数据库不优秀,而是设计阶段tag和field划分不当。

7. 踩坑与调优:边缘时序数据库最容易翻车的几个细节

7.1 WAL刷盘频率与写入抖动

最初实现WAL时,我图省事,每收到一条HTTP请求就刷一次盘。结果是请求耗时直接翻了几倍,写入性能惨不忍睹。后来优化成“每攒够1毫秒或128条数据,再组批刷盘一次”,性能立刻恢复正常,丢数据窗口也只有毫秒级,对边缘采集场景完全可以接受。

这个坑的根源在于“可靠性和性能的平衡点”。很多工程师一看到丢数据就慌,把刷盘频率拉到最猛,结果服务本身先被拖垮了,反而更容易在异常情况下丢更多数据。正确的思路是:定义清楚业务可接受的最大丢数据窗口,再反推刷盘策略。

7.2 合并线程干扰在线查询

存储引擎的合并任务会把多个小文件压缩成一个大文件。在低配机器上,如果合并线程抢占了CPU,在线查询的延迟会显著上升。我遇到过聚合查询从300毫秒暴涨到3秒的情况,排查半天才发现是合并线程正在全速跑。

解决办法是在合并线程里加入自适应的限速逻辑:当最近1分钟内查询平均延迟超过阈值,合并线程自动降低工作频率;当系统空闲时,合并且全速执行。加了这个逻辑之后,在线查询的P99延迟基本稳定了。

7.3 高精度时间戳带来的压缩灾难

项目测试阶段收到过一批设备数据,压缩比奇差无比,单个数据块膨胀到原来的两倍。查来查去发现是设备上报了微秒级时间戳,而后端存储逻辑忽略精度差异,直接用原始微秒值做二阶差分,差分值的波动幅度极大,压缩算法几乎失效。

解决方式很粗暴:写入入口统一把时间戳标准化为毫秒精度,多余的微秒偏移单独存一个扩展字段。这样既保留了精度信息,又不会破坏主链路压缩效率。如果你在接入设备数据时发现压缩比异常,先看看是不是时间戳精度不统一。

7.4 重复上报与乱序到达的处理

设备重启后重新连网,很容易把之前缓存的旧数据重新上报一遍。sfsDb早期版本遇到重复上报,会直接当成新数据写入,导致查询结果出现重复点。后来在写入层增加了一个轻量去重逻辑:同一时间线、同一时间戳,只保留最后一次写入的值。

乱序到达数据则分两种情况处理:迟到时间窗口在5分钟以内的,直接合并进当前缓冲块;超过这个范围的,落到单独的乱序文件。这样做的原因是乱序数据如果随便插进不可变文件,会破坏压缩文件和索引的一致性。这点在处理边缘设备弱网重连时非常关键。

8. 后面的路:让边缘时序数据真正沉淀为价值

8.1 计划中的边缘推理插件

sfsDb现在的角色更多是“数据的搬运工和临时仓库”,下一步我打算在它上面挂一个轻量推理插件,让边缘侧直接跑一些简单的异常检测模型,比如基于滑动窗口的离群点检测和基于残差的突变判断。模型在边缘算完,只把告警事件和统计特征回传中心,这样原始波形和最终结论都能各得其所。

8.2 更顺畅地与中心库双向桥接

目前sfsDb通过HTTP接口同步数据,基本能用,但生态还不够顺滑。后续想补一个标准化的中心数据桥接器,让边缘节点可以直接对接常见的数据管道协议。同时也打算支持从中心库下发配置参数到边缘,这样批量修改采集策略的时候,不需要一台一台去登盒子了。

8.3 关于这一轮实践的几点体会

做完sfsDb这一轮折腾,我最直接的感受是:边缘计算时代的时序数据问题,并不是把中心数据库搬小一点就能解决的,它需要针对“弱网、低配、无人值守”这三个关键词重新设计。先解决能存、能查、能传回来的问题,再考虑分析和智能,路径会走得稳很多。

如果你也在设计边缘侧的数据底座,我最后的建议是:不要把功能堆得太满。边缘场景的需求是分层的,先把数据留在本地,把查询做快,把同步做可靠,就已经解决了八成以上的实际问题。至于那些听起来很酷的分布式、完整SQL、自动容灾,等业务真正需要的时候再加也不迟。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进