最近一直在折腾物联网设备的数据接入,从传感器上报到云端存储,再到大屏实时刷新,看起来链路不长,但真正把数据稳定存下来、查得动,踩过的坑比想象中多得多。最开始的方案用的是 MySQL 硬扛,每隔几秒一批设备上报,表数据量涨到千万级以后,查询直接卡死,磁盘占用也失控,最后才下决心把存储层换成了专门的时序数据库,同时用 Go 语言把采集端整体重写了一遍。这套系统跑了几个月,接口稳定性和查询体验都有了质的提升,算是把“数据采集 + 存储”这条链路彻底理顺了。
这篇内容我决定完整梳理一遍当时的项目实践,从时序数据库的选型理由、Go 语言采集端的工程化写法,到写入模型、分区策略、降采样规则,以及实际遇到的坑和排查过程,都会拿出来讲。如果你也在做物联网数据接入、设备监控平台或者边缘采集网关,正准备选型或者优化存储架构,这篇文章应该能帮你少走不少弯路。
1. 项目启动:先想清楚要存什么、怎么查
动手写代码之前,我最先做的事不是选数据库,而是把数据特征列了一遍。这个习惯是从一个失败的项目里学来的——上一套系统没想清楚就上线,结果后期存储结构改得欲仙欲死。
1.1 物联网数据的核心特征
物联网场景下的数据,和传统业务数据有一个明显的差别:数据是“按时间批量产生的”,而且基本是追加写入,很少修改。比如一个温湿度传感器,每 10 秒上报一次,一天就是 8640 条记录,一个月将近 26 万条。如果现场有 500 个这样的传感器,单月就是 1.3 亿条。
把这层数据特征摊开看,大概有这几条:
- 写多读少:采集端持续在写,但查询往往是“偶尔看实时曲线”或者“排查某个时间段的异常”。
- 时间维度是核心:几乎所有查询都会带时间范围,要么看最近 5 分钟,要么看过去 24 小时。
- 数据冷热分明:刚写入的数据访问频繁,越老的数据访问越少,但不能直接删,得留作追溯。
- 批量上报:不是一条条来的,往往是一次网络请求带一批数据。
- 标签维度丰富:同一个设备有多个指标(温度、湿度、电量),不同设备属于不同分组,需要灵活的标签过滤。
理解这些特征之后,就能明白为什么不能用传统关系型数据库硬扛——它们的索引模型和存储结构,天生不是为这种“海量小写入 + 时间范围扫描”设计的。
1.2 传统关系型数据库的瓶颈在哪
举例来说,假设我把采集数据存在 MySQL 表里,表结构大概是:
sql复制CREATE TABLE sensor_data (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
device_id VARCHAR(32),
metric VARCHAR(16),
value DOUBLE,
ts TIMESTAMP,
KEY idx_device_ts (device_id, ts)
);
这套结构在初期没问题,设备少、数据量小,SQL 怎么写都行。但等到数据量到了亿级别,问题就出来了:
一是单表数据量过大,即使建了索引,B+ 树的层级增加,随机 IO 变多,按时间范围扫描的效率明显下降。二是写入瓶颈,MySQL 的每行写入都要走事务日志、更新索引,写入吞吐量上不去,采集端一多就会堆积。三是存储膨胀严重,关系型数据库的行存储格式对这种数值型时序数据不友好,即使把 float 压缩,整体占用仍然很夸张。
后来还试过分库分表,按天建表、按设备 hash 路由,确实解决了一部分问题,但代码复杂度上来了:采集端要自己做路由,查询端要做结果合并,跨天查询还要拆 SQL。这套方案只撑了不到半年,就被我放弃了。
1.3 为什么选择时序数据库
就在我纠结到底是“继续在 MySQL 上堆机器”还是“引入新组件”的时候,一个做工业物联网的朋友给我推荐了时序数据库。
时序数据库在架构设计上,几乎就是照着物联网数据特征来的:底层存储针对时间戳做了特殊优化,按时间分区、按列存储,配合专门的压缩算法,相似的数据能压得很小;写入模型也针对“批量、追加”场景做了优化,比传统数据库更适合高并发写入。
更关键的是它的降采样和自动清理能力,让我能把历史数据按精度做分级存储:原始数据只保留一段时间,更老的数据自动聚合成分钟级、小时级的数据,既满足追溯需求,又控制存储成本。
最终我选择基于 Go 语言的生态来搭建整套系统,这个稍后详细讲。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 整体架构与技术选型
整套系统的目标很明确:支持大量物联网设备实时上报数据,提供稳定的写入接口,同时支持 Web 端实时查询和历史数据回溯。基于这个目标,我把系统拆成了三层。
2.1 系统分层设计
我用一张逻辑图来同步最后方案:
采集层是部署在现场的传感器或网关,通过 MQTT 协议接入到消息中间件,避免设备直接跟存储层耦合。应用服务层是核心业务逻辑,负责从消息队列消费数据,做格式解析、数据清洗、单位换算,然后批量化写入时序数据库。存储层则是时序数据库集群,保存原始数据和降采样后的汇总数据。
选择 MQTT 做设备接入,主要考虑物联网场景里网络不稳定、设备功耗受限,MQTT 的轻量级、断线重连机制、QoS 分级是最成熟的方案。这一层单独拆出来,也是为了让设备端和存储端互不影响,即使数据库抖动,设备上报数据也能在消息队列里缓冲住。
2.2 Go 语言在采集服务中的优势
应用服务层我有两个选择:Java 或者 Go。Java 我熟,生态全;Go 那会儿只是粗略了解过语法,并用它写过一些内部工具。但认真评估后,我选择用 Go 重写采集服务。理由很直接。
首先,这个服务的核心是高并发网络 IO。设备数据经过 MQTT broker 转发,最终落到我的采集服务上,说白了就是一个高吞吐的消费者程序。Go 的 goroutine 模型天然适合这种场景,几千上万个连接同时在线时,Go 的调度器能做到轻量级切换,而传统的线程模型在这种规模下内存开销会大得多。
其次,部署简单。Go 编译出来就是一个二进制的可执行文件,扔到服务器上就能跑,没有 JVM 那一堆依赖和调优参数。对我们的运维节奏来说,少一个环节就少一堆麻烦。
还有一个被低估的点:Go 在数据采集场景的标准库支持很扎实,尤其是 encoding/json、net/http、database/sql 这些包,配合 goroutine 能很干净地写出高并发的采集逻辑,不需要引入全套重量级框架。
2.3 时序数据库选型对比
存储层我当时考察了三个主流选择:
Inf
