引言
设备接入层几乎绕不开 MQTT。它用极简的报文头与发布订阅模型,把设备与应用解耦,让几十万连接能在单集群上稳定共存。但协议简单不等于工程简单:QoS 语义理解偏差会导致重复扣费或数据丢失,会话配置不当会在重连风暴时打爆 Broker 内存,Topic 设计失控会让 ACL 与订阅匹配变成性能瓶颈。
本文按「报文语义到 Broker 运维」的顺序展开。先讲清发布订阅模型的价值,再逐个拆解 CONNECT、PUBLISH、SUBSCRIBE 的字段与确认流程,然后覆盖 Retain、遗嘱、持久会话这些容易误用的机制,最后落到 MQTT 5.0 新特性、Topic 规范、Broker 选型与压测。
协议栈选型上,MQTT 与 CoAP 的定位差异已在架构篇说明;受限设备上的对象模型与注册流程见 CoAP 与 LwM2M 受限设备协议 。安全加固的通用原则见 物联网安全加固 。
目录
- 发布订阅模型与解耦价值
- CONNECT 与 CONNACK 报文
- PUBLISH 与 QoS 0/1/2
- SUBSCRIBE 与主题通配符
- Retain 与遗嘱消息
- 持久会话与离线消息
- MQTT 5.0 新特性
- Topic 设计规范
- Broker 选型与集群
- 认证授权与 TLS
- 压测与关键指标
- 桥接与规则引擎集成
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 | 协议名,固定 MQTT | 5.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 / PUBACK | 2 个报文 | 事件上报,可容忍重复 |
| 2 | 恰好一次 | PUBLISH / PUBREC / PUBREL / PUBCOMP | 4 个报文 | 计费、指令下发 |
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.x | Erlang | MQTT 3.1.1/5.0、CoAP、LwM2M | 原生集群 | 大规模接入,百万连接 |
| Mosquitto 2.x | C | MQTT 3.1.1/5.0 | 无原生集群 | 边缘、小规模、调试 |
| HiveMQ | Java | MQTT 3.1.1/5.0 | 企业版集群 | 企业级、强支持 |
| VerneMQ | Erlang | MQTT 3.1.1/5.0 | 原生集群 | 自建、需定制 |
| NanoMQ | C | MQTT 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 在功耗与对象模型上的取舍;再结合设备影子把状态管理与消息通道分开,最后用规则引擎把接入与业务解耦。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。