跳转至

深入专题:MQTT 与设备状态

这篇把协议层连接、MQTT 会话、业务在线状态、设备影子和命令回执放在同一张图中。

很多 IoT 系统的问题来自把不同层的“状态”混为一谈:

TCP 连接状态
MQTT Session 状态
设备业务在线状态
设备实际运行状态
平台期望状态
命令执行状态

它们有关联,但不是同一个事实。

第一层:从设备建立连接开始

简化流程:

设备
→ TCP 连接
→ 可选 TLS 握手
→ MQTT CONNECT
→ Broker 鉴权与授权
→ CONNACK
→ 订阅命令 Topic
→ 开始发布心跳和遥测

TCP 已连接

只说明两端存在传输连接。设备业务进程可能卡住,仍未及时上报有效数据。

MQTT 已连接

说明 Broker 接受了客户端身份和连接参数。仍不代表设备所有传感器、业务模块都健康。

业务在线

应该由产品定义:

最近 90 秒收到有效心跳,并且身份未冻结,视为在线。

业务在线是推导状态,不是协议直接给出的永恒真相。

Client ID 与身份

Broker 使用 Client ID 标识 MQTT 客户端会话,但 Client ID 本身不是安全凭据。

如果两个连接使用相同 Client ID,很多 Broker 会让新连接替换旧连接。可能出现:

  • 设备被错误配置成相同 ID;
  • 攻击者抢占会话;
  • 设备快速重连导致旧连接被踢;
  • 在线/离线事件快速抖动。

需要同时设计:

  • Client ID 唯一性;
  • 每设备认证凭据;
  • Topic ACL;
  • 重复连接策略;
  • 审计。

QoS 1 为什么会重复

QoS 1 简化交互:

Publisher → PUBLISH(packet_id=10)
Broker    → PUBACK(packet_id=10)

如果 Broker 已收到并处理 PUBLISH,但 PUBACK 丢失:

Publisher 不知道 Broker 已收到
→ 重发 PUBLISH,DUP=1

协议层根据 packet_id 处理当前会话中的交付流程,但业务系统仍可能在 Broker 之后收到重复投递,尤其跨重连、桥接、规则转发和业务 MQ 时。

业务消息需要独立 message_id:

{
  "message_id": "device-1001-boot-72-seq-8801",
  "device_id": "1001",
  "boot_id": 72,
  "seq": 8801,
  "timestamp": 1753761600
}

boot_id + seq 帮助区分设备重启后的序号重新开始。

QoS 2 为什么也不等于业务恰好一次

QoS 2 提供 MQTT 协议交付范围内的 exactly once 流程,但后续链路可能是:

Broker
→ Rule Engine
→ HTTP
→ MQ
→ Consumer
→ Database

任意后续组件仍可能重试或重复。因此关键业务仍使用幂等。

Session 解决断线后的什么

持久 Session 可以保存:

  • 订阅;
  • 未完成 QoS 1/2 流程;
  • 离线期间符合条件的消息。

Session 不等于消息历史数据库,也不能无限保存。

需要配置:

  • Session Expiry;
  • 离线消息上限;
  • 消息过期;
  • 单设备积压;
  • 重连后的流量控制。

一台设备离线一周后重连,如果补发海量过期命令,可能造成危险。

Retained Message 与设备影子

Retained Message 可以保存 Topic 最后一条值:

device/1001/config → {"interval": 30}

它适合快速把最新值给新订阅者,但完整设备影子还需要:

  • desired;
  • reported;
  • version;
  • 更新时间;
  • 修改来源;
  • 权限;
  • 冲突处理;
  • 历史。

Retain 是协议能力,影子是业务模型。可以配合,不能等同。

Will 与离线判断的竞态

场景:

10:00:00 网络断开
10:00:02 Broker 发布 Will=offline
10:00:03 设备通过 4G 重连
10:00:04 新心跳 online
10:00:05 旧离线事件晚到状态服务

如果状态服务无条件写 offline,最终状态错误。

解决需要状态版本:

每次连接/启动生成 connection_epoch 或 session_epoch
状态事件携带 epoch 和时间
只接受不旧于当前版本的变化

也可以在处理离线前再次检查最新连接和 last_seen。

心跳、Keep Alive 与遥测

Keep Alive

协议层判断连接空闲是否异常。

业务心跳

可以携带:

  • boot_id;
  • 固件版本;
  • uptime;
  • 网络质量;
  • 存储/电量;
  • 业务模块健康;
  • 时间同步状态。

