1. 选型先想清楚:Java做物联网平台到底图什么
先说结论:用Java做物联网平台不是因为它“时髦”,而是因为它在一个长连接高并发 + 后台业务逻辑复杂 + 团队更容易招人的场景里,综合成本最低。我前后参与过三套不同规模的设备接入系统,从几百台传感器的小项目到十万级设备连接的平台都有,最后落地的技术栈几乎都回到了JVM这条路上。很多刚入行的同学刷了一堆Java面试题、背了八股文,但真到要把设备的数据收上来、把指令发下去的时候,会发现题目和现实是两回事——这篇文章就是讲这中间的真实落差。
物联网平台的核心任务其实很朴素:把分散在各处的设备连起来,把数据收上来存好,把指令稳稳当当地发下去,再对外提供一套接口让上层业务调用。听起来简单,但它同时踩在嵌入式、网络通信、分布式、数据库、前端可视化五个领域的交界处。标题里的“JAVA物联网平台”,重点不在“平台”这个词有多宏大,而在于你用什么语言、什么框架去承接这四件事。Java的选择,本质上是拿运行时的稳定性和生态的成熟度,去换一点内存开销和启动速度。
这篇内容适合三类人看:一是做过Java后端但没碰过设备侧通信的同学,想搞清楚长连接、协议解析是怎么回事;二是做嵌入式或硬件的工程师,设备端调通了,但不知道怎么把数据接到一个正经的服务端平台;三是正在做课程设计或者实训项目的人,需要一套能跑起来、能讲清楚原理的完整方案。我不会堆概念,会尽量把每个选型背后的“为什么”讲透,参数怎么算、坑在哪里、代码骨架长什么样,都尽量给到能直接抄的程度。
1.1 物联网平台的典型分层,以及Java卡在哪一层
一套完整的物联网平台,从下往上大致分四层,我用一个生活化的类比帮你记住:设备端像小区里的水表,接入层像抄表员,平台层像物业的数据中心,应用层像给业主看的手机App。
第一层是设备端,通常是MCU(比如STM32系列)加通信模组(4G、NB-IoT、WiFi、以太网),负责采集传感器数据、执行控制指令。这一层大多用C语言写,和Java没关系,但它决定了后面所有数据的格式和上报节奏,所以你必须懂它的脾气。
第二层是接入层,也是Java真正的主战场。它要维护几十万条TCP或MQTT长连接,处理心跳、鉴权、编解码、限流。这一层对并发模型要求极高,Java的NIO框架Netty在这里几乎是标配。
第三层是平台层,负责设备台账、物模型、规则引擎、告警、时序数据存储。这一层是典型的业务系统,Spring Boot、Spring Cloud这一套用起来非常顺手,也是Java生态最厚实的地方。
第四层是应用层,面向具体业务,比如大屏展示、报表导出、开放API。报表导出这种活儿,用Apache POI生成Word或者Excel,甚至往Word里塞图表,Java都有成熟方案,这块省心。
Java的优势集中在第二到第四层。它不太适合放在设备端(除非是算力较强的网关),但作为承上启下的服务端,它把“高并发连接”和“复杂业务”这两件本来矛盾的事,用一套技术栈解决了。这就是选它的核心理由。
1.2 Java做接入层的真实优势与必须直面的短板
优势有三点,都很实在。第一是生态完整,Netty解决网络、Disruptor解决队列、Kafka和RocketMQ解决削峰、Redis解决缓存和在线状态,几乎所有轮子都有经过大规模验证的实现,你不需要自己造。第二是团队可替换性强,招一个会Spring Boot的Java工程师,比招一个既懂嵌入式又懂分布式的全栈工程师容易得多,这在项目长期维护上是决定性因素。第三是JVM的调优空间大,堆内存、GC策略、线程模型都能调,遇到性能瓶颈有得治。
短板也得摊开讲。Java的内存占用偏高,一台4核8G的机器,跑一个Netty接入服务加上业务服务,连接数上到几万就要开始精打细算了。另外原生Java对硬件和协议的支持弱,像串口通信、Modbus这类工业协议,虽然有jSerialComm、Modbus4J这样的库,但稳定性和设备端原生C比起来还是有差距,很多项目最终还是把协议解析放在网关侧用C完成,Java只接标准化的MQTT或HTTP。
还有一个容易被忽视的点:Java的GC停顿对实时性有害。如果你做的是毫秒级响应的控制类场景(比如远程实时操控),大堆导致的Stop-The-World会带来抖动。解决办法是把接入和业务拆开,接入层用低延迟GC(ZGC或Shenandoah),业务层用大堆做吞吐。这个拆分后面会细讲。
1.3 我常用的技术栈清单和版本建议
下面这张表是我在最近一个十万级连接项目里实际用的组合,不是标准答案,但经过生产验证,可以直接作为起点。
| 层次 | 组件 | 版本建议 | 选它的理由 |
|---|---|---|---|
| 接入框架 | Netty | 4.1.x | 成熟的NIO封装,社区活跃,坑基本都有人踩过 |
| 业务框架 | Spring Boot | 3.2.x | 生态成熟,配合JDK17长期支持 |
| 消息中间件 | RocketMQ | 5.x | 削峰、顺序消息、事务消息都支持 |
| 时序存储 | TDengine 或 InfluxDB | 3.x / 2.x | 写入吞吐高,按时间分区查询快 |
| 关系存储 | MySQL | 8.0 | 存设备台账、物模型、用户权限 |
| 缓存 | Redis | 7.x | 设备在线状态、会话、限流计数 |
| 运行时JDK | OpenJDK | 17 或 21 | 长期支持,ZGC可用 |
这里特别说一下JDK版本。很多人本地装的是JDK8,编译时遇到“源发行版 17 需要目标发行版 17”这种警告就懵了,其实是编译参数里的source和target没对齐。新项目我建议直接上JDK17,它的ZGC已经在生产可用,对大堆低延迟场景很友好;如果团队保守,JDK11也能撑,但别再往JDK8上压了。顺带提一句,Java 21的虚拟线程在处理大量阻塞IO(比如调用外部HTTP接口)时能显著简化代码,值得关注。
2. 设备接入层:把不稳定的物理世界接进来
接入层是整个平台最容易出问题的地方,因为它的对端是不受你控制的物理设备,网络会抖、设备会掉电、报文会乱序。这一层的设计目标只有一个:在混乱中保持服务的稳定,把脏活累活挡在业务层之外。下面分三块讲,都是实战中反复打磨出来的做法。
2.1 MQTT Broker选型与Topic命名规范
设备接入协议主流有三类:MQTT、HTTP、自定义TCP二进制。HTTP适合上报频率低的场景(比如一天几次),实现简单但连接开销大;MQTT适合长连接、双向通信、订阅推送,是物联网最常用的;自定义TCP适合对带宽和功耗极致敏感的场景,但开发成本最高。
MQTT的话,Broker选型我一般推荐两类:EMQX(Erlang写的,单机轻松扛几十万连接,集群成熟)和Mosquitto(轻量,适合中小规模)。有些团队想用Java自己写Broker,比如基于Netty从零实现,我不建议——除非是做教学或者极度定制化的需求,否则MQTT协议的各种边界情况(QoS重传、遗嘱消息、保留消息)会耗尽你的耐心。
Topic设计是最容易被低估的环节。我见过把设备ID直接拼成device/发过来的乱码这种,后期根本没法做权限隔离。推荐一套清晰的结构:
上行(设备到平台):/{产品ID}/{设备ID}/up/{功能} 下行(平台到设备):/{产品ID}/{设备ID}/down/{功能}比如/sensor001/dev0022/up/temperature就是设备dev0022上报的温度数据。这种结构的好处是EMQX可以用通配符订阅/sensor001/+/up/#统一接入,同时授权规则可以精确到某台设备的某个主题,防止A设备伪造B设备的数据。产品ID和设备ID分开,是为了让同一款固件的设备共享一套协议解析逻辑,减少重复代码。
注意:
#是MQTT的多层通配符,+是单层通配符。别在授权规则里用#做全量匹配,那等于给任何设备开了所有主题的权限,安全上是大忌。
设备身份认证至少要有两级:连接时的用户密码或证书,以及主题级别的ACL。生产环境建议用一机一密,即每台设备有独立的用户名密码或客户端证书。别图省事用全局共享密码,一旦泄露,整个平台的设备都能被冒充。
2.2 自定义二进制协议的编解码与粘包处理
如果你的设备用的是私有TCP协议,就不能只靠MQTT了,需要自己处理粘包和拆包。这块是新手最容易翻车的地方。TCP是字节流,没有消息边界,发两次数据可能被合并成一个包,也可能被拆成两个包。解决办法是在协议头里定义长度。
我常用的一套帧结构是这样:魔数(2字节) + 版本(1字节) + 消息类型(1字节) + 数据长度(4字节) + 负载(N字节) + 校验(2字节)。魔数用来快速识别包边界,长度用来切分,校验用来防传输错误。解析时用Netty的LengthFieldBasedFrameDecoder,把长度字段的偏移、长度、跳过字节数配好,剩下的粘包问题它自动帮你解决。
// Netty 服务端 pipeline 里配置长度域解码器 // maxFrameLength=1024, lengthFieldOffset=4, lengthFieldLength=4, // lengthAdjustment=0, initialBytesToStrip=8 ch.pipeline().addLast( new LengthFieldBasedFrameDecoder(1024, 4, 4, 0, 8)); ch.pipeline().addLast(new DeviceMessageDecoder());lengthAdjustment和initialBytesToStrip这两个参数最容易配错。lengthAdjustment是长度字段后面还有多少字节不算在长度里,initialBytesToStrip是解完帧之后从头部剥离多少字节。配错了的表现是:要么报TooLongFrameException,要么解出来的负载总是少几个字节。我的技巧是先用Wireshark抓一段真实报文,按字节数数清楚,再回去填参数,比凭空猜快得多。
解码器里还要处理半包:如果长度字段只收了一半,解码器会等待下一次读事件,这是对的,但你必须在业务处理器里保证处理是幂等的,因为QoS或重连可能带来重复消息。编解码这块,我建议单元测试覆盖率做到90%以上,用字节数组喂给解码器,断言解出来的对象,这个投入在后期排查问题时回报极高。
2.3 连接鉴权、心跳与断线重连的工程细节
设备连上来之后,平台要做三件事:认它是不是合法、判断它还活着、掉了之后能不能回来。
鉴权方面,MQTT在CONNECT报文里带用户名密码,你可以在Broker的认证插件里接数据库或者HTTP回调校验。自建TCP的话,通常在握手阶段让设备先上报设备ID和签名,服务端比对后再放行。签名用HMAC,把设备ID、时间戳、密钥拼一起做摘要,防止重放。时间戳窗口设成5分钟,超时拒绝。
心跳是判断在线状态的手段。MQTT自带KeepAlive机制,设备在1.5倍KeepAlive时间内没发包,Broker就认为它掉线,触发遗嘱消息。自建TCP就要自己设计心跳包,我一般要求设备每30秒发一次,服务端90秒没收到就判定离线,同时清理会话和在线状态。心跳间隔不能太短,否则设备功耗和带宽吃不消;也不能太长,否则掉线感知慢,指令发出去石沉大海都没人知道。
断线重连要区分设备侧和服务端侧。设备侧一般用指数退避,第一次1秒重连,失败后2秒、4秒、8秒,封顶60秒,避免网络刚断时一堆设备同时重连把服务端打垮。服务端侧要保证重连后能恢复上下文,用设备ID做会话标识,把未确认的指令重新下发。这里有个坑:如果设备是“干净会话”模式,重连后订阅关系会丢,需要在CONNECT时设置cleanSession=false保留会话,否则平台推的配置消息设备永远收不到。
实操心得:在线状态别只存在内存里,用Redis的key加过期时间来做,key是
online:{deviceId},每次心跳刷新过期时间。这样多实例部署时状态天然一致,不用自己搞同步,比维护一个全局Map省心得多。
3. 数据链路实操:从一条上报报文到落库
接入解决了“收得到”,接下来解决“存得下、查得快、不丢不重”。这一条链路是很多人做课程设计时最容易糊弄过去的部分,但恰恰是区分玩具项目和真实项目的分水岭。
3.1 接入服务工程骨架
一个可用的接入服务,我通常拆成三个模块:网络层、协议层、业务层。网络层只管连接和收发字节;协议层把字节变成业务对象;业务层把对象投递到消息队列,然后立即返回,绝不在这里做数据库操作。为什么要拆这么细?因为长连接线程是稀缺资源,任何阻塞操作都会拖垮整个接入服务。想象一下,接入线程在等MySQL返回,这时候几万个连接的心跳全在排队,服务直接雪崩。
工程结构大致这样:
iot-access ├── net // Netty ServerBootstrap、ChannelInitializer、编解码器 ├── codec // 协议解析,字节 <-> 消息对象 ├── session // 设备会话管理,基于 Redis 或本地缓存 ├── handler // 业务处理器,校验、投递 MQTT/Kafka └── config // 线程池、参数、连接数上限配置业务处理器里的核心逻辑是:解析出设备ID和消息类型,做基础校验(设备是否注册、频率是否超限),然后把消息序列化后投到RocketMQ的一个主题里。整个处理器是纯内存操作加一次异步发送,耗时控制在毫秒级。
// 业务处理器伪代码:校验后投递,不碰数据库 public void channelRead(ChannelHandlerContext ctx, Object msg) { DeviceMessage dm = (DeviceMessage) msg; if (!deviceRegistry.exists(dm.getDeviceId())) { ctx.close(); // 未注册设备,直接断开 return; } if (rateLimiter.tryAcquire(dm.getDeviceId())) { mqProducer.sendAsync("iot-up", dm.toJson()); } else { metrics.counter("device.throttled").increment(); } }限流这里用的是令牌桶,每个设备一个桶,防止单台设备疯狂上报把队列塞满。桶的容量和速率按设备的物理特性设定,比如温度传感器5秒一次,那速率就是0.2 QPS,突发容量给3。这个设计能有效防止一台故障设备拖垮整个平台,是必须做的防护。
3.2 削峰与数据一致性
消息进了队列,下游的存储服务就可以按自己的节奏消费,这就是削峰。设备上报的高峰可能每秒几万条,而数据库的写入能力是有限的,中间隔一层队列,两边解耦,各自按最优速率工作。
关于数据一致性,这是热词里经常被问到的点。物联网场景的一致性要求分情况:监控类数据允许少量丢失,丢一两条温度点不影响趋势分析;计费类或控制类数据必须准,比如电表读数、阀门开关指令。所以别一刀切,按数据类型分主题处理。
保证不丢的常用手段是消费端手动确认加幂等写入。消费者处理完落库后再ack,如果处理失败就不ack,消息会重投;同时用设备ID加时间戳加序列号做唯一键,重投时用INSERT IGNORE或者Redis的setnx去重,做到“至少一次”投递加“幂等”消费,等效于精确一次。
顺序性也要注意。同一台设备的数据如果乱序,曲线会跳来跳去。RocketMQ的顺序消息靠指定同一个队列实现,把设备ID做哈希选队列,同一设备的消息就落在同一个队列里,消费端单线程消费这个队列,顺序就有保证了。代价是并行度降低,所以通常只在控制指令这类对顺序敏感的主题上开顺序消息,普通遥测数据不用。
// 发送顺序消息:按设备ID选队列 producer.send(msg, (mqs, m, arg) -> { int index = Math.abs(arg.hashCode()) % mqs.size(); return mqs.get(index); }, deviceId);3.3 时序数据落库与冷热分离
存储选型上,关系库存元数据,时序库存测量数据,对象存储存文件,这是标准分工。设备台账、物模型定义、用户权限放MySQL;温度、电压、位置这类带时间戳的数值放TDengine或InfluxDB;设备上传的图片、日志文件放对象存储。
时序库相比MySQL的优势在于:按时间自动分区、列式压缩、聚合查询快。同样是查一台设备某天的温度平均值,MySQL在千万行数据上要几秒,时序库几十毫秒就出来了。建表时按设备维度建超级表,时间戳做第一列,其他字段做普通列或标签,写入时批量提交,每批几百到几千条,比一条条写快十倍以上。
冷热分离是省钱的招。最近7天的数据查得最频繁,放在高速存储上;30天以上的历史数据压缩后转到廉价存储。TDengine支持多级存储,InfluxDB可以配置保留策略,或者你自己写定时任务把老数据导到对象存储,需要时再拉回来。我做过一个项目,把一年以上的历史数据落到对象存储,存储成本直接降了七成,查询时用异步接口返回,用户体验影响很小。
注意:批量写入时序库时,务必处理部分失败的情况。一批1000条里失败3条,不能简单重试整批,否则会造成大量重复。用带时间戳的主键去重,或者把失败的单独摘出来重发。
4. 平台能力:设备管理、规则引擎与开放API
数据收上来只是原料,真正让平台有价值的是它对外提供的能力:能管设备、能配规则、能开放接口。这一层是Java的舒适区,Spring Boot的生态优势在这里体现得淋漓尽致。
4.1 设备台账与在线状态维护
设备台账是所有业务的根。它至少包含:设备ID、所属产品、固件版本、注册时间、当前状态、绑定的用户或项目。产品维度很重要,同一款产品的设备共享物模型(也就是数据字段的定义),改一次定义,所有设备生效,不用逐台配置。
在线状态我前面提到用Redis,这里补充细节。写入时用SET online:{deviceId} 1 EX 90,读取用EXISTS。设备离线时,除了key自动过期,还要有个定时任务扫描最近一分钟内过期但状态仍标记为在线的设备,发离线事件通知业务方。为什么不只依赖过期?因为Redis的过期是惰性的,key过期了但没人访问就不会触发删除,你需要用键空间通知或者自己轮询兜底。
物模型的实现建议用JSON存储字段定义,包含字段名、类型、单位、取值范围。上报的数据进平台后,用物模型做一次校验和单位换算,把原始值转成标准值再落库。这样上层应用拿到的数据格式统一,不用每个应用自己做转换。
4.2 轻量规则引擎怎么落地
规则引擎的作用是:当设备数据满足某个条件时,自动执行动作。比如“温度超过80度就发告警”“电量低于10%就推提醒”。市面上的重型规则引擎(比如Drools)功能强但学习成本高,物联网场景通常用不上那么复杂的推理,我一般用轻量的表达式引擎(比如Aviator或MVEL)来实现。
规则配置存成JSON:触发条件、数据来源、动作列表。消费服务收到消息后,查出该设备适用的规则(用Redis缓存规则),用表达式引擎求值,命中就执行动作。动作可以是发MQTT指令、写告警表、调HTTP回调。整个链路异步执行,不阻塞数据落库。
{ "ruleId": "r-1001", "deviceId": "dev0022", "condition": "payload.temperature > 80", "actions": [ {"type": "alarm", "level": "high", "message": "温度超限"}, {"type": "mqtt", "topic": "/down/warning", "payload": "cooling"} ] }规则求值要加防抖和抑制,否则温度在阈值附近波动,一分钟能触发几百条告警。我通常设一个静默窗口,同一规则同一设备在5分钟内只触发一次,或者用状态机记录上一次状态,只在状态翻转时触发。这个细节不做,告警轰炸会让运维直接关掉通知。
4.3 对外开放接口的鉴权、限流与报表导出
平台做出来是给别人用的,开放API必不可少。设计上有几个要点:统一鉴权、细粒度授权、限流保护、版本管理。鉴权用OAuth2或者简单的API Key加签名,API Key绑定应用,应用再绑定它能访问的设备范围。不要给一个应用全平台的数据权限,最小权限原则在这里同样适用。
限流按应用维度做,用Redis的滑动窗口。每个应用有QPS配额,超了就返回429。这一层放在网关(比如Spring Cloud Gateway)上做,业务服务不用关心。
报表导出是很多物联网项目的刚需,比如导出某台设备一个月的运行报表,生成Word或者Excel。Java这块用Apache POI很成熟,生成Excel用XSSFWorkbook,生成Word用XWPFDocument,往里面插表格、图片、甚至图表都可以。要注意的是POI在导出大文件时内存占用高,几万行数据用SXSSFWorkbook流式写,不要用XSSFWorkbook一次性加载。如果是要生成带图表的Word,POI对图表的支持相对弱一些,有时候需要曲线救国,先用JFreeChart生成图片再插入Word,效果反而更可控。
实操心得:报表导出一定要做成异步任务。用户点导出,后端返回一个任务ID,后台慢慢生成,生成完推到对象存储,再通知用户下载。同步导出大报表十有八九会超时,用户体验极差。
5. 上线前后必看:问题排查与压测踩坑
前面讲了怎么搭,这一节讲怎么让它稳稳地跑。真实项目里,接入服务出问题的形式五花八门,但排查思路是有套路的。我整理了一张速查表,都是真金白银换来的。
5.1 连接类问题速查表
| 现象 | 可能原因 | 排查手段 | 解决方向 |
|---|---|---|---|
| 设备连不上,握手就断 | 鉴权失败、IP白名单、端口不通 | 抓包看CONNECT返回码;查Broker日志 | 核对密钥;检查防火墙和ACL |
| 连接数上不去 | 文件句柄限制、内存不足 | ulimit -n;看JVM堆和直接内存 | 调大句柄数;调优Netty内存池 |
| 连接频繁掉线重连 | 心跳超时、网络抖动 | 统计掉线时间分布;看是否集中某区域 | 调整心跳间隔;排查运营商网络 |
| 消息丢失 | QoS配置、消费未ack | 对比设备发送计数和平台入库计数 | 提高QoS;消费端手动ack |
| 消息重复 | 重传、消费失败重投 | 用设备ID加序列号统计重复率 | 消费端幂等;去重表 |
| CPU飙高 | 空轮询bug、死循环、频繁GC | top看线程;jstack看火焰 | 升级Netty;排查业务逻辑;调GC |
这张表里我想特别强调文件句柄。Linux默认单进程可打开1024个文件,一个TCP连接占一个句柄,几万连接必须先把ulimit -n调到几十万,同时检查/proc/sys/fs/file-max。这个坑几乎每个新手都会踩,表现就是连接数到1024左右再也上不去,报“Too many open files”。
5.2 数据不一致的排查与幂等设计
数据不一致通常有三种表现:设备说发了,平台没收到;平台说入库了,查询没数据;同一条数据在库里出现两次。逐一拆解。
第一种,设备侧发送成功但平台没收。可能是QoS为0(至多一次),消息在网络中丢了。改成QoS1(至少一次),并在设备侧记录发送日志,平台侧统计接收计数,两边定期对账,差值超过阈值就告警。
第二种,入库了但查不到。常见于主从延迟或者写入和读取走了不同分片。刚写完立刻查,从库还没同步过来。这种场景读主库,或者写入后返回自增ID,让前端拿着ID去查。时序库也有类似问题,写入后立即可查的一般是单节点,集群版有同步延迟,重要数据要等一个小的延迟再查。
第三种,重复数据。幂等是唯一解药。我的做法是在消息里带一个全局唯一的msgId(设备ID加毫秒时间戳加随机数),消费端用Redis的SETNX msg:{msgId} 1 EX 3600判断是否处理过,处理过直接ack跳过。这个方案简单有效,代价是多一次Redis调用,但相比数据重复带来的业务混乱,这点开销完全值得。
5.3 JVM参数、压测方法与部署要点
JVM调优不神秘,接入服务和业务服务的策略完全不同。接入服务追求低延迟,堆不用太大,4G到8G足够,因为大部分内存是直接内存(Netty的ByteBuf),GC用ZGC,-XX:+UseZGC -Xmx8g -Xms8g,年轻代不用特别调,让ZGC自己管。业务服务追求吞吐,堆可以大一些,16G到32G,GC用G1,设置合理的停顿目标-XX:MaxGCPauseMillis=200。
压测千万别用真实设备,成本太高。用JMeter或者自己写Netty客户端模拟,单机模拟几千到几万连接,重点观察三件事:连接建立速率、消息吞吐、GC频率。我一般分三轮压:第一轮只连不发,测连接容量;第二轮稳定发消息,测吞吐;第三轮模拟大批设备同时断线重连,测抗抖动能力。第三轮最容易被忽视,但它恰恰是生产环境最常见的故障场景。
部署上,接入服务建议用容器化加Jenkins持续集成,但要注意容器的文件句柄和内存限制。K8s里Pod默认的文件句柄可能不够,需要显式配置ulimits。另外容器里看到的CPU核数是宿主机的,JVM默认的GC线程数和并行度会按宿主机算,导致资源争抢,要显式设置-XX:ActiveProcessorCount。
# 接入服务的典型启动参数 java -XX:+UseZGC -Xmx8g -Xms8g \ -XX:ActiveProcessorCount=4 \ -XX:MaxDirectMemorySize=4g \ -jar iot-access.jar顺带说一句,很多公司用K8s的HPA做自动扩缩容,但长连接服务扩容后,新连接会分到新Pod,老Pod的连接不会自动迁移,所以扩容有效但缩容要谨慎,别把还有几万连接的Pod直接干掉。优雅下线要做好,先停止接受新连接,等现有连接自然断开或者主动通知设备重连,再退出进程。
我个人在这些项目里踩过的坑,最深刻的还是过度设计。一开始总想着把所有协议都支持、把规则引擎做得无所不能,结果半年过去核心功能还没稳定。后来学乖了,先支持一种协议(通常是MQTT)、一种存储(时序库加MySQL)、一个最简单的规则引擎,把这条主链路打磨到能扛住压测,再逐步加东西。物联网平台不是一天建成的,它的复杂度来自真实设备的多样性和不确定性,而这些只有上线之后才会暴露出来。先把地基打牢,比什么都重要。