做个坦诚的开场:你要是只用 MQTT 玩过几十个设备,那“百万级智能锁平台”听上去就是个标题党。但这几年智能门锁出货量上探到千万级之后,协议选型、报文设计、Topic 字典这些事已经没法靠拍脑袋硬扛了。我自己带过锁端和云端联调的项目,真枪实弹压过百万连接之后回头看,最容易被低估的其实只有两件事:MQTT 报文里每一字节的含义,以及 Topic 命名设计对网关处理资源的影响。这篇内容就把这两块彻底摊开,从协议头部逐字节拆到 Topic 字典的全链路规划,适合正在做 IoT 接入层、准备从十几万台往上冲规模、或者被线上丢消息搞到头秃的人参考。
先说结论:智能锁平台用 MQTT 做接入通道,不是技术炫技,是被场景逼出来的。锁端资源小、网络环境差、用户对开锁时延敏感、固件升级和实时状态上报又多,需要一套能同时满足“轻量”、“可靠”、“支持发布订阅解耦”的协议。MQTT 恰好每个点都命中,而且生态成熟,从锁端 SDK 到服务端 Broker 都有大量可用组件,团队不需要从零造轮子。但“协议选对了”和“用得对不对”是两码事,报文没控制好、Topic 设计得随心所欲,照样会把百万级平台拖垮。
1. 先说顶层设计:为什么智能锁场景逃不开 MQTT
1.1 锁终端特征的约束
智能锁不是手机,它身上的约束写得很死。主控 MCU 通常只有几百 KB 内存,很多跑不了完整 TLS 栈,网络模块可能是 2G/4G 或者 NB-IoT,有的甚至走蓝牙网关代理上报。这意味着锁端和云端之间的通信协议必须满足三个条件:控制报文头开销足够小、支持断线重连和消息补发、能在低带宽高延迟链路里稳定工作。MQTT 的固定报头最小只需要 2 字节,一个 PUBLISH 消息在 QoS 0 模式下连主题带负载全部算进去,几十字节就能搞定,这对锁端 flash 和流量套餐都是实打实的友好。
再往产品形态上看,智能锁业务天然是“事件驱动”的。用户按指纹、输密码、远程下发临时密码、管理员解绑用户,锁端只需要把这些离散动作变成消息发出去,而不是像 HTTP 那样频繁轮询。MQTT 的发布订阅模型刚好匹配:锁端只上报事件,服务端按需订阅,两者通过 Broker 解耦,谁都不用关心对方在不在线。这个模型还有一个额外好处:同一把锁的数据可以被多个消费者同时订阅,比如业务订单系统、风控引擎、用户 App 推送服务,大家各取所需,不会因为消费逻辑不同而互相阻塞。
1.2 部署架构里 MQTT 的位置
在百万级锁平台里,MQTT 不是孤零零的一个东西,它的上下游关系通常长这样:锁端 SDK 通过 TCP/TLS 连到接入网关集群,接入网关背后是 Broker 集群,Broker 把消息按 Topic 路由给消费端。消费端可能是规则引擎、实时计算任务或者普通的业务 API 服务。所以从部署角度说,MQTT 在整个架构里承担的是“长连接接入 + 消息路由”的职责,帮助业务侧把“设备连接管理”这件事抽象掉,让后端服务只需要面对 Topic 和方法,而不是直接处理十万级 TCP 连接的保活和粘包问题。
这里有个很关键的设计取舍:Broker 选型不能只看开源项目 Star 数,要考察它对海量连接的管理能力和 Topic 匹配算法效率。部分老牌 Broker 在几十万连接时 Topic 匹配的 CPU 消耗会直线上升,因为通配符匹配是逐级扫描的,Topic 树维护不好,整个集群的吞吐就塌了。我自己比较倾向在生产环境用支持共享订阅和连接多租户隔离的方案,配合前置的接入网关做协议适配、限流和连接鉴权,Broker 专心做路由转发,压力小很多,问题也更好排查。
2. 逐字节拆解 MQTT 报文:你真看懂过一帧数据吗
2.1 MQTT 报文的骨架
MQTT 报文的构成分三层:固定报头(Fixed Header)、可变报头(Variable Header)和负载(Payload)。固定报头是所有报文都有的,至少 2 字节,第一字节拆成四段来看:bit 7-4 是报文类型,bit 3 是 DUP 标志,bit 2-1 是 QoS 等级,bit 0 是 RETAIN 标志。第二字节是剩余长度(Remaining Length),表示后面可变报头和负载加在一起的总字节数。很多初学者会忽略剩余长度的编码规则,实际上它不是单纯一个字节,而是一个变长编码,最大四个字节,每个字节的低 7 位存数据,最高位作为连续标志。换成人话就是:如果剩余长度小于 128,一个字节就够;超过 128,就得拆成多字节编码。当年我们第一次接锁端 SDK,发现锁上报大数据包时服务端解析错位,查到最后就是剩余长度多字节解析没有按规则处理。
报文类型一共有 14 种,真正天天打交道的没几个:CONNECT/CONNACK 负责建连,PUBLISH/PUBACK/PUBREC/PUBREL/PUBCOMP 负责消息收发,SUBSCRIBE/SUBACK 负责订阅,PINGREQ/PINGRESP 负责保活,DISCONNECT 负责正常断开。智能锁场景里大多数锁端只实现 CONNECT、PUBLISH、SUBSCRIBE、PINGREQ 就足够跑通业务,但如果要走 QoS 1 上报,就必须支持 PUBACK 甚至 PUBREC/PUBREL/PUBCOMP 的完整 QoS 2 流程,否则 Broker 和锁端的状态机就对不上。
2.2 CONNECT 报文里容易埋坑的字段
锁端发起连接时,CONNECT 报文是最先出去的,这里有一堆参数直接影响百万连接下的资源占用。第一是 ClientID,热点词里不少人搜“mqtt服务器搭建”之后就卡在这里。ClientID 必须全局唯一,而且不能太长,因为在锁定 Topic 上报时,服务端经常要把 ClientID 映射为设备标识。有些团队直接把设备序列号填进 ClientID,序列号 32 位,一千万台设备就能让 Broker 的内存里多出不少字符串占用。我的习惯是用内部自增 ID 做 ClientID,同时把设备标识放到 PUBLISH 报文的 Topic 或者负载里,两边都省。如果你用设备原始标识做 ClientID,又碰上同一台设备重复建连,新连接会把旧连接挤掉,日志里全是“ClientID collision”,定位成本很高。
第二是 KeepAlive。锁端不能像手机 App 那样依赖系统推送通道保持长连接,必须自己发 PINGREQ 维持连接,服务器如果在 1.5 倍 KeepAlive 时间内没收到任何报文,就会主动断开。锁端默认 KeepAlive 设 60 秒的话,一次 PINGREQ 报文不算 TCP 头大约是 2 字节固定头加 2 字节可变头,每个锁每天这类心跳包就是 1440 次,百万锁一天就是 14.4 亿条控制报文。这个量级对 Broker 是真实压力,所以我建议锁端把心跳间隔放到 120 秒到 300 秒之间,前提是锁端有一颗稳定的网络模块,不会因为长时间不通信被运营商掐断。
最后是 Clean Session 标志。在 MQTT 3.1.1 里面,这个字段控制会话是否需要被 Broker 持久化。智能锁业务强烈依赖离线消息和遗嘱消息,所以我一般建议会话相关能力走 MQTT 5.0 的 Session Expiry Interval,或者至少在 3.1.1 下把 Clean Session 设为 0,让 Broker 保留订阅关系和离线消息。不过要注意,开启持久会话对应的是 Broker 内存成本,你必须控制每个会话离线消息的条数和大小,不然锁几天不上线、积压几千条消息,一上来全部推给锁端,锁端直接内存溢出重启,这就是线上事故了。后面在“常见问题”里我会单独展开这个坑。
2.3 PUBLISH 报文逐字节现场解剖
PUBLISH 是业务数据真正的载体。固定头第一字节里,QoS 位直接决定这把锁上传的可靠性级别。锁上报门锁状态、电量这类普通事件,用 QoS 0 就可以,丢了不会出大事,上 QoS 1 反而增加网络流量和重试复杂度。但用户开锁指令、临时密码下发这类关键指令,必须 QoS 1 起步,否则你没办法向用户交代“为什么密码没收到”。QoS 2 在智能锁场景我用得很少,主要是因为实现完整 QoS 2 握手需要四段报文交互,锁端资源消耗大,而且大部分业务对“恰好一次”的诉求并没有那么强,用 QoS 1 + 服务端幂等去重就能满足。
PUBLISH 可变报头里最值得注意的字段是 Topic Name。Topic Name 本身是 UTF-8 编码的字符串,长度占 2 字节。你每多写一个层级、每多用一点长单词,都在烧锁端的流量和 Broker 的匹配 CPU。一会儿 Topic 字典里我会给出实操建议,这里只说一句:Topic 字符串尽量控制在 40 字节以内,能缩到 30 字节内更好,不要塞人类可读的完整产品名、项目名、环境名前缀。
真到协议字段层面,还有一个大多数人不会去看的字段——Payload Format Indicator。MQTT 5.0 的 PUBLISH 可变报头里带了属性(Properties),其中这个指示符告诉服务端负载是 UTF-8 文本还是二进制。智能锁行业经常遇到锁端上报的数据是二进制结构体,和云端定义完全对不上,两边各解析各的,定位问题要靠两边开发一起拉群。我强烈建议在设计规范里明确:负载一律用 UTF-8 JSON,或者反过来说,如果锁端 MCU 资源实在紧张只能用二进制,那属性位必须标清楚,不能让消费端靠猜。这点后面也会再讲。
2.4 SUBSCRIBE 报文怎么影响平台开销
SUBSCRIBE 报文是反向控制通道的关键,锁端用订阅关系表达“我需要哪些指令”。语法上,SUBSCRIBE 的负载是 Topic Filter 列表,每个 Filter 后面跟一个 requested QoS 字节。锁端数量上了百万之后,最怕的是每个锁都订阅一个独有 Topic,例如lock/{sn}/cmd,那 Broker 在路由查询时就要维护巨大数量的 Topic 树节点,每次 PUBLISH 推送指令时都要做精确叶子查找,性能和内存都会随着设备数线性增长,这是最差的一种结构。
更合理的方式是让所有锁订阅同一个广播级 Topic(比如cloud/lock/cmd),然后服务端在负载里带目标设备号,锁端自己解析后判断是否处理。这样从 Topic 数量上讲,百万设备只对应一个订阅 Filter,Broker 的内存是可控的。但代价是每条指令会广播给所有锁,白白消耗每个锁的流量和电量。所以折中方案是做成两级组合:在线数量大的锁走“组播 Topic”,比如按固件版本、批次、省份分组,每组一个订阅 Filter;真正给单把锁的点对点指令,通过服务端校验 ClientID 后单独推到一个通用 Topic 里,负载带设备定位信息。这个设计既能控制 Topic 数量,又不会让无关设备收太多垃圾消息。
这里还有个订阅偏移问题:锁端因为网络闪断重连后,如果 Clean Session 设为 1,之前订阅的关系就全部丢失,锁端需要重新走一遍 SUBSCRIBE。如果锁端 SDK 没把订阅动作做在重连流程里,就会出现“连接是好的,但服务端下发指令锁收不到”的诡异故障。排查思路其实很简单,看 Broker 的订阅列表里还有没有这台设备,没有就是重连后没补订阅。
3. Topic 字典设计:百万级智能锁的“目录学”
3.1 分层的思路和层级数量控制
Topic 字典不是随便起名字,而是一套有约束的命名规范。按 MQTT 规范,Topic 用/做层级分隔符,支持+单层通配符和#多层通配符。设计原则第一条:层级数量控制在三层到四层,顶层是域,第二层是设备类型,第三层是能力分类,第四层可选是动作。例如:
iot/lock/status/online iot/lock/cmd/update这样拆的好处是通配符订阅可以保持足够精细的粒度。业务侧想订阅所有锁的在线状态,就直接写iot/lock/status/+,想订阅某一类锁的全部数据,就写iot/lock/#。层级太深会导致每个 Topic 字符串变长,流量和内存成本上去;层级太浅又会导致通配符订阅覆盖范围过大,每次消息都要发给一大批无关消费者。四层是一个比较平衡的数字,能表达清楚业务含义,又不会给通路添堵。
另外,Topic 首层不要用环境名(dev/prod)或者团队名(xxxteam)做前缀。环境隔离应该让不同环境用不同的 Broker 或单独的接入网关,而不是靠改写 Topic 前缀。一旦把环境写进 Topic,后期做灰度发布和数据迁移会有无数坑,你要么全量改 Topic 让所有端升级,要么服务端做两套兼容逻辑,白白增加复杂度。
3.2 用白话说一遍 Topic 字典的成员
设备侧和平台侧涉及的核心 Topic 其实就是一个完整的输入输出矩阵。我只说我们实际沉淀下来的版本,你可以直接抄:
- 锁端 -> 平台:
iot/{productKey}/lock/{lockId}/status状态上报,iot/{productKey}/lock/{lockId}/event事件上报,iot/{productKey}/lock/{lockId}/log操作日志,iot/{productKey}/lock/{lockId}/online在线状态,iot/{productKey}/lock/{lockId}/ota/progressOTA 升级进度。 - 平台 -> 锁端:
iot/{productKey}/cloud/{lockId}/cmd点对点指令,iot/{productKey}/cloud/broadcast/{group}组播指令,iot/{productKey}/cloud/{lockId}/config配置下发,iot/{productKey}/cloud/{lockId}/ota/request升级任务触发。
注意,锁端订阅的一侧要尽量少,最好一个#就能覆盖平台下发的全部指令类型。但前面说过纯#会带来无关消息,所以我们实际建议锁端订阅iot/{productKey}/cloud/{lockId}/#,点对点指令都挂在锁私有 Topic 下。广播和组播另起一层,锁端如果不需要收群发消息,就不要订阅那一段。换句话讲:锁端要带着设备号过滤能力,能精准只关心自己的消息,平台侧的广播能力只当作紧急逃生通道使用。
3.3 动态设备标识还是业务标识,Topic 里放什么
这是 Topic 字典设计里最核心的决策。lockId应该放内部稳定的设备唯一标识,而不是用户手机号、房间号或者用户自定义名称。因为用户可能换绑手机号,房间号可能在小区重新分区后改变,一旦变了一次,设备上订阅的旧 Topic 链就断了,你得给全量锁重发订阅关系。用内部设备序列号做lockId,从出厂到报废不变,换绑业务逻辑全部下沉到服务端业务表里去,Topic 通道不受影响。
同时,不要把用户 ID 放 Topic 层。用户 ID 属于业务实体,和物理设备不是一一对应的,一个用户可以绑定多个锁,一个锁也可以授权多个用户。你把用户 ID 放进 Topic,等于让物理通道层依赖业务关系,后面做多用户共享锁、权限变更、离线路由,全都要改 Topic。正确的是,Topic 只表达“这是哪台设备的能力”,用户和锁的绑定关系交给后端去映射。这条原则我们还专门写进了联调规范,避免新来的后端同学想当然把 userId 拼进 Topic。
3.4 通配符误用的代价
通配符是 MQTT 的能力,也是隐患。系统里要让风控服务订阅所有锁的上报事件,有人图省事直接写iot/#,这一下把 OTA 进度、状态上报、操作日志全部拉下来,消费者要处理大量自己不关心的数据。长期下来,Broker 的 Topic 匹配负载和消费端的资源都被白白吃掉。更细一层,+只能匹配一层,#能匹配剩余所有层,两种通配符在 Broker 内部的匹配算法复杂度和缓存友好度完全不同,有些 Broker 对#开头的过滤器做了特殊索引,性能好一些,有些则退化成线性扫描。所以在设计订阅之初,就要定清每个消费者组的权限范围和 Topic 模式,宁可多写几个精确 Filter,组合成订阅集合,也不要一个#走天下。
4. 报文长度、QoS 和 DUP 标志位:别让流量白烧
4.1 控制报文大小的三个方向
锁端流量是稀缺资源,尤其是 NB-IoT 和 2G 模块,套餐按月算,流量超了直接断网,锁就变砖。控制报文大小的方向有三个:第一,负载编码要精简,用短字段名 JSON,或者是长度前缀的紧凑编码,字段能缩就缩;第二,Topic 字符串长度必须控制,前面已经说了,全链路加在一起省几十字节很可观;第三,报文频率要服务端配合,连续的状态上报可以合并为一条批量消息,事件上报没变化不重复发。
我实测过一把锁如果每秒上一条 200 字节的报文,一天就是 17.28MB 流量,月度 518MB 绝对超出 NB-IoT 套餐。但如果把报文压到 80 字节、上报频率改成事件触发 + 分钟级周期心跳,月度流量能降到 1MB 以内,差距就是这么夸张。所以报文结构设计不是一个“优雅”问题,而是实打实的成本问题。
4.2 QoS 1 重发的反复与去重
走 QoS 1 时,PUBLISH 报文发出后如果没收到 PUBACK,发送端必须重新发送,这时 DUP 标志会被置 1。锁端弱网环境特别容易触发重发,服务端如果没做去重,就可能出现“一条开锁指令被锁端执行两次”的事故。智能门锁执行两次开锁,用户侧感受是“咋自动开了两次”,风控侧就是事件异常。所以必须有一套幂等机制,最常用的是给每次下发的指令生成唯一指令 ID,锁端在本地缓存最近 N 条已处理指令 ID,遇到重复 DUP 报文直接忽略。
这里有一个容易被忽略的细节:QoS 1 不是“发送端到 Broker 一次”,也不完全是“Broker 到接收端一次”,而是每一跳独立确认。发送端和 Broker 之间靠 PUBACK 完成确认,Broker 和接收端之间又有一套确认,中间任何一跳断了都可能造成同一份消息出现在两端流程里。正确理解这一点,排查重复执行问题时才能知道该去看哪一跳的日志。
4.3 RETAIN 标志和遗嘱消息的实际用法
RETAIN 标志是很多人的知识盲区。PUBLISH 带上 RETAIN 后,Broker 会保留这条消息在 Topic 上,新订阅者一上来就能收到最近一条保留消息。智能锁平台里,我建议把锁的在线状态和最新电量、固件版本号做成保留消息,新服务端节点上线订阅iot/lock/status/+时,能立刻拿到全量设备的最近状态,而不需要回源查库。这个技巧可以帮你省掉“订阅后主动拉全量快照”这种额外接口,架构上干净不少。
遗嘱消息(LWT)则是给锁的“最后遗言”。锁端可以在 CONNECT 报文里指定一个遗嘱 Topic 和遗嘱消息,如果锁异常掉线(网络中断、电源断),Broker 会代发这条消息。百万锁平台非常依赖遗嘱消息做在线状态感知,锁正常离线时走 DISCONNECT,Broker 就不发遗嘱;异常掉线时订阅方立刻收到离线事件。但要注意,遗嘱消息不是绝对可靠,Broker 宕机、网络分区都可能让遗嘱丢失或延迟,所以服务端不能把在线状态完全钉死在遗嘱上,最好是“遗嘱事件 + 周期性心跳 + 业务探活”三管齐下。
5. 和热点问题对上:485 设备接入、客户端选型、Windows 环境
5.1 MQTT 如何给 485 设备发指令、读取数据
现在很多智能锁园区网关底下挂的是 RS485 门锁控制器,这些设备本身跑不了 MQTT,需要通过网关做协议转换。网关上行走 MQTT 连平台,下行走 Modbus/自定义 485 协议驱动锁控制器。平台要下发开门指令,实际上是把消息 PUBLISH 到网关订阅的 Topic,网关收到后翻译成 485 指令,再按地址码转发给指定锁控制器。
这里的链路设计关键在于:网关和设备地址的映射关系不要写死在 Topic 里。平台下发指令时,Topic 指向网关,负载里带目标锁的地址码(或者设备编号),网关维护一张地址映射表。假如你把地址码写进 Topic,比如iot/{gatewayId}/485/{addr}/cmd,一旦现场重新布线调整了 485 地址,你就得让平台改订阅 Topic,链路就断了。我见过不止一次现场工程师把 A 锁和 B 锁的地址搞混,结果平台命令全下给了 A 锁,门没开,日志上看指令又确实到了网关,排查很久才发现是地址映射表错了。
另外,485 链路本身是半双工轮询式的,网关一次只能和一个或者几个设备会话。所以 Topic 负载里带总线仲裁信息,比单纯把每个 485 设备包装成独立 MQTT 客户端更合理。平台侧要控制下发频率,避免一条命令还没执行完,下一条命令又压到网关,导致 485 总线冲突。结论是:MQTT 到 485 的链路,MQTT 侧只做“可靠投递”,真正的总线和设备时序管理要落在网关里,不要在 MQTT 协议层跟它纠缠,否则故障边界根本划不清。
5.2 MQTT 客户端选型和 Windows 环境搭建
有不少人在搜“mqtt客户端”、“windows安装mqtt安装包”,应该都是刚开始做本地联调。联调阶段我建议不要直接上重型云端,本地跑一个 Broker 就够了。Windows 上安装 MQTT Broker,最省事的是下载 EMQX 的 Windows 安装包,解压后命令行启动,默认端口 1883,Dashboard 端口 18083,一共两步就能把环境拉起来。另外也可以用 Mosquitto,Windows 下有安装版,配置简单,适合纯协议学习,但对百万连接的管理能力和监控告警都比较基础,做压力测试和全链路联调我还是推荐 EMQX,它的规则引擎和消息追踪接口对排查问题很友好。
客户端的话,锁端 SDK 和联调工具是两回事。锁端如果资源受限,可以考虑嵌入式 C 客户端,例如 Eclipse Paho Embedded C,裁剪后能塞进大部分 MCU;如果网关基于 Linux/Android,直接用 Paho 的 C 或 Java 版本。联调阶段,我习惯用 MQTTX 这类图形化客户端,订阅一个 Topic,手动发消息,观察解析结果,很快就能暴露报文格式和编码问题。还有一个贴士:联调时打开 Broker 的协议日志,把 HEX 报文打出来,跟文档逐字节对照,很多隐蔽问题一眼就能看见。
5.3 Broker 集群的 Topic 路由与扩展性
单机 Broker 撑死也就处理几万到十几万连接,百万锁平台肯定要上集群,而 Topic 路由在集群里会变成新的挑战。核心问题是:某台设备连在 Broker A,消费者订阅却在 Broker B 上,A 收到 PUBLISH 后要把消息路由给 B。Broker 集群内部的 Topic 路由表,实际上是一棵分布式的订阅树,订阅关系的同步延迟直接决定了“平台下发指令锁多久能收到”。实测下来,在跨节点路由模式下,端到端时延多数花在订阅关系同步而不是消息转发本身,所以我建议把业务上消息频次高的消费者尽量和对应设备落在同一 Broker 节点或者同一可用区,避免跨机房路由。
集群规模扩大时,最好按产品线或者按地域切分独立的 Broker 集群,而不是全部塞一个逻辑集群。智能锁本身有强地域属性,华东的设备不会频繁给华南的锁下发指令,那何必让路由消息跨地域跑一圈。百万连接不是“一台机器扛一百万”,而是“十台机器分别扛十万,且彼此不互相拖累”,这个思维转变很重要。
6. 常见问题与排查技巧实录
6.1 锁收不到平台下发指令
这类问题的排查顺序我踩过很多次,直接给一套成熟思路:先看锁端是否在线,再看订阅关系是否还在,再看 Broker 路由日志,最后看负载解析是否失败。很多情况下,锁重连后订阅丢失是最常见的原因,尤其是 Clean Session 为 1 的会话,网络抖动几秒钟,重连之后 SDK 没触发重新订阅,Topic 订阅列表就变成空的了。解决办法是让 SDK 在 CONNACK 之后无条件执行订阅动作,并且订阅结果要检查 SUBACK 的返回码,不要只管发不管成功。
6.2 收到消息乱码或解析失败
乱码的原因基本逃不开三个:字符编码不一致、二进制和 JSON 混用、字段大小端不对。锁端用 C 结构体按小端序填充二进制数据,云端 Java 服务用大端序解析,很容易错。我的建议是所有锁端上报统一用 JSON 字符串,编码固定 UTF-8,数字类型统一按字符串传递,避免大小端争论。如果锁端资源实在不够,非得用二进制,那必须在报文里带协议版本号和负载格式标识,不能让消费端猜。
6.3 Broker 内存增长失控
开启持久会话和保留消息之后,Broker 内存会随着离线设备数量和保留消息大小持续增长。需要做三件事:限制离线消息数量(比如每个会话最多 20 条,超出丢弃并记录)、限制保留消息大小(数据大报文不要开 RETAIN)、定期清理过期会话。还有一条隐藏规则:客户端反复断线重连,每次都用不同的 ClientID,Broker 会一直保留旧的会话资源,这属于会话泄漏。解决办法是严格规定 ClientID 生成规则,重连必须复用同一个 ClientID,并且服务端加会话过期时间兜底。
6.4 高并发下 Broker 出现大量 PUBACK 超时
这是压测或线上高峰期最容易暴露的问题。PUBACK 超时通常不是 Broker 不行,而是后端消费速度跟不上,Broker 把消息转发给消费端之后,消费端处理不过来,Broker 等待确认的时间被拉长,大量发送端的重发报文又涌进来,形成恶性循环。解法是给消费端加排队和限流,同时启用 QoS 0 场景就不要硬塞 QoS 1,能异步处理的业务不要卡在同步确认上。线上百万锁平台,开锁指令 QoS 1 必须保,但普通日志、电量上报走 QoS 0 就够了,分场景分级控制,带宽和 Broker 压力都会下来很多。
7. 一个上线前必须做的报文和 Topic 审查清单
我习惯在项目发布前拉着开发和运维一起过一遍清单,不用全部自动化,但每一条都得答得上来:第一,锁端所有 Topic 是否在字典里有登记,有没有人直接写死字符串到处拼;第二,Topic 层级是否超过四层,每个 Topic 字符串是否超过阈值;第三,关键指令是否都带了唯一指令 ID,DUP 重发是否安全;第四,离线消息队列是否限制条数,RETAIN 消息是否可控;第五,订阅关系重连后能不能自动恢复;第六,遗嘱消息和心跳上报的时延是否满足业务容忍度。这些问题看起来基础,但每次线上事故回查,最后都能落到清单里某一条。
如果项目到了百万级规模,字典设计的价值就藏在每一次联调、每一条日志和每一台 Broker 的负载数据里。你当然可以先跑起来再改,但 Topic 一旦铺到全量设备,改命名就是牵一发动全身,锁端那边全部要发新固件才能切过来。所以,在刚开始接入第一批设备之前,把报文结构和 Topic 字典定死,看起来是最不紧急的事,实际上是整个平台长期稳定性里最划得来的一笔投入。