遥测

业务数据到达也能证明设备在某个时间点活跃,但若某类设备长时间无遥测,不能只靠遥测判断在线。

在线判断可以组合多信号:

最新有效心跳
+ Broker 连接状态
+ 最近遥测
+ 设备冻结状态

最终规则必须可解释。

TTL 方案的内部边界

每次心跳:

SET device:last_seen:1001 <timestamp> EX 90

查询 key 存在非常快,但要理解:

  • Redis 过期不是严格实时调度;
  • key 消失不会自动更新数据库;
  • key 消失不会自动从 Set 移除;
  • Keyspace Notification 不应成为唯一可靠业务事件;
  • Redis 故障后 key 需要重建。

因此完整设计常见两条路径:

查询路径:
Redis 快速回答当前状态

迁移路径:
ZSet/时间轮/扫描 Worker
→ 确认超时
→ 状态机迁移
→ 发离线事件

ZSet 超时调度

member = device_id
score = expected_offline_at

心跳:

ZADD offline_deadlines new_deadline device_id

Worker:

取 score <= now 的成员
→ 领取
→ 再次检查当前 deadline
→ 如果仍超时,执行 online → offline

为什么再次检查?

设备可能在 Worker 取出后、真正处理前刚好重连并更新 deadline。

并发 Worker 需要:

  • 分片;
  • 领取 lease;
  • 条件更新;
  • 幂等迁移;
  • 失败重试。

命令下发的完整语义

平台要重启设备:

1. 创建 command_id
2. 检查用户、租户、设备权限
3. 保存 created
4. 发布 MQTT command
5. Broker 接管 → dispatched
6. 设备收到 → received
7. 设备开始 → executing
8. 重启后上报 → succeeded

Broker PUBACK 只能帮助确认第 5 步附近的协议交付,不能代替第 8 步业务回执。

重复命令

设备端也需要记录最近 command_id:

已执行 → 返回已有结果
执行中 → 返回当前状态
新命令 → 执行

只在云端幂等不够,因为云端可能重新发布。

大规模同时重连

十万设备因网络恢复在一分钟重连:

约 1,667 次连接/秒

每次可能触发:

  • TLS;
  • 鉴权数据库/缓存;
  • Session 恢复;
  • 多个订阅;
  • 上线事件;
  • 补发消息;
  • 配置同步。

连接本身不是唯一负载。要限制:

  • 随机重连退避;
  • 每设备离线队列;
  • 会话和消息过期;
  • 鉴权缓存;
  • 状态事件去抖;
  • 下游消费并发。

完整状态模型

连接事实:
connected/disconnected + connection_epoch

业务在线:
unknown/offline/online + last_seen + version

设备影子:
desired_version / reported_version

命令:
created/dispatched/received/executing/succeeded/failed/timeout

不要用一个 status 字段承载所有含义。

深度自测

  1. TCP connected、MQTT connected、业务 online 为什么不同?
  2. QoS 1 的重复发生在哪个故障窗口?
  3. QoS 2 为什么不保证数据库业务端到端恰好一次?
  4. Retained Message 与设备影子的差别是什么?
  5. Will 离线事件晚到时怎样避免覆盖新上线?
  6. TTL key 为什么不能独立产生可靠离线事件?
  7. ZSet Worker 为什么在迁移前再次检查 deadline?
  8. Broker PUBACK 为什么不能表示设备已经重启?
参考答案要点
  1. 分别是传输、协议会话和业务规则推导的不同层事实。
  2. PUBLISH 已处理而 PUBACK 丢失,发布者会重发。
  3. Broker 后仍有规则、HTTP、MQ、消费者和数据库等重试边界。
  4. Retain 只保存 Topic 最后一条;影子包含 desired/reported、版本、权限和冲突。
  5. 使用连接 epoch/状态版本,或迁移前检查最新连接和 last_seen。
  6. 过期不严格实时,也不会自动执行数据库迁移和集合维护。
  7. 设备可能在领取和执行之间已经刷新心跳。
  8. 它只确认协议交付,不确认设备业务动作完成。

完成标准

  • 能画出六种不同状态
  • 能推导 QoS 1 重复
  • 能设计带 boot_id/seq 的业务消息
  • 能处理 Will 与重连竞态
  • 能设计 TTL 查询路径和 ZSet 迁移路径
  • 能设计命令回执和设备端幂等