1. 设备接入层面试到底在考什么
面物联网后端,设备接入层这块基本是绕不开的。我面过不少人,也被人面过,发现一个规律:面试官问 MQTT,从来不是想听你把协议规范背一遍,而是想看你对“设备怎么连上来、消息怎么不丢、量大了怎么办”这三件事有没有实战判断。标题里说的“高频 10 问”,我把它归成四类:协议理解、连接管理、消息可靠性、架构选型。这四类问题背后其实是一套完整的设备接入层知识体系,你只要把这条线捋顺了,面试时不管对方怎么换着花样问,你都能接得住。
先说清楚设备接入层在整个物联网后端里的位置。一个典型的物联网平台,从下往上看大概是:设备端(MCU、模组、网关)→ 接入层(MQTT Broker、协议适配)→ 消息处理层(规则引擎、流处理)→ 业务层(Spring Boot 微服务、数据库)→ 应用层(Web、App)。设备接入层就是那道“闸门”,所有设备的数据从这里进来,所有下发的指令从这里出去。它要解决的核心问题就三个:海量连接怎么扛住、异构协议怎么统一、消息怎么保证可靠。面试官问的每一个问题,本质上都在试探你对这三个问题的理解深度。
为什么是 MQTT 而不是 HTTP?这个问题几乎每轮面试都会出现。很多人答“因为 MQTT 轻量”,这个答案只能拿及格分。你得往下说:HTTP 是请求-响应模型,设备要主动轮询才能拿到指令,轮询间隔短了费电费流量,间隔长了指令延迟高;MQTT 是发布-订阅模型,设备订阅一个主题,服务端有指令直接推,设备平时保持一个长连接就行。对于电池供电的传感器,这个差别是致命的——HTTP 轮询可能让一颗纽扣电池撑一周,MQTT 长连接能撑一年。再加上 MQTT 的报文头最小只有 2 字节,HTTP 光头部就几百字节,在 NB-IoT 这种按流量计费的场景下,成本差距非常明显。
但 MQTT 也不是万能的。我见过有人拿 MQTT 传固件升级包,一个包几兆,走 MQTT 分片传,结果 Broker 内存直接爆了。MQTT 适合小报文、高频次、低功耗的场景,大文件传输应该走 HTTP 或对象存储的预签名 URL。这个判断在面试里如果能主动说出来,面试官会觉得你确实踩过坑,而不是只看了几篇博客。
设备接入层的技术选型,绕不开 Broker 的选择。自己基于 Netty 写一个 MQTT Broker 行不行?行,但没必要。EMQX 这类成熟 Broker 已经把连接管理、会话保持、集群分片、规则引擎都做好了,你重复造轮子的收益极低。面试时如果被问到“为什么用 EMQX 不用自己写”,你可以从三个角度答:第一,连接规模,EMQX 单节点能扛百万级连接,自己写很难做到;第二,协议完整性,MQTT 3.1.1 和 5.0 的细节很多,QoS 2 的两次握手流程、会话过期的处理,自己实现容易出 bug;第三,运维生态,EMQX 有 Dashboard、有 Prometheus 指标、有规则引擎可以直接把消息转到 Kafka 或数据库,省掉大量胶水代码。
Spring Boot 在接入层里的角色,主要是做设备管理 API 和消息消费端。设备鉴权、设备影子、指令下发接口这些用 Spring Boot 写很顺手,但 Broker 本身不建议用 Spring Boot 嵌进去——Spring Boot 的线程模型和内存管理不适合处理百万级长连接。正确的做法是 Broker 独立部署,Spring Boot 通过 MQTT 客户端或 Kafka 跟 Broker 交互。这个边界感在面试里很重要,很多人把“用 Spring Boot 做物联网”理解成“所有东西都塞进 Spring Boot”,这是典型的架构认知不清。
2. MQTT 协议高频问题拆解
2.1 QoS 0/1/2 到底怎么选
QoS 是 MQTT 面试的必考题,但很多人只背了“0 最多一次、1 至少一次、2 恰好一次”,问到实际怎么选就卡壳了。我一般这么答:QoS 的选择取决于“丢一条消息”和“收一条重复消息”哪个代价更大。
QoS 0 是发出去就不管了,Broker 不回复确认,发送方也不知道对方收没收到。适合什么场景?高频传感器数据。比如温度传感器每秒上报一次,丢一两条完全不影响趋势判断,用 QoS 0 最省资源。QoS 1 是发送方发完等 PUBACK,没收到就重发,所以至少到达一次,但可能重复。适合指令下发、告警上报这种不能丢但重复了也能处理的场景。QoS 2 是四次握手(PUBLISH → PUBREC → PUBREL → PUBCOMP),保证恰好一次,但开销最大,适合计费、开关控制这种重复执行会出问题的场景。
这里有个实操细节:QoS 是双向的。发布时指定 QoS 1,订阅时也指定 QoS 1,最终生效的是两者中较小的那个。我见过有人发布用 QoS 2,订阅用 QoS 0,然后问为什么消息会丢——因为订阅端的 QoS 0 把级别拉下来了。这个点在面试里能主动提,说明你真的配过。
还有一个坑:QoS 1 的重复消息怎么处理。Broker 重发是因为没收到 PUBACK,但有可能 PUBACK 在路上丢了,消息其实已经到了。所以消费端必须做幂等。常见做法是用消息里的 messageId 或者业务上的设备 ID + 时间戳做去重。EMQX 的规则引擎里可以用 SQL 做去重,或者在 Spring Boot 消费端用 Redis 的 setnx 做短期去重。
2.2 会话保持与 Clean Session
Clean Session 这个参数,MQTT 3.1.1 里叫 cleanSession,5.0 里改叫 Clean Start 加 Session Expiry Interval。面试问这个,其实是在问你对离线消息的理解。
cleanSession = true 的时候,每次连接都是全新的会话,Broker 不保存订阅关系和未确认消息。设备断线重连后,之前订阅的主题全没了,得重新订阅。cleanSession = false 的时候,Broker 会保存会话,设备离线期间发到它订阅主题的消息会缓存起来,等它重连后推给它。
什么时候用 false?指令下发场景。比如你给一个水表下发“立即上报读数”的指令,水表正好在信号盲区,如果 cleanSession = true,这条指令就丢了;用 false 的话,水表回到信号区重连后能收到这条指令。什么时候用 true?高频上报的传感器。它不需要离线消息,断线重连后重新订阅、继续上报就行,用 false 反而会让 Broker 缓存大量无用的历史数据。
这里有个容易忽略的点:MQTT 5.0 的 Session Expiry Interval。3.1.1 里 cleanSession = false 的会话是永久保存的,设备如果再也不上线,Broker 里就留着一堆僵尸会话。5.0 允许你设置会话过期时间,比如 3600 秒,超过这个时间没重连就自动清理。生产环境里这个参数一定要设,不然 Broker 内存会被慢慢吃光。
2.3 遗嘱消息与设备离线检测
遗嘱消息(Will Message)是 MQTT 里一个很实用但经常被忽略的特性。设备连接时告诉 Broker:“如果我异常断线了,你帮我发一条消息到某个主题。” 这条消息就是遗嘱。
为什么需要遗嘱?因为 TCP 连接断开和业务下线是两回事。设备可能因为断电、网络故障突然消失,Broker 检测到 TCP 断开后,如果配置了遗嘱,就会立刻发布遗嘱消息。后端订阅这个遗嘱主题,就能知道设备离线了,可以触发告警或者更新设备状态。
但遗嘱有个坑:它只在异常断线时触发,正常 DISCONNECT 不触发。设备主动发 DISCONNECT 包下线,Broker 认为它是正常离开,不会发遗嘱。所以如果你想让设备正常下线也通知后端,得在业务层再发一条“离线”消息,不能只依赖遗嘱。
另外,遗嘱的延迟取决于 Keep Alive 时间。设备连接时设置的 Keep Alive 是 60 秒,Broker 在 1.5 倍 Keep Alive 时间内没收到任何包,才判定设备离线。也就是说,设备断电后最长可能要 90 秒后端才知道。对实时性要求高的场景,这个延迟要提前跟业务方对齐。
2.4 主题设计与通配符
主题设计是设备接入层的基本功,但面试里能答好的人不多。常见错误是主题层级太深或者太浅。太深了,比如china/beijing/chaoyang/device001/sensor/temperature,通配符匹配效率低;太浅了,比如device001,没法做批量订阅。
我一般推荐的主题结构是:{产品ID}/{设备ID}/{消息类型}。比如watermeter/dev001/telemetry和watermeter/dev001/command。这样后端订阅watermeter/+/telemetry就能收到所有水表的上报,订阅watermeter/dev001/command就能给特定设备下发指令。
通配符有两个:+匹配单层,#匹配多层。watermeter/+/telemetry里的+匹配任意一个设备 ID。watermeter/#匹配 watermeter 下面所有层级。注意#只能放在最后,watermeter/#/telemetry是非法的。这个细节面试里经常考。
还有一个性能问题:通配符订阅多了会拖慢 Broker。EMQX 内部用主题树来匹配,如果大量客户端订阅#,每次发布消息都要遍历整棵树。生产环境要限制客户端订阅#的权限,用 ACL 控制。
2.5 保留消息的使用边界
保留消息(Retained Message)是 Broker 为某个主题保存的最后一条消息,新订阅这个主题的客户端会立刻收到它。典型场景是设备状态:设备上线后发布一条 retained 消息到device/dev001/status,内容是 online。后端或者新上线的监控服务订阅这个主题,立刻就能知道设备当前状态,不用等设备下一次上报。
但保留消息不能滥用。每个主题只能保留一条,新消息会覆盖旧的。如果你用 retained 消息传传感器数据,那新订阅者只会收到最后一条,历史数据全丢了。另外,retained 消息会一直存在 Broker 里,如果主题很多,内存占用会持续增长。EMQX 里可以设置 retained 消息的过期时间,生产环境建议配上。
删除 retained 消息的方法是:向该主题发布一条空 payload 的 retained 消息。这个操作在设备下线清理时很有用。
3. 设备接入层的架构与实操
3.1 EMQX 集群部署的关键参数
单机 EMQX 扛个几万连接没问题,但生产环境要考虑高可用和水平扩展。EMQX 集群的节点发现方式有几种,手动加入、DNS 发现、etcd 发现等。我一般用static 手动配置,在emqx.conf里写死集群节点列表,简单可控。
集群里有两个关键参数:node.name和cluster.discovery_strategy。node.name 格式是emqx@ip,集群里每个节点的名字必须唯一。discovery_strategy 设为 static 后,在cluster.static.seeds里列出所有节点。
会话在集群里怎么分布?EMQX 默认用session.persistent模式,会话数据存在本地。如果设备重连到了另一个节点,会话就找不到了。解决办法是用session.sticky或者把会话存到外部存储。EMQX 企业版支持会话持久化到 Redis,开源版可以用emqx_retainer插件配合外部数据库。面试里如果问到集群会话,能说到这一层就很加分。
还有一个实操经验:集群节点之间的网络延迟要低。EMQX 节点之间会同步路由表和会话状态,如果跨机房部署,延迟高了会导致消息转发变慢。同机房部署,节点间延迟控制在 1ms 以内最好。
3.2 Spring Boot 消费 MQTT 消息的完整实现
Spring Boot 里消费 MQTT 消息,最常用的是 Eclipse Paho 客户端。下面是一个可以直接抄的配置。
先加依赖:
<dependency> <groupId>org.eclipse.paho</groupId> <artifactId>org.eclipse.paho.client.mqttv3</artifactId> <version>1.2.5</version> </dependency>然后写配置类:
@Configuration public class MqttConfig { @Value("${mqtt.broker-url}") private String brokerUrl; @Value("${mqtt.client-id}") private String clientId; @Value("${mqtt.username}") private String username; @Value("${mqtt.password}") private String password; @Bean public MqttClient mqttClient() throws MqttException { MqttClient client = new MqttClient(brokerUrl, clientId, new MemoryPersistence()); MqttConnectOptions options = new MqttConnectOptions(); options.setUserName(username); options.setPassword(password.toCharArray()); options.setCleanSession(false); options.setAutomaticReconnect(true); options.setConnectionTimeout(10); options.setKeepAliveInterval(60); client.connect(options); return client; } }这里有几个参数值得说。cleanSession 设为 false,这样 Spring Boot 服务重启后,离线期间的消息还能收到。automaticReconnect 设为 true,网络抖动时客户端自动重连。keepAliveInterval 设为 60 秒,客户端每 60 秒发一次 PING,Broker 在 1.5 倍时间内没收到就判定离线。
消费消息的代码:
@Component public class MqttMessageConsumer implements MqttCallback { @Autowired private MqttClient mqttClient; @PostConstruct public void init() throws MqttException { mqttClient.setCallback(this); mqttClient.subscribe("watermeter/+/telemetry", 1); } @Override public void messageArrived(String topic, MqttMessage message) { String payload = new String(message.getPayload()); // 解析 topic 拿到设备 ID String[] parts = topic.split("/"); String deviceId = parts[1]; // 业务处理 handleTelemetry(deviceId, payload); } @Override public void connectionLost(Throwable cause) { // 重连逻辑,Paho 的 automaticReconnect 会处理 } @Override public void deliveryComplete(IMqttDeliveryToken token) { // 发布消息的回调,消费端一般不用 } }注意 messageArrived 是单线程回调的,如果处理逻辑耗时,会阻塞后续消息。生产环境要把消息丢到线程池或者内存队列里异步处理。我一般用 Disruptor 或者 LinkedBlockingQueue 做缓冲,消费线程只负责入队,业务线程池负责处理。
3.3 设备鉴权与 ACL 配置
设备接入层必须做鉴权,不然任何人都能连上你的 Broker 发消息。EMQX 支持多种鉴权方式:用户名密码、JWT、HTTP 回调、MySQL 查询等。我一般用HTTP 回调鉴权,因为设备信息在业务数据库里,Broker 通过 HTTP 接口问 Spring Boot“这个设备合法吗”,Spring Boot 查库返回结果。
EMQX 配置 HTTP 鉴权:
# emqx.conf auth.http.auth_req.url = http://backend:8080/api/device/auth auth.http.auth_req.method = post auth.http.auth_req.params = clientid=%c,username=%u,password=%PSpring Boot 侧实现这个接口:
@PostMapping("/api/device/auth") public AuthResult auth(@RequestBody AuthRequest request) { Device device = deviceRepository.findByClientId(request.getClientid()); if (device == null) { return AuthResult.deny(); } if (!device.getPassword().equals(request.getPassword())) { return AuthResult.deny(); } return AuthResult.allow(); }ACL 控制设备能发布和订阅哪些主题。比如设备只能发布到watermeter/{自己的设备ID}/telemetry,不能发布到别的设备主题。EMQX 的 ACL 可以配 HTTP 回调,也可以配内置规则。内置规则用{clientid}占位符:
acl.rule.1 = allow pubsub watermeter/${clientid}/# acl.rule.2 = deny all这样设备只能操作自己 ID 下的主题,其他一律拒绝。
3.4 指令下发的完整链路
指令下发是设备接入层的另一个核心功能。链路是:业务系统调 Spring Boot 接口 → Spring Boot 发布 MQTT 消息到指令主题 → Broker 转发给设备 → 设备执行后回复 → Spring Boot 收到回复更新状态。
Spring Boot 发布指令:
public void sendCommand(String deviceId, String command) { String topic = "watermeter/" + deviceId + "/command"; MqttMessage message = new MqttMessage(command.getBytes()); message.setQos(1); mqttClient.publish(topic, message); }指令下发要考虑超时。设备可能不在线,消息发出去没人收。我的做法是:发布指令后,在 Redis 里存一个command:{commandId}的 key,设置 30 秒过期。设备回复时带上 commandId,Spring Boot 收到回复就删除这个 key。如果 30 秒后 key 还在,说明设备没回复,触发超时告警。
设备回复的主题是watermeter/{deviceId}/command/reply,Spring Boot 订阅watermeter/+/command/reply来收所有设备的回复。
3.5 消息持久化与规则引擎
EMQX 的规则引擎可以把 MQTT 消息直接转到 Kafka、MySQL、Redis 等。这样 Spring Boot 不用自己消费 MQTT,直接从 Kafka 消费就行,解耦更彻底。
规则引擎的 SQL:
SELECT payload.deviceId as deviceId, payload.temperature as temperature, payload.humidity as humidity, timestamp as ts FROM "watermeter/+/telemetry"动作配置成 Kafka 的 topic,EMQX 就会把匹配的消息转发过去。Spring Boot 用@KafkaListener消费:
@KafkaListener(topics = "telemetry") public void consume(String message) { TelemetryData data = JSON.parseObject(message, TelemetryData.class); telemetryService.save(data); }规则引擎的好处是削峰填谷。设备上报高峰时,消息先进 Kafka,Spring Boot 按自己的节奏消费,不会被冲垮。坏处是链路变长,延迟增加。对实时性要求高的指令回复,还是走 MQTT 直连 Spring Boot 更合适。
4. 高频面试题实战答法
4.1 设备量大了怎么扩展
这个问题考的是架构扩展能力。我的答法是分三层:接入层、消息层、存储层。
接入层用 EMQX 集群,前面挂负载均衡。设备连接时通过 LB 分发到不同 EMQX 节点。EMQX 集群内部同步路由,消息能正确转发。如果单集群扛不住,可以按业务线拆多个集群,比如水表一个集群、电表一个集群。
消息层用 Kafka 做缓冲。EMQX 规则引擎把消息转到 Kafka,Spring Boot 从 Kafka 消费。Kafka 分区数根据消费能力定,一般一个分区对应一个消费线程。如果消费跟不上,加消费者实例就行。
存储层用时序数据库,比如 TDengine 或 InfluxDB。MySQL 存设备元数据和指令记录,时序数据单独存。不要用 MySQL 存高频传感器数据,写入压力扛不住,查询也慢。
4.2 消息丢失怎么排查
消息丢失的排查要分段定位:设备到 Broker、Broker 内部、Broker 到后端。
设备到 Broker 丢消息,先看 QoS。QoS 0 本来就不保证,改成 QoS 1。然后看设备网络,弱网环境下 TCP 重传可能超时,设备端要有重发机制。
Broker 内部丢消息,看 EMQX 的日志和指标。emqx_metrics里有messages.dropped指标,如果这个值在涨,说明 Broker 在丢消息。常见原因是队列满了,max_mqueue_len默认 1000,可以调大,但根本解决办法是加快消费速度。
Broker 到后端丢消息,看 Spring Boot 的消费逻辑。如果 messageArrived 里抛异常,消息就丢了。要在回调里 try-catch,异常时记录日志并做补偿。用 Kafka 的话,关掉自动提交 offset,处理成功再手动提交。
4.3 设备频繁上下线怎么处理
设备频繁上下线,首先看 Keep Alive 设置。Keep Alive 太短,网络稍微抖动就判定离线;太长,离线检测延迟高。一般设 60 到 120 秒比较合适。
然后看遗嘱消息。设备异常断线会触发遗嘱,后端收到遗嘱后不要立刻标记设备离线,而是等一个宽限期,比如 30 秒。如果 30 秒内设备重连了,就取消离线标记。这个宽限期能过滤掉大量网络抖动导致的误报。
EMQX 里可以配置retry_interval和await_rel_timeout来控制重连行为。另外,设备端要有指数退避的重连策略,不要一秒重连一次,那样会把 Broker 冲垮。第一次 1 秒,第二次 2 秒,第三次 4 秒,最大 60 秒。
4.4 MQTT 5.0 有哪些实用新特性
MQTT 5.0 加了不少东西,面试里常问的有几个。共享订阅,多个后端实例订阅同一个主题,Broker 轮询分发消息,天然支持负载均衡。主题别名,把长主题映射成短数字,减少报文大小。用户属性,可以在消息里带自定义 key-value,不用改 payload 格式。原因码,每个确认包都带原因码,排查问题更方便。
但 5.0 的普及度还不如 3.1.1,很多设备模组只支持 3.1.1。EMQX 同时支持两个版本,按客户端连接时协商的版本走。新项目可以用 5.0,老设备兼容 3.1.1。
4.5 如何做设备影子
设备影子是设备状态的缓存。设备上报状态后,后端存一份;设备离线时,后端要下发指令,先更新影子,等设备上线后同步。
实现方式:Redis 里存shadow:{deviceId}的 hash,字段包括reported(设备上报的状态)和desired(期望状态)。设备上线后订阅shadow/{deviceId}/delta主题,后端对比 reported 和 desired,有差异就发布 delta 消息,设备收到后执行并更新 reported。
EMQX 有emqx_shadow插件,可以直接用。自己实现也不复杂,核心是 reported 和 desired 的对比逻辑。
4.6 弱网环境下怎么保证可靠
弱网环境三个手段:QoS 1 加幂等、消息缓存加重传、连接保活加退避。
QoS 1 保证至少到达,重复消息用 messageId 去重。设备端要有本地缓存,发送失败的消息存起来,网络恢复后重传。Keep Alive 适当调大,减少误判离线。重连用指数退避,避免风暴。
NB-IoT 场景还要注意PSM 和 eDRX。PSM 是省电模式,设备进入后不接收下行消息,只能等它主动上报时才能下发指令。所以 NB-IoT 设备的指令下发要设计成“等设备下次上报时携带指令”,不能指望实时推送。
4.7 如何做压测
压测设备接入层,工具用 emqtt_bench 或者 JMeter 的 MQTT 插件。emqtt_bench 是 EMQX 官方出的,能模拟大量并发连接和消息发布。
压测指标看几个:连接建立速率、消息吞吐量、消息延迟、CPU 和内存占用。连接建立速率反映 Broker 处理新连接的能力,消息吞吐量反映转发能力,延迟反映实时性。
压测时要注意:客户端和服务端不要在同一台机器,不然客户端先扛不住了。用多台压测机,每台模拟几万连接。EMQX 单节点百万连接需要调优 Linux 内核参数,比如ulimit -n调到 100 万,net.core.somaxconn调大。
4.8 如何监控接入层
监控分三层:Broker 指标、业务指标、设备指标。
Broker 指标用 EMQX 的 Prometheus 插件暴露,包括连接数、消息速率、丢弃消息数、CPU 内存等。Grafana 做面板,设置告警规则,比如连接数突降 20% 就告警。
业务指标在 Spring Boot 里用 Micrometer 埋点,比如指令下发成功率、消息处理延迟。设备指标包括在线率、上报频率、信号强度,这些从设备上报的数据里提取。
告警要分级。连接数波动是 P2,消息大量丢弃是 P1,Broker 宕机是 P0。不同级别走不同的通知渠道。
4.9 设备接入层怎么做安全
安全分四块:传输安全、鉴权、ACL、审计。
传输安全用 TLS,MQTT over TLS 默认端口 8883。设备端要预置 CA 证书,防止中间人攻击。但 TLS 会增加握手开销,低功耗设备可能扛不住,可以用 PSK 或者轻量级加密。
鉴权用一机一密,每个设备有独立的 clientId 和 password。密码不要明文存,存哈希。ACL 控制设备只能操作自己的主题。审计记录所有连接和发布订阅操作,异常行为告警。
4.10 项目里怎么答
面试官问“你项目里设备接入层怎么做的”,不要背架构图,讲一个具体问题怎么解决的。
比如:“我们水表项目,设备用 NB-IoT,上报频率一小时一次。一开始用 QoS 1,发现重复消息很多,因为 NB-IoT 信号不稳定,PUBACK 经常丢。后来改成 QoS 0 加应用层确认,设备上报后等后端回复,没收到就下次上报时带上历史数据。这样既省流量又保证最终一致。”
这种答法有场景、有问题、有方案、有取舍,面试官能看出你真的做过。
5. 踩过的坑与实操心得
5.1 客户端 ID 冲突导致连接互踢
MQTT 协议规定,同一个 clientId 同时只能有一个连接。如果两个设备用了相同的 clientId,后连的会把先连的踢下线。我见过一个项目,设备出厂时 clientId 写死成模组 IMEI,结果一批模组 IMEI 重复,设备上线后互相踢,表现就是设备频繁上下线。
解决办法:clientId 用设备唯一标识,比如 SN 码或者 MAC 地址。如果设备端改不了,Broker 侧可以做映射,但最好从源头解决。EMQX 可以配置max_clientid_len和clientid_override,但不建议依赖这个。
5.2 大 payload 导致 Broker 内存暴涨
MQTT 默认最大 payload 是 256MB,但实际生产环境不可能传这么大。我见过有人用 MQTT 传图片,一张几百 KB,并发一上来 Broker 内存直接打满。
解决办法:在 EMQX 里限制max_packet_size,比如 64KB。大文件走 HTTP 上传到对象存储,MQTT 只传 URL。这个限制要在设备端和后端都对齐,不然设备发了大包被 Broker 拒绝,设备端不知道原因。
5.3 订阅关系没清理导致消息堆积
cleanSession = false 的会话,如果设备再也不上线,Broker 会一直为它缓存消息。时间长了,这些僵尸会话占大量内存。
解决办法:MQTT 5.0 用 Session Expiry Interval,3.1.1 用 EMQX 的session_expiry_interval配置。另外,后端要定期清理长期不活跃的设备,把它们的会话删掉。EMQX 有 REST API 可以查会话列表,写个定时任务清理。
5.4 Spring Boot 消费线程阻塞
前面提过,Paho 的 messageArrived 是单线程回调。如果处理逻辑里有数据库操作或者 HTTP 调用,耗时几百毫秒,消息就会堆积。
解决办法:messageArrived 里只做入队,业务逻辑用线程池处理。线程池大小根据 IO 密集程度定,一般 CPU 核数乘以 2 到 4。队列用有界队列,满了就拒绝,配合监控告警。
5.5 时间戳不同步导致数据乱序
设备上报的数据带时间戳,如果设备时间不准,后端按时间戳排序就会乱。我见过设备时间停在 1970 年,数据全排到最前面。
解决办法:后端收到消息后,用服务端时间做主要时间戳,设备时间作为参考字段。如果业务需要设备时间,要在设备端做 NTP 对时,或者后端做时间偏移校正。
5.6 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决办法 |
|---|---|---|---|
| 设备频繁上下线 | clientId 冲突 | 看 Broker 日志有没有 kick | 改用唯一 clientId |
| 消息丢失 | QoS 0 或订阅 QoS 低 | 检查发布和订阅的 QoS | 改成 QoS 1 |
| Broker 内存高 | 僵尸会话或大 payload | 看会话数和消息大小分布 | 清理会话,限制包大小 |
| 消费延迟高 | 回调线程阻塞 | 看消费线程堆栈 | 异步处理,加线程池 |
| 指令下发超时 | 设备离线或 QoS 0 | 看设备在线状态 | 用 QoS 1,加超时重试 |
| 集群消息转发慢 | 节点间网络延迟高 | ping 节点间延迟 | 同机房部署 |
5.7 一个实用的调试技巧
调试 MQTT 问题时,我常用mosquitto_sub和mosquitto_pub命令行工具。订阅所有主题看消息流:
mosquitto_sub -h broker-host -p 1883 -u username -P password -t '#' -v-v会打印主题名,方便看消息发到了哪个主题。发布测试消息:
mosquitto_pub -h broker-host -p 1883 -u username -P password -t 'watermeter/dev001/command' -m '{"action":"read"}' -q 1这两个命令在排查“消息到底有没有到 Broker”“主题对不对”时非常有用。EMQX 的 Dashboard 里也有 WebSocket 客户端,可以直接在浏览器里订阅发布,不用装工具。
设备接入层这块,面试问来问去就是那些东西,但真正拉开差距的是你有没有在生产环境里被坑过。QoS 选错导致重复计费、clientId 冲突导致设备互踢、僵尸会话吃光内存,这些坑踩过一次就忘不了。面试时把这些经历讲出来,比背十页协议规范都管用。