自研物联网平台ThingLink-IoT:从架构设计到上线避坑的完整实战记录 “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平台从立项到稳定运行最值得分享的经验其实不是技术选型而是“先想清楚边界再动手”。接入层统一格式、数据先落库、影子状态跟踪这三件事是第一版就做对的后面所有新增功能都是在这个地基上堆的。如果当时偷懒只接设备不改格式或者数据不落库只实时展示现在这个平台大概率已经改不动了。后续我们计划做两件事一是把协议适配器改造成可动态加载的插件形式接新设备时不用重新打包服务二是给规则引擎增加图表化编排界面让业务方自己拖拽就能配联动逻辑。如果你也在自建物联网平台我的建议是先从一台设备、一条链路跑通开始不要一开始就追求大而全的平台把链路打通了规模扩展是水到渠成的事。