MQTT 协议与 Broker 实践

本文讲解 MQTT 协议报文语义与 Broker 工程实践,覆盖 CONNECT 与 CONNACK 字段、QoS 0/1/2 的确认流程与开销、订阅通配符、Retain 与遗嘱消息、持久会话与离线消息,以及 MQTT 5.0 的共享订阅与主题别名等新特性。文章给出十六进制报文示例、Topic 命名规范、EMQX 等 Broker 选型对照、ACL 与 TLS 加固要点。

引言

设备接入层几乎绕不开 MQTT。它用极简的报文头与发布订阅模型,把设备与应用解耦,让几十万连接能在单集群上稳定共存。但协议简单不等于工程简单:QoS 语义理解偏差会导致重复扣费或数据丢失,会话配置不当会在重连风暴时打爆 Broker 内存,Topic 设计失控会让 ACL 与订阅匹配变成性能瓶颈。

本文按「报文语义到 Broker 运维」的顺序展开。先讲清发布订阅模型的价值,再逐个拆解 CONNECT、PUBLISH、SUBSCRIBE 的字段与确认流程,然后覆盖 Retain、遗嘱、持久会话这些容易误用的机制,最后落到 MQTT 5.0 新特性、Topic 规范、Broker 选型与压测。

协议栈选型上,MQTT 与 CoAP 的定位差异已在架构篇说明;受限设备上的对象模型与注册流程见 CoAP 与 LwM2M 受限设备协议 。安全加固的通用原则见 物联网安全加固 。

目录

  1. 发布订阅模型与解耦价值
  2. CONNECT 与 CONNACK 报文
  3. PUBLISH 与 QoS 0/1/2
  4. SUBSCRIBE 与主题通配符
  5. Retain 与遗嘱消息
  6. 持久会话与离线消息
  7. MQTT 5.0 新特性
  8. Topic 设计规范
  9. Broker 选型与集群
  10. 认证授权与 TLS
  11. 压测与关键指标
  12. 桥接与规则引擎集成

1. 发布订阅模型与解耦价值

发布订阅模型的核心是发布者与订阅者互不知晓对方存在,只通过主题这一层间接寻址。这带来三个工程收益。

第一,连接解耦。设备只维护到 Broker 的一条连接,无论后端有几个消费系统。新增一个告警服务不需要动设备固件,只需新增一个订阅。

第二,时间解耦。发布者与订阅者不必同时在线,配合持久会话可以把离线期间的消息补发。

第三,空间解耦。设备只需知道主题命名规则,不需要知道消费方的地址与端口,天然适配弹性伸缩的后端。

设备 A --PUBLISH--> topic: plant/line1/dev9/temp
设备 B --PUBLISH--> topic: plant/line1/dev10/temp
Broker  --分发--> 订阅者1: plant/line1/+/temp    (监控)
Broker  --分发--> 订阅者2: plant/#               (归档)
Broker  --分发--> 订阅者3: plant/line1/dev9/temp (告警)

代价是 Broker 成为核心组件,它的可用性直接决定整条链路。这也解释了为什么 Broker 需要集群、桥接与多级部署。

2. CONNECT 与 CONNACK 报文

客户端与 Broker 建立连接的第一步是发送 CONNECT,Broker 回 CONNACK。这是唯一一次可以携带身份与初始会话配置的交互,字段设置决定了后续所有行为。

字段作用工程取值建议
Protocol Name协议名,固定 MQTT5.0 客户端填 MQTT
Protocol Level版本号,4 为 3.1.1,5 为 5.0新项目用 5.0
ClientId客户端标识,需全局唯一用设备 ID,长度控制在 23 到 64
Clean Start是否丢弃旧会话常连设备设 0,短连设备设 1
Keep Alive心跳间隔秒数取平台会话超时的 1/1.5 到 1/2
Will Flag是否设置遗嘱有离线检测需求时开启
Will Topic / Payload遗嘱主题与内容用状态主题,载荷带设备 ID
Username / Password认证凭据优先证书认证,密码走 TLS
Session Expiry Interval会话保留时长(5.0)按业务离线容忍窗口设置

CONNACK 只回两个关键字段:Session Present 表示是否复用了已有会话,Reason Code 表示连接结果。Session Present 为 0 但客户端以为有会话时,说明服务端已丢弃状态,客户端应重新订阅。

CONNECT 报文(MQTT 3.1.1,ClientId=dev9,KeepAlive=30)
10 1f                                  -- 固定头:类型 0x10,剩余长度 31
  00 04 4d 51 54 54                   -- 协议名 "MQTT"
  04                                  -- 协议级别 4
  02                                  -- 连接标志:CleanSession=0
  00 1e                               -- Keep Alive = 30
  00 04 64 65 76 39                   -- ClientId "dev9"

3. PUBLISH 与 QoS 0/1/2

QoS 决定消息投递保证与开销,是 MQTT 最常被误解的部分。

QoS语义交互流程开销适用场景
0最多一次发送即结束1 个报文高频遥测,丢点可接受
1至少一次PUBLISH / PUBACK2 个报文事件上报,可容忍重复
2恰好一次PUBLISH / PUBREC / PUBREL / PUBCOMP4 个报文计费、指令下发

QoS 1 的重复来自重传:客户端未收到 PUBACK 会重发,Broker 可能已处理。因此 QoS 1 的业务侧必须做幂等,通常用消息内的自增序号或业务 ID 去重。

QoS 2 用两次握手消除重复,但代价是四次报文往返与 Broker 侧的状态存储,在高吞吐场景会显著增加内存与延迟。

QoS 2 完整流程
Client --PUBLISH--> Broker    (带 PacketId)
Client <--PUBREC--  Broker
Client --PUBREL-->  Broker
Client <--PUBCOMP-- Broker    至此双向确认完成,可丢弃状态

注意:QoS 描述的是客户端与 Broker 之间的保证,不是端到端保证。Broker 到订阅者的投递 QoS 取订阅时协商值与发布值中的较小者,因此设备发 QoS 2、订阅方订 QoS 0,最终仍可能丢。

4. SUBSCRIBE 与主题通配符

订阅通过 SUBSCRIBE 报文声明主题过滤器,Broker 用 SUBACK 返回每个过滤器的授权结果与授予 QoS。

通配符只有两个,规则严格:

  • + 匹配单层,如 plant/+/temp 匹配 plant/line1/temp,不匹配 plant/line1/dev9/temp。
  • # 匹配多层,只能出现在末尾,如 plant/# 匹配 plant 下所有层级。
合法:plant/+/temp      plant/line1/#      #
非法:plant/#/temp      plant/line1/temp#  plant+ /temp

订阅是叠加的,同一客户端多次订阅匹配同一消息时,默认只收到一条(由 Broker 决定是否按最高 QoS 投递)。MQTT 5.0 引入订阅选项,可控制是否接收 Retain 消息、是否开启无本地转发(No Local),后者在双向通信的网关场景很有用,避免自己发的消息被自己收到。

4.1 通配符的性能影响

# 订阅会匹配海量主题,若同时有大量此类订阅,Broker 的主题匹配开销会显著上升。工程上限制应用侧只能订阅明确前缀,把全局订阅收敛到规则引擎内部完成。

5. Retain 与遗嘱消息

Retain 让 Broker 为每个主题保留最后一条消息,新订阅者订阅后立即收到,不必等待下一次发布。它解决的是「状态类主题新订阅者拿不到当前值」的问题。

Retain 语义
- 发布时 Retain=1,Broker 保存该主题最后一条消息
- 新订阅者订阅后立即收到保留消息
- 发布空载荷且 Retain=1,表示清除该主题的保留消息

用法上有两条纪律:状态主题(在线状态、配置)用 Retain,事件主题(告警、日志)不要用,否则新订阅者会被历史事件淹没。清除保留消息必须发空载荷,仅仅停止发布不会清除。

遗嘱消息(LWT)在客户端异常断开时由 Broker 代为发布,用于检测离线。它只在非正常断开时触发,客户端主动发 DISCONNECT 不会触发遗嘱,这是排查「遗嘱没生效」时最常见的误解。

Will 配置示例
Will Topic   : plant/line1/dev9/status
Will Payload : {"did":"dev9","online":false}
Will QoS     : 1
Will Retain  : 1

配合 Retain,遗嘱可以让在线状态主题始终反映最新状态:上线时发 online true,掉线时 Broker 发 online false。

6. 持久会话与离线消息

会话状态包含订阅列表、未确认消息与离线队列。MQTT 3.1.1 用 Clean Session 控制,MQTT 5.0 拆成 Clean Start 与 Session Expiry Interval。

Clean Start 决定连接时是否丢弃已有会话,Session Expiry Interval 决定断开后会话保留多久。二者分离的价值在于:可以让设备每次连接都从干净状态开始,同时让会话在断开后保留一段时间以接收离线消息。

MQTT 5.0 会话配置示例
Clean Start            = 1    连接时丢弃旧会话
Session Expiry Interval= 3600 断开后保留 1 小时
效果:重连后 Session Present=0,但断开期间的消息进入离线队列

风险在于离线队列无上限。十万台设备离线一天、每台积压一万条消息,Broker 内存会被瞬间吃光。工程上必须设置单客户端队列上限与消息 TTL,超限时丢弃最旧消息或直接拒绝。

7. MQTT 5.0 新特性

MQTT 5.0(2019 年 OASIS 标准)补齐了 3.1.1 在可观测性与大规模运维上的短板,生产环境建议直接用 5.0。

  • Reason Code:CONNACK、PUBACK、SUBACK 都带原因码,如 0x80 未指定错误、0x87 未授权、0x97 配额超限,排障不再靠猜。
  • User Properties:报文可携带自定义键值对,用于链路追踪与灰度标记,不污染主题。
  • Topic Alias:用两字节别名替代重复的长主题名,窄带场景可省 30% 以上带宽。
  • Flow Control:通过 Receive Maximum 声明在途消息上限,避免快发方压垮慢收方。
  • Shared Subscription:$share/group/topic 让多个订阅者负载均衡消费同一主题,天然支持水平扩展。
  • Request/Response:用 Response Topic 与 Correlation Data 实现请求响应语义,替代手工拼主题。
共享订阅示例
订阅:$share/workers/plant/line1/+/temp
效果:同组 workers 内多个消费者分摊消息,每条消息只投递给其中一个
注意:与普通订阅混用时语义不同,普通订阅是广播

共享订阅是后端消费扩展的关键机制,配合规则引擎可以把「设备接入」与「业务消费」彻底解耦。

8. Topic 设计规范

Topic 是 MQTT 的命名空间,也是 ACL 与计费的基础,设计失控后极难迁移。

推荐的分层结构:

{租户}/{区域}/{设备类型}/{设备ID}/{数据类别}
例:acme/cn-north/th-sensor/dev9f3a/telemetry
    acme/cn-north/th-sensor/dev9f3a/status
    acme/cn-north/th-sensor/dev9f3a/cmd

设计纪律:

  • 层级从左到右由粗到细,便于 ACL 按前缀授权。
  • 设备 ID 放中间,避免同一设备的不同类别分散在多个前缀下。
  • 不用通配符字符作为主题名的一部分,避免歧义。
  • 主题长度控制在 128 字节内,长主题会放大内存与匹配开销。
  • 下行指令与上行数据用不同末级(cmd 与 telemetry),便于限流隔离。

反例是把时间戳或随机数放进主题,导致主题数无限增长,Broker 的主题表膨胀,内存持续上涨。

9. Broker 选型与集群

Broker语言协议集群适用场景
EMQX 5.xErlangMQTT 3.1.1/5.0、CoAP、LwM2M原生集群大规模接入,百万连接
Mosquitto 2.xCMQTT 3.1.1/5.0无原生集群边缘、小规模、调试
HiveMQJavaMQTT 3.1.1/5.0企业版集群企业级、强支持
VerneMQErlangMQTT 3.1.1/5.0原生集群自建、需定制
NanoMQCMQTT 3.1.1/5.0、桥接边缘为主边缘网关、低占用

EMQX 是自建大规模接入的主流选择,5.x 用 Erlang/OTP 实现,单集群可支撑百万级连接,支持规则引擎、桥接与插件扩展。Mosquitto 轻量但无原生集群,适合边缘节点与开发环境。NanoMQ 面向边缘,内存占用可低至几 MB,适合跑在网关上做协议转换。

9.1 集群与分片

集群的关键问题是会话归属。EMQX 用一致性哈希把 ClientId 映射到节点,设备重连若落到不同节点需要迁移会话。跨机房部署时,建议按设备 ID 前缀做分区,把同一批设备固定到同一区域,减少跨区状态同步。

9.2 桥接

桥接用于多级部署:边缘 Broker 把消息转发到云端 Broker。配置要关注主题前缀映射、QoS 与断线重连策略。

bridges:
  mqtt:
    cloud:
      server: "mqtts://cloud-broker:8883"
      clientid: "edge-gw-01"
      forwards: ["plant/#"]
      clean_start: false
      keepalive: "60s"
      retry_interval: "10s"

10. 认证授权与 TLS

默认匿名接入是最大的风险源。生产环境必须做到三点:认证、授权、加密。

认证方式按强度排序:双向 TLS 证书(X.509)优于 Token(JWT)优于用户名密码。设备侧推荐一机一证,证书 CN 绑定设备 ID,便于撤销。

授权用 ACL 按主题前缀限制读写:

ACL 规则示例
allow  dev9f3a  publish    acme/cn-north/th-sensor/dev9f3a/telemetry
allow  dev9f3a  publish    acme/cn-north/th-sensor/dev9f3a/status
allow  dev9f3a  subscribe  acme/cn-north/th-sensor/dev9f3a/cmd
deny   all      subscribe  #

关键纪律是最小权限:设备只允许发布自己的主题、只允许订阅自己的指令主题,禁止 # 订阅。否则一台被攻陷的设备可以窃听全网数据。加密方面,MQTT over TLS 用 8883 端口,禁用 1883 明文;TLS 1.3 可减少握手往返,弱网设备收益明显。证书轮换与吊销流程要在设备生命周期管理中提前设计,与 OTA 机制配合。

11. 压测与关键指标

上线前必须压测,重点验证连接建立速率、消息吞吐与尾延迟三项。

emqtt_bench conn -h broker.local -p 1883 -c 50000 -i 10   # 建 50000 连接,速率 10/s

emqtt_bench pub -h broker.local -p 1883 -c 200 -I 10 -t "bench/%i" -s 128 -q 1
emqtt_bench sub -h broker.local -p 1883 -c 10 -t "bench/#" -q 1

emqx ctl stats        # 连接数、消息进出速率
emqx ctl broker       # 会话、订阅、路由统计

关键指标口径:

  • 连接建立速率:每秒新建连接数,决定重连风暴时的恢复能力。
  • 消息吞吐:msg/s,区分入站与出站,两者常相差数倍。
  • P99 延迟:发布到订阅的端到端时延,均值无意义,要看 P99。
  • 内存与队列深度:单连接内存占用与离线队列长度,是雪崩的先行指标。

压测要覆盖故障场景:Broker 重启后重连风暴、网络抖动下的会话迁移、慢消费者导致的队列堆积。Broker 的事件驱动模型决定了它在连接密集场景下的性能,理解事件循环与文件描述符管理有助于定位瓶颈,相关机制可参考 事件驱动网络编程 。

12. 桥接与规则引擎集成

Broker 只是通道,真正的业务价值在规则引擎与下游系统。以 EMQX 为例,规则引擎用类 SQL 语法做过滤、转换与投递。

SELECT
  payload.temp AS temp,
  clientid AS did,
  timestamp AS ts
FROM
  "acme/+/+/+/telemetry"
WHERE
  payload.temp > 40

规则可以投递到多种下游:Webhook、Kafka、TSDB、对象存储、另一个 MQTT 主题。工程上把「过滤与路由」放规则引擎,「业务逻辑」放后端服务,避免在设备侧写业务规则。

与设备影子的配合是常见模式:设备上报状态主题,规则引擎写入影子存储,应用读取影子获取最新状态而不必订阅原始主题,一致性模型见 设备影子与设备管理 。

权衡取舍

  • QoS 等级:QoS 0 省资源但会丢,QoS 2 保证强但开销四倍,多数场景 QoS 1 加业务幂等最划算。
  • 会话保留时长:越长离线消息越完整,但 Broker 内存压力越大,按业务离线容忍窗口设。
  • Retain 使用:状态类主题必开,事件类主题开了会污染新订阅者。
  • 主题粒度:粒度细便于 ACL 与限流,但主题数量膨胀增加匹配开销。
  • 共享订阅:解决消费扩展,但与普通订阅语义混用易误判为丢消息。
  • Broker 选型:EMQX 功能全但资源占用高,Mosquitto 轻量但无集群,边缘与云端应分开选。

常见坑清单

  • 遗嘱不触发:现象是设备掉线但状态未更新,原因是设备主动发 DISCONNECT 或 Keep Alive 内正常保活,规避方法是区分正常与异常断开的判定逻辑。
  • QoS 1 重复消费:现象是数据重复入库,原因是不知 QoS 1 会重传,规避方法是用业务 ID 做幂等。
  • 误以为 QoS 是端到端:现象是设备发 QoS 2 仍丢数据,原因是订阅侧协商成 QoS 0,规避方法是核对订阅授予 QoS。
  • Clean Start 设错:现象是每次重连都收不到离线消息,原因是设成 1 且未配 Session Expiry,规避方法是按需分离两个参数。
  • 离线队列无上限:现象是 Broker 内存暴涨 OOM,原因是持久会话无限积压,规避方法是设置队列上限与消息 TTL。
  • 设备用 # 订阅:现象是数据越权可见,原因是 ACL 未限制订阅范围,规避方法是按前缀授权并禁止全局订阅。
  • 主题含随机串:现象是 Broker 内存持续上涨,原因是主题表无限膨胀,规避方法是固定主题结构。
  • Keep Alive 过大:现象是设备上下线抖动,原因是心跳大于平台会话超时,规避方法是按 1.5 倍关系取值。
  • 共享订阅与普通订阅混用:现象是消息时而广播时而单发,原因是语义不同,规避方法是同组统一用 $share/。
  • 明文端口对外:现象是被扫描出现异常连接,原因是 1883 未关闭,规避方法是只开放 8883 并强制 TLS。

小结

MQTT 的工程价值来自发布订阅带来的三重解耦,而它的复杂度集中在 QoS 语义、会话状态与主题治理三处。理解 CONNECT 的每个字段、QoS 的确认流程与开销、会话参数的分离设计,是避免线上事故的基础。

Broker 层面,EMQX 适合大规模自建,Mosquitto 与 NanoMQ 适合边缘与调试。集群的核心是会话归属,桥接的核心是断线续传与主题映射。安全上必须一机一证加最小权限 ACL,禁止匿名与全局订阅。

下一步建议横向对比受限设备协议,理解 CoAP 与 LwM2M 在功耗与对象模型上的取舍;再结合设备影子把状态管理与消息通道分开,最后用规则引擎把接入与业务解耦。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「物联网」更多文章

  1. 工业物联网协议与网关
  2. 边缘 AI 推理
  3. 设备配网与批量运维