我做了三年多的物联网落地项目,从几十个节点的原型验证,到几千台设备的生产环境,一个感受特别深:硬件成本、流量成本都能靠架构设计抠出来,但存储成本往往是最容易被忽视、又最容易失控的一项。尤其是当你的设备上了量、采了几个月数据之后,数据库账单会以肉眼可见的速度膨胀。我在好几个项目里都遇到过这种状况——数据调取开始变慢、存储费用翻倍、甚至因为磁盘不够被迫清库。
后来我把精力放到数据本身的“压缩”上,配合边缘网关做预处理,效果立竿见影:同一批数据,存储占用降了 70% 以上,查询响应反而快了接近一倍。这篇博文我就想把这套思路完整拆开,从物联网数据的特征分析、算法选型,到端侧与云端的具体实现,再到实测效果和踩坑记录,一次性讲清楚。
这套内容比较适合正在做物联网项目的开发者、做物联网毕业设计的同学,以及被存储成本困扰的运维和架构师。哪怕你没接触过数据压缩,只要懂一点编程基础,按下面的步骤跟下来也能上手。
1. 项目概述:被存储成本逼出来的压缩方案
1.1 一个让我开始折腾压缩算法的真实场景
先说一个让我印象很深的项目。当时我负责一套环境监测系统,设备端用的是 ESP32S3,采集温湿度、PM2.5、电池电压和信号强度这几个量,每 30 秒上报一次。单个节点一天大约产生 2880 条记录,看起来不算大。但部署到 500 个点位之后,一天就是 144 万条记录。那时候我图省事,直接把 JSON 数据原封不动存进了时序数据库,用的还是默认配置。
三个月后一查,存储占用接近 800GB,而且还在以每天好几个 GB 的速度涨。数据库查询开始出现明显的延迟,有时候一个区域的历史曲线要转好几秒。最难受的是,大部分数据其实长得非常相似——同一片区域的温度在几分钟内可能一直稳定在 27.4℃ 和 27.6℃ 之间波动,湿度变化也不大。也就是说,我们花大量存储空间存的,其实是一堆高度重复的数字。
当时我就在想,如果能在数据入库之前做一层压缩,把这些重复信息榨干,是不是存储成本就能大幅降下来?这个念头推动我开始系统研究物联网数据压缩算法,也才有了后面的整套方案。
1.2 物联网数据的特点决定了压缩的收益空间
很多人一听到“压缩”就想到 ZIP、RAR,觉得那是文件压缩的事,跟数据库存储没什么关系。其实数据压缩在物联网场景下的收益,比大家想象中大得多。核心原因在于物联网数据有几个非常明显的特征:
第一,时间序列特性强。绝大多数物联网数据都是按固定间隔采集的时序数据,相邻两条数据在业务含义上往往只差一个采样周期。温度不会忽然从 20℃ 跳到 80℃,电压也不会在一秒内骤变。这个连续性意味着相邻数据的差值很小,非常适合用差分类算法处理。
第二,数据规模增长极快。设备数量一旦过了千级,哪怕每条记录只有几十字节,日积月累也是个恐怖的数字。如果每条设备每分钟上报 10 个字段、每个字段按 4 字节算,一万台设备一天的数据量就能轻松超过 1 亿字节,一年下来就是 40GB 起步。这还只是估算,实际上加上时间戳、设备 ID、协议开销,翻几倍很正常。
第三,周期性规律明显。工业设备、环境监测、农业大棚这类场景,数据的波动往往跟时间周期强相关。比如白天气温高、夜间气温低,这几个大周期叠加在一起,导致数据在更大时间尺度上也存在可预测性。好的压缩算法能识别出这种结构,进一步压低存储空间。
正是这三个特征,让物联网数据成为数据压缩算法的理想用武之地。通用压缩算法虽然也能用,但针对时序特征的专用方案,往往能获得更高的压缩比。
2. 数据特征分析与算法选型思路
2.1 先搞清楚数据长什么样,再谈压缩
在选算法之前,我强烈建议你先做一件事:把设备上报的原始数据真实导出一批出来,逐字段分析它的类型、取值范围和变化规律。我看到很多项目一上来就套用某个压缩算法,结果效果不好,问题往往就出在没吃透数据特征。
以我那个环境监测项目为例,原始上报的数据结构大概是这样的:
- 设备 ID:普通字符串,看起来像
ESP32S3-0001、ESP32S3-0002,特点是前缀完全相同,只有末尾几位在变。 - 时间戳:Unix 毫秒级整数,单调递增,相邻两条之间基本固定相差 30000 毫秒。
- 温度:浮点数,保留 1 位小数,取值范围在 -20 到 60 之间。
- 湿度:浮点数,保留 1 位小数,取值范围在 0 到 100 之间。
- 电池电压:浮点数,保留 2 位小数,取值范围在 3.2 到 4.2 之间。
- 信号强度:整数,通常在 -100 到 -30 之间。
把这些字段列出来后,压缩思路就清晰了一半。比如设备 ID 明显有大量重复前缀,做字典编码就很合算;时间戳相邻差值稳定,做差分编码几乎能榨干冗余;温度和湿度是缓慢变化的小数,整体做一次缩放转成整数,再做差分,压缩效果会非常可观。
2.2 通用压缩算法与物联网场景的匹配度
接下来聊聊算法选型。很多工程师第一反应是用 gzip,毕竟它太普及了,随手就能调库。我自己也试过,gzip 对 JSON 文本的压缩率不错,能把 10KB 压到 1KB 左右,但它是通用算法,没有利用时序数据的任何结构特征。更重要的是,gzip 压缩时需要维护较大的字典窗口,对 CPU 和内存都有一定开销,在低功耗嵌入式设备上跑起来并不划算。
我整理过一张常用压缩算法跟物联网场景的匹配度对比,实际参考价值比较高:
| 算法 | 压缩比 | 压缩速度 | 解压速度 | CPU 开销 | 物联网适配性 |
|---|---|---|---|---|---|
| gzip | 高 | 慢 | 中 | 高 | 适合冷数据归档,不适合端侧实时压缩 |
| LZ4 | 中 | 极快 | 极快 | 低 | 适合网关或端侧,兼顾速度和压缩比 |
| Snappy | 中 | 极快 | 极快 | 低 | 适合数据管道,Google 系协议常用 |
| Zstandard | 高 | 快 | 快 | 中 | 性价比很高,可调压缩级别 |
| delta+varint | 很高(时序专用) | 极快 | 极快 | 极低 | 适合数值型时序数据,强烈推荐 |
从这张表能看出,通用算法追求的是对任意数据都能压,而物联网场景更多是压那些有规律的数字序列。因此在端侧或者网关位置,我更倾向于用轻量级方案,把“通用压缩”和“时序专用处理”结合起来,而不是直接上一个 gzip 了事。
2.3 时序数据专用压缩思路:delta 编码和 Gorilla 方案的启示
这个领域有一篇绕不开的论文,就是 Facebook 提出的 Gorilla 时序数据压缩算法,后来也演化成了我常用的思路。它的核心不在于发明什么惊天动地的数学原理,而是非常聪明地抓住了时序数据中“相邻数据变化极小”这个事实。
先说最简单的 delta 编码。对于时间戳序列,比如1700000000000、1700000030000、1700000060000,如果直接存原始值,每个要占 8 字节。但如果我们存的是差值,那就是30000、30000,第一个数需要 8 字节,后面的每个只需要几个字节就够了。更进一步,Gorilla 提出来的 delta-of-delta:对差值再做一次差值,因为采样间隔固定,第二步计算出来的值基本是 0。存一串 0 的成本几乎可以忽略不计。
再比如温度值27.4、27.5、27.5,先把小数缩放成整数274、275、275,再做一个一阶差分,得到274、1、0。后面这些小数用什么方式表示都行,关键是数据被极大地“规整”了,后续不管是继续做变长整数编码,还是进一步做霍夫曼编码,效果都会好很多。
这套思路本质上就是把“数据之间的相关性”显式地抽出来,而不是让压缩器自己从字节流里慢慢摸索规律。对物联网这种采样频率高、相邻数据波动小的场景,这是非常对路的方案。
3. 核心实现:一套开箱即用的压缩与存储方案
3.1 整体架构:端侧预处理 + 网关二次压缩 + 云端列式存储
在讲具体算法之前,先把整体链路定下来。我目前推荐并验证过的架构分三层:
第一层是端侧。在设备端拿到原始数据后,不直接发 JSON,而是先做一个轻量级的规整处理。包括丢弃冗余字段、将小数统一放大成整数、将时间戳转成相对偏移等。这层只做无损的变换,不做复杂的统计编码,因为设备端的计算资源有限。
第二层是边缘网关。设备数据发到网关后,由网关做二次压缩。网关的算力通常比设备强不少,可以运行 LZ4 或 Zstandard,甚至可以对一批数据做整体的差分和变长编码。多设备数据的聚合压缩,这层我认为是收益最大的地方,因为同一时间点附近的数据之间存在较大的横向相关性。
第三层是云端存储。收到网关上传的压缩数据后,云端负责解压、校验、入库。因为数据在传输前已经压过一遍,网络流量开销也同步降了下来。云端存储建议配合列式存储或时序数据库使用,这类系统本身对压缩友好,再叠加我们前两层的处理,效果可以拉满。
这三层说到底是一条流水线:规整、聚合、压缩、存储。每一层只做自己最擅长的事,避免让某一端承担过重任务。
3.2 端侧算法实现:差分编码 + 可变长整数编码
下面我用一个具体示例,演示端侧最常用的一套轻量压缩流程的实现。这里我用 Python 写伪代码,方便讲解思路,设备端建议用 C 或 Rust 实现同样的逻辑。
假设我们需要上报温度和湿度:
- 原始数据:
{"device":"ESP32S3-0001","ts":1700000000000,"temp":27.4,"hum":65.3} - 端侧变换前,先对数据排序并做一些规整:
# 伪代码:端侧数据规整与差分编码 def encode_batch(samples): # samples 是一个列表,每个元素是 (ts, temp, hum) # 第一步:把温度/湿度放大为整数,避免浮点存储 int_samples = [(ts, int(round(temp * 10)), int(round(hum * 10))) for ts, temp, hum in samples] # 第二步:对时间戳做一阶差分 ts_prev = int_samples[0][0] deltas = [ts_prev] # 第一个时间戳完整保留 temp_prev = int_samples[0][1] temp_deltas = [temp_prev] hum_prev = int_samples[0][2] hum_deltas = [hum_prev] for ts, temp, hum in int_samples[1:]: deltas.append(ts - ts_prev) ts_prev = ts temp_deltas.append(temp - temp_prev) temp_prev = temp hum_deltas.append(hum - hum_prev) hum_prev = hum return deltas, temp_deltas, hum_deltas差分编码做好后,还需要把整数序列压缩成尽可能少的字节。常见的手段是可变长整数编码,也就是所谓的 varint。原理简单说就是:数值小的时候,用一个字节就能表示,而不是固定用 4 字节或 8 字节。
def varint_encode(value): # 将非负整数编码为变长字节序列 value = value & 0xFFFFFFFF out = bytearray() while True: b = value & 0x7F value >>= 7 if value: out.append(b | 0x80) else: out.append(b) break return bytes(out)负数怎么处理?可以对原始值做一次 zigzag 变换,把负数映射成正数,比如-1 -> 1、1 -> 2、-2 -> 3,这样所有数都能用 varint 统一编码。
实测下来,这套“差分 + varint”对温湿度这类缓慢变化的数据,平均每条记录能压到 2-4 字节。如果原始 JSON 一条要 100+ 字节,这已经是几十倍的差距。
3.3 计算参数与配置:采样间隔、量化精度怎么选
压缩效果好不好,很多时候在采集策略阶段就已经决定了。这里有几个参数我觉得可以重点把控:
- 采样间隔:间隔越稳定,差分编码的收益越大。很多设备因为网络抖动导致上报时间忽长忽短,差分会变得没什么规律。建议端侧做好时间戳对齐,尽量按固定间隔采集,实在有漂移就在网关层做插值或对齐处理。
- 量化精度:温度传感器标称精度可能是 0.1 摄氏度,那我们放大 10 倍就够了,没必要放大 100 倍。精度越高,差分的范围越大,反而压缩效果变差。这里要做一次“够用就好”的取舍。
- 批处理大小:端侧如果攒一批数据再压缩上报,压缩效果会明显优于单条处理。比如一次打包 100 条记录做差分,首条记录占用的“固定成本”被摊薄了。当然批处理会增加延迟,具体看业务容忍度。
- 字典替换:设备 ID 这类重复度高的字段,建议在网关层做一个简单的字典映射。我有个项目里,设备 ID 从字符串
ESP32S3-0001(大概 12 字节)映射成 uint16 整数(2 字节),光这一项整库体积就降了约 8%。
这些参数不是拍脑袋定的,建议根据真实数据做一轮离线测试,看看不同配置下压缩比的差异,再上线调整。
4. 实测效果、成本收益与问题排查实录
4.1 压缩效果对比实测
为了说明这套方案的实际价值,我把我环境监测项目里的真实数据做了一轮对比实验。实验对象是同一个时段、同一批 10 万条设备记录,分别采用几种不同方式存储:
| 存储方式 | 原始大小 | 压缩后大小 | 压缩比 | 备注 |
|---|---|---|---|---|
| 原始 JSON 文本 | 12,480 KB | - | 1.00 | 直接入库,最笨的做法 |
| JSON + LZ4 | 12,480 KB | 1,340 KB | 约 9.3:1 | 网关压缩,速度极快 |
| 规整后的二进制 + delta | 8,920 KB | 390 KB | 约 22.9:1 | 端侧规整 + 差分 |
| 规整后 + delta + Zstandard | 8,920 KB | 210 KB | 约 42.5:1 | 全链路叠加 |
需要说明的是,压缩比会受数据波动程度的影响:数据越稳定,压缩比越高;数据剧烈变化时,压缩比会相应下降。但即便在波动较大的工业场景,我用这套方案也至少能压到 10:1 以上。
从成本角度看,假设原始方案一年产生 100TB 的存储需求,按云厂商时序数据库每 GB 每月 0.2 元估算,一年存储费用约为 24 万元。如果把数据压到 1/10,存储费用直接降到 2.4 万元;再配合冷热分层,把超过 30 天的数据转存到对象存储,费用还能再省一截。这笔账不论对创业团队还是大厂项目,都不是小数。
4.2 常见问题速查表
在实际落地过程中,我整理了下面这一张高频问题表,基本都是踩过坑之后才总结出来的:
| 问题 | 可能原因 | 处理思路 |
|---|---|---|
| 压缩率突然从 20:1 掉到 4:1 | 采样间隔不稳定,或数据里混入了异常值 | 检查原始数据是否有大量缺失,补数逻辑是否引入了重复填值 |
| 端侧压缩导致上报延迟明显增加 | 批处理窗口设得太大 | 调小批处理条数,或改成按时间窗口触发压缩 |
| 解压后的数据跟原始值对不上 | 浮点转整数时精度丢失 | 放大倍数必须统一,解压侧用同样的倍数还原 |
| CPU 占用过高 | 在低功耗设备上跑 Zstandard/gzip | 端侧只做差分和 varint,把重压缩放到网关 |
| 设备 ID 字典越来越大 | 新增设备数量过多,字典频繁重建 | 设计好字典分片或按项目ID隔离,不要全局共用一个字典 |
| 查询变慢 | 批量解压导致查询线程阻塞 | 优先在写入侧压缩,查询时直接读列式存储,尽量不去运行时解压 |
4.3 排查思路与避坑技巧
最后分享几个我觉得特别值得注意的细节。
第一个是压缩必须和查询解耦。很多人把压缩做在查询侧,也就是存原始数据,等查询的时候再临时解压。这个做法在数据量不大时没毛病,但数据量超过几十 GB 之后,查询时的 CPU 开销和延迟会让人抓狂。我强烈建议把压缩做在写入侧,存进去的就是紧凑格式,查询侧直接读已压缩的列数据。像 TDengine、ClickHouse 这类系统本身就支持列级压缩,配合起来非常顺手。
第二个是不要盲目追求极致压缩比。压缩比高当然好,但如果为了压缩一个本来就只有 200 字节的小报文,而引入复杂的状态管理、字典同步、跨批次编码,一旦其中一个环节出错,整个数据链路都会变得脆弱。我看到过有项目为了压缩比把简单业务搞到难以维护,这是得不偿失的。一个务实的策略是:端侧做规整,网关做聚合压缩,冷数据再上高压缩比算法归档。把合适的算法放在合适的层,比单纯某一个地方压得狠要重要得多。
第三个是压缩方案要能回滚、能灰度。数据压缩一旦上线,存量数据和新数据的格式可能不一致。我在实践中会先在测试环境验证一轮,比对解压后的数据与原数据的一致性,再选择几个低风险设备灰度上线,确认无误后逐步放量。宁可上线慢一点,也不要出现解压后数据对不上的事故。
回到我自己经历的那个环境监测项目,上线这套压缩方案之后,存储占用从最高 800GB 降到了不到 80GB,查询从几秒降到了毫秒级,每月的存储账单也只剩下原来的一个零头。后面我又在另外两个项目里复用了这套思路,针对不同的业务场景做了少量参数调整,效果都很好。压缩这件事,本质上就是花一点计算资源,去换存储和带宽的成本。对物联网这种天生数据密集、设备分散、成本敏感的场景来说,这笔账怎么算都划算。如果你正被存储成本困扰,我建议别急着扩容或清库,先看看数据本身,把压缩这件事安排上。