“ThingLink-IoT物联网平台”这个名字,最早是我们在一个智慧园区项目里被逼出来的产物。当时现场要接的设备又杂又多,协议五花八门,有走Modbus的老式电表,有只上报HTTP的摄像头补光灯,还有一批压根没联网的温湿度传感器要自己加网关。市面上的物联网平台试用了一圈,要么太重,部署一套要养好几台服务器;要么太封闭,底层数据模型改不动,想接入一个非标设备得排期等官方支持。后来大家拍板,干脆自己搭一个轻量的平台,统一设备接入、数据清洗、规则告警和可视化展示。这篇文章就把这个平台从零到一的设计过程、核心模块、关键代码和上线后踩过的坑完整记录一下,给同样想自建物联网平台的团队一个参考。
ThingLink-IoT平台(下文简称“TLI”)核心能力可以概括为三句话:让任何设备都能用一个统一协议接入,让接入的数据能自动被处理并触发业务动作,让所有设备状态和数据有一个地方可以统一查看和管理。文章适合正在做物联网平台选型、准备自研接入层,或者被设备接入问题困扰的开发者、架构师和项目负责人阅读。我会把平台架构拆成几个关键部分来讲,每个部分都会给出实际用过的配置、代码和排查思路。
1. 内容整体设计与思路拆解
1.1 为什么选自研而不是直接用开源平台
在决定自研之前,我们整理了一份需求清单,发现几个核心诉求是市面方案很难同时满足的。
第一是接入成本要低。项目中大量设备来自不同厂商,有的设备本身只支持私有TCP协议,有的虽然有MQTT能力但数据格式自定义得很离谱——有的把温度放在“data.temp”,有的放在“params.T”。如果依赖平台方的设备接入SDK,每个设备都要单独开发适配,成本很高。我们希望有一个“协议适配层”,把不同设备的数据在平台入口就统一成标准格式,后续所有业务模块只认这一种格式。
第二是规则处理要灵活。项目里的告警逻辑并不仅仅是“超阈值发短信”。还有“温度连续3次超过60度且湿度低于20%才报警”“白天和晚上采用不同阈值”“某两个设备状态同时异常时触发联动控制”这类复杂条件。通用物联网平台的规则引擎大多针对固定场景,想自由编排得看平台脸色。
第三是数据主权。园区客户明确要求原始数据必须存在自己内网,不能上公有云。这直接排除了很多纯SaaS方案。
综合这些原因,我们决定自研一个贴合业务、足够轻量的平台。现在回头看,这个决策是对的,但前提是团队里得有人对设备接入、消息中间件、数据存储这些技术都熟悉,否则项目很容易烂尾。
1.2 平台的三个核心设计原则
TLI从架构第一天起就定了三条原则,后面所有模块都是围绕它们展开的。
第一条原则是设备与业务解耦。设备接入层只负责把物理设备的数据收上来、转成标准物模型格式,然后发到消息总线。业务模块(告警、可视化、联动控制)只订阅自己关心的主题,所有设备对它们来说是透明的。这样新增设备类型时不需要改动业务代码,新增业务逻辑时也不需要动接入层。
第二条原则是数据优先落时序库。所有设备上报的数据在进入消息总线的同时,必须落一份到时序数据库。即使后续规则引擎没配好、告警漏发,原始数据还在,可以事后回溯。这是平台后面排查问题最大的底气。很多自研平台第一版没做这一步,出问题后想复盘发现数据丢了,非常被动。
第三条原则是一切状态可查。每个设备连接状态、最新上报值、上下线记录、配置版本都作为平台自身的“设备影子”状态保存,前端可以实时看到。这个设计在排查设备频繁掉线和“设备显示在线但不上报数据”这类问题上帮了大忙。
2. 接入层的设计与选型逻辑
2.1 接入协议选型:MQTT为主、HTTP为补充
接入层是整个平台最关键的部分。我们最终的协议策略是:默认支持MQTT,特殊情况用HTTP补充,边缘网关负责把非MQTT协议转成MQTT。
选MQTT当主力协议的原因很现实。首先它在弱网环境表现好,设备经常分布在园区各角落,网络质量不稳定,MQTT基于TCP长连接,配合心跳机制能在网络抖动后快速恢复。其次是它原生支持QoS1(至少一次)语义,能保证数据不丢,这对电表读数、环境监测这类场景非常重要。第三是它的主题订阅机制天然适合平台的消息分发架构,一个设备上报的数据,既可以落到存储服务,也可以同时被规则引擎消费,彼此不影响。
HTTP只用于两类场景。一类是非常老旧的设备,固件写死了HTTP上报,没法改;另一类是平台自身的服务端API,提供给前端查询状态用。设备上报和下发控制全部走MQTT。
MQTT Broker的选型这里要提一下。我们没选特别重的商业方案,直接用了开源社区常见的Broker实现,单机部署。实测下来,单台扛住几千个长连接设备完全没问题,Broker本身不是瓶颈,瓶颈在后面的数据处理链路。
2.2 主题规划:接入层的地基
主题规划设计好坏,直接决定接入层代码好不好写。TLI的主题规范最终定为三段式:
{产品标识}/{设备标识}/up # 上行:设备上报数据 {产品标识}/{设备标识}/down # 下行:平台下发指令 {产品标识}/{设备标识}/event # 上行:设备事件 {产品标识}/{设备标识}/shadow # 上下行复用:设备影子这里“设备标识”是全局唯一的设备编号,格式类似SN-20240115-001。“产品标识”是设备型号的分组,比如环境传感器、电表、摄像头。主题里不带任何业务语义,业务语义全部体现在消息内容的物模型字段中。
为什么这么设计?因为主题的层级越简单越好。带太多层级看似灵活,实际会让Broker的权限控制、通配符订阅变得很难维护。我们最初设计过/region/{区域}/type/{类型}/device/{设备}/data这样的主题,上线一周就发现区域和设备一多,通配符订阅怎么写都别扭,后来才简化成现在这样。
接入层服务启动后,每一台设备上线时会动态订阅属于自己的/down主题。这意味着下发控制是点对点的,不会广播给无关设备,安全和性能都有保障。
2.3 物模型与数据统一格式
设备数据能够“统一”,靠的是物模型定义。TLI中每一种产品都维护一份JSON Schema,描述这个产品有哪些属性、属性类型是什么、取值范围是多少。设备上报的原始数据先经过协议适配器,转换成符合物模型的标准化结构,再进入消息总线。
标准上行消息格式如下:
{ "mid": "a1b2c3d4e5", "product": "env-sensor", "device": "SN-20240115-001", "method": "report", "timestamp": 1737025200000, "properties": { "temperature": 23.5, "humidity": 45.2, "battery": 98 } }字段说明:mid是消息唯一ID,用于数据去重;method表示消息类型,report是属性上报,event是事件上报;properties是属性值,key必须与物模型定义一致。所有时间戳统一使用毫秒级Unix时间戳,避免不同设备时区问题导致的时间错乱。
这套格式定下来后,后续每接入一种新设备,只需要为它写一个“协议适配器”脚本,把设备私有格式转换成这个标准格式。平台里现有设备的适配器逻辑都不复杂,很多设备就是字段名映射加单位换算两个步骤。
3. 核心模块拆解与实操要点
3.1 设备认证与连接管理
设备接入第一步是认证。TLI采用典型的“产品密钥+设备密钥”三元组机制,和很多商用平台类似:设备出厂时烧录productKey、deviceName、deviceSecret三个信息,上线时用它们换取一个临时token,后续每次MQTT连接都用token认证。
Token生成规则我们直接用HMAC-SHA256签名。服务端为每台设备签发一个有效期7天的token,签名内容包含设备标识和过期时间,使用设备密钥作为HMAC密钥。
import hmac import hashlib import time def generate_device_token(device_secret: str, device_name: str, expire_days: int = 7) -> str: expire_ts = int(time.time()) + expire_days * 24 * 3600 payload = f"{device_name}:{expire_ts}".encode("utf-8") signature = hmac.new(device_secret.encode("utf-8"), payload, hashlib.sha256).hexdigest() return f"{payload.decode()}:{signature}"设备端拿到token后,MQTT连接时分别在用户名和密码字段中携带设备名和token。Broker侧通过自定义Auth插件校验token有效性。这套机制的好处是设备密钥不需要暴露在网络传输中,即使token被截获,最长7天也会失效,而且可以针对单设备吊销。
设备连接状态管理是接入层最容易忽视但又非常重要的一块。TLI在接入层维护了一套在线状态表,设备上线时写入Redis,心跳超时后标记离线,同时推送一条上下线事件到消息总线。这个状态表就是前面说的“一切状态可查”的基础。前端大屏上设备在线率,直接查询这张表即可。
有个细节:MQTT的遗嘱消息一定要用起来。设备异常断电时,Broker会主动发布遗嘱消息,平台收到后能立即知道设备离线,而不是等到心跳超时才被动发现。这个能力在处理现场“半夜设备集体掉线”这类问题时价值巨大。
3.2 规则引擎:从阈值告警到联动控制
规则引擎是TLI里业务上最值钱的模块。第一版我们用条件表达式硬编码,后来发现业务方需求变化太快,硬编码撑不住,就改成了JSON规则配置加动态执行。
一个完整的规则长这样:
{ "ruleId": "rule_001", "name": "车间高温联动排风扇", "trigger": { "type": "device_property", "product": "env-sensor", "property": "temperature", "condition": { "operator": ">", "value": 55, "duration": 3 } }, "actions": [ { "type": "alert", "level": "warning", "content": "车间温度超过55度,持续3分钟" }, { "type": "device_command", "product": "fan-controller", "device": "SN-20240115-002", "command": "turn_on" } ] }这里duration: 3意味着温度需要连续3次上报都超阈值才触发告警,而不是单次抖动就误报。这是我们在现场踩坑后加的字段,最早没有这个参数,夏天中午空调稍微波动就疯狂告警,运维被骚扰得很惨。
规则引擎的执行逻辑放在事件流处理层,对每个设备属性维护一个滑动窗口,窗口内满足条件次数达到阈值才触发动作。规则执行的性能和可扩展性是目前平台投入产出比最高的模块。
3.3 设备影子与命令下发
设备影子的作用,用一个生活类比来说:就像你在通讯录里给不在线的人发消息,消息先存在对方邮箱里,等对方上线后查收。TLI的设备影子保存两块内容:设备的期望属性值(用户想让设备达到的状态)和实际属性值(设备最新上报的状态)。
比如用户想让一个智能开关打开,下发的指令先写入影子“期望值”为开,同时通过MQTT下发命令给设备。如果设备在线,执行成功后上报最新状态,影子自动更新;如果设备离线,影子保持期望值为开,设备重新上线后影子服务会检测差异,自动补发命令。
设备影子在项目里的一个实际应用场景是批量调节园区照明:管理员在后台一次选中多台照明控制器,批量下发“亮度调到80%”。平台把指令写入每台设备的影子期望值,然后通过批量下行通道推送。这个方案比逐台下发接口的方式快很多,也稳定很多。
4. 数据链路与存储实践
4.1 上行数据的消费链路
设备上报的数据从Broker出来之后,并不是直接进库,而是先经过一个消息转发层,路由给不同的消费者。整个链路是:设备 → MQTT Broker → 接入服务 → 消息总线 → 存储服务/规则引擎/影子服务。
接入服务是整个链路里最忙的组件,它要解析上行消息、做设备认证、校验物模型格式、生成mid去重标识,然后发布到内部消息总线。这里有个性能调优点:接入服务和消息总线之间启用了批量发布模式,攒一批消息再统一发布,实测吞吐量提升非常明显。特别是现场大量传感器同时上报的“整点风暴”场景,批量模式几乎是必须的。
为什么中间要多加一层消息总线,而不是让接入服务直接写数据库?因为消费者不止一个。存储服务要写数据,规则引擎要判断条件,可视化要看实时数据,离线回溯要查历史。如果每个消费者都从接入服务单独拉一份,接入服务的复杂度会爆炸。引入消息总线后,接入服务的职责变得非常单一,就是“收数据、验数据、转发数据”。
4.2 时序数据存储与查询优化
设备数据是典型的时序数据,90%以上是写入多、查询少,且数据按时间顺序追加。我们选择了时序数据库作为核心存储,同时搭配关系型数据库存设备元数据和规则配置。这样分工明确,时序库存数据,关系库存结构。
时序库的表结构按“产品+指标”来设计:
CREATE TABLE env_temperature ( ts TIMESTAMP, device_name VARCHAR(64), value DOUBLE, quality INT, PRIMARY KEY (ts, device_name) );这个表每个小时会产生大量数据点,但查询模式非常固定——查某台设备最近一天的温度曲线、查某产品所有设备的平均值。针对这两种查询,我们建了两个维度的降采样任务:原始数据保留7天,7天以上数据自动聚合成每分钟均值,30天以上再聚合成每小时均值。降采样放在流处理任务里异步执行,不影响主链路写入性能。
查询性能上还有一个关键优化:设备维度的排序因子。所有时序表都以ts + device_name作为联合主键,这样查单设备时间范围数据时,可以走索引定位,不需要全表扫描。这个设计让1亿行数据量的单设备历史查询都能在毫秒级返回。
4.3 实时数据通道与可视化
平台可视化层直接订阅消息总线上的实时数据主题,用WebSocket推送到前端页面。这样画面上看到的温度曲线、设备状态是实时变化的,不是轮询接口。实时数据从设备上报到前端页面显示,端到端延迟实测在200毫秒以内,50Hz刷新率下画面也很流畅。
可视化大屏我们做了两块:一块是全园区总览,展示在线设备数、告警数、平均温度湿度;另一块是单设备详情,展示最近24小时曲线和事件记录。总览大屏的数据来源是实时统计任务,每5秒刷新一次统计结果,避免每次刷新前端都查询原始时序库。这个小优化让大屏在大规模数据下也能保持流畅。
5. 上线前必须做好的验证工作
5.1 连接稳定性压测
物联网平台有一个特点是“设备数量和连接频率波动极大”。我们上线前用模拟设备客户端做了一轮压测:同时保持5000个长连接,每个连接每10秒上报一条数据,持续运行12小时,重点观察Broker的连接数曲线、接入服务的CPU占用和消息总线堆积情况。
压测过程中暴露的第一个瓶颈是消息消费能力不足。模拟设备以固定频率上报时,消息总线缓存积压持续上涨,消费服务处理不过来。后来通过增加消费者实例数量和调整批量拉取策略解决。注意,这类压测一定要用真实的消息大小和数据频率,否则结果没有参考意义。
5.2 安全加固与权限控制
设备接入的网络安全必须上线前搞定,不能等出问题再补。TLI做了四件事:
第一是MQTT传输层开启TLS加密,虽然会带来一点性能开销,但设备认证信息和数据不会明文暴露。第二是前面说的token机制,配合密钥定期更换策略。第三是Broker侧的Topic ACL控制,每台设备只能在自己的主题范围内发布订阅,不能跨设备操作。第四是核心服务不暴露公网IP,全部走内网,外部访问只能通过统一API网关。
5.3 设备断线重连逻辑验证
断线重连是设备端最容易写错的逻辑。现场环境不是电脑机房,网线松动、Wi-Fi掉线、交换机重启、供电波动,都会导致设备断开。TLI的MQTT客户端统一使用如下重连参数:心跳间隔30秒,连接超时5秒,最大重连间隔60秒,指数退避算法,无限重试。
这里有一个经验:心跳间隔和Broker的keepAlive参数必须匹配。有的设备端心跳设得很短,比如5秒,但Broker默认keepAlive是60秒,这种不匹配会导致设备频繁断连。我们统一约定设备端心跳30秒,Broker keepAlive设为45秒,保证设备在2个心跳周期内能感知连接异常。
6. 实施中踩过的坑与排查实录
6.1 坑一:设备频繁掉线,以为是网络问题
项目上线第二天,现场反馈有一批设备每过几个小时就掉线一次,重新上线后正常一阵子又掉。最初怀疑是现场Wi-Fi不稳定,排查了很久无果。后来抓了Broker端日志,发现连接断开的原因是keepalive timeout。进一步排查发现,这批设备使用的4G物联网卡处于一个长期NAT环境中,运营商的NAT映射超时时间比Broker的keepAlive时间短,连接空闲稍久就被运营商断开,设备端没有及时感知。
解决方法是把设备端心跳从默认的60秒改成30秒,并且开启MQTT的ping请求,让连接在NAT超时之前就有数据活动。改完后掉线频率大幅下降。所以排查设备掉线问题时,优先看是不是心跳和网络链路不匹配。
6.2 坑二:消息重复导致数据翻倍
开关量设备上报QoS0时,偶发丢数据;改用QoS1后数据不丢了,但消费端偶尔收到重复消息。设备上报一条“开关状态=开”,数据库里插入了两条相同记录,曲线图上出现毛刺。
原因很典型:QoS1语义是“至少一次”,Broker在重传时可能导致重复投递。解决方式是在接入服务里按消息mid做去重,消费端维护一个最近5分钟的mid缓存,重复的消息直接丢弃。去重一定要做在写入数据库之前,否则数据已经污染了,后面再清洗就麻烦了。
6.3 坑三:规则引擎被瞬时波动反复触发
最早一版规则引擎没有加持续时间判断,单次超阈值就触发告警。结果夏季中午温度上下震荡时,告警信息每条间隔几分钟就来一次,值班人员直接麻木了。后来在规则配置里加了duration参数,要求连续多次超过阈值才触发。同时给每条规则加了“告警冷却时间”,同一规则5分钟内不重复触发相同告警。
6.4 坑四:时序数据膨胀太快
设备数量从几百涨到几千后,时序库存储空间增长速度超预期。最初设计保留一个月原始数据,结果半个月磁盘就报警了。后来做两件事:一是把原始数据保留期缩短到7天,更早的数据只看降采样聚合结果;二是优化了写入批次大小,原来一条条写入改成了批量写入,同样的数据量存储空间反而下降了。
这里给所有做物联网平台的团队一个建议:数据量估算时一定预留2倍的余量,且存储策略必须上线前就设计好,而不是等磁盘满了再补。
收尾的一点个人体会
回过头看,ThingLink-IoT平台从立项到稳定运行,最值得分享的经验其实不是技术选型,而是“先想清楚边界再动手”。接入层统一格式、数据先落库、影子状态跟踪,这三件事是第一版就做对的,后面所有新增功能都是在这个地基上堆的。如果当时偷懒只接设备不改格式,或者数据不落库只实时展示,现在这个平台大概率已经改不动了。
后续我们计划做两件事:一是把协议适配器改造成可动态加载的插件形式,接新设备时不用重新打包服务;二是给规则引擎增加图表化编排界面,让业务方自己拖拽就能配联动逻辑。如果你也在自建物联网平台,我的建议是先从一台设备、一条链路跑通开始,不要一开始就追求大而全的平台,把链路打通了,规模扩展是水到渠成的事。