做无人超市的物联网设计方案时我原本以为搞定传感器数据上报、门锁控制和扫码枪识别这几条主线就差不多了。后来一次意外让我在EMQX控制台里看到了一堆以$SYS开头的主题并且它们还在自动变化——连接数、消息量、运行时长、字节数……这些数据不是任何人主动发出来的而是MQTT服务器自己维护的“系统主题”。当时我还只是个初中生正愁方案文档里的“运维监控”章节没什么内容可写。把$SYS摸清楚之后整个设计的完整度高了一大截。这篇博文就把系统主题是什么、有哪些字段、怎么订阅、怎么跟无人超市的业务Topic分开设计以及我踩过的几个坑一次性讲透。适合正在做物联网课程设计、参加竞赛或者准备MQTT相关面试的同学参考。1. 系统主题$SYS到底是什么从一次意外发现说起1.1 控制台里多出来的那批奇怪主题我第一次接触$SYS是在用MQTTX客户端调试无人超市的收银台扫码枪时。当时为了确认扫码枪有没有把商品条码发到Broker上我建了一个订阅主题填的是supermarket/cashier/pos-1/scan。扫了几下消息正常收到了但我在MQTTX“已订阅主题”之外又顺手看了一眼Broker的Dashboard发现其实EMQX内部还有一长串主题点进去一看$SYS/brokers/emqx127.0.0.1/version$SYS/brokers/emqx127.0.0.1/uptime$SYS/brokers/emqx127.0.0.1/stats/connections.count$SYS/brokers/emqx127.0.0.1/stats/messages.received.count最初我不明白这些主题是谁创建的因为无人超市方案里没有任何一个设备向$SYS开头的主题发送过数据。后来才搞清楚这些系统主题的发布者是Broker自身也就是说消息不是“我的设备发出的”而是“MQTT服务器在汇报自己的运行状态”。这个发现让我意识到之前的方案里少了一块特别重要的拼图——服务器的可观测性。1.2 $SYS是Broker面向自己的“体检报告”MQTT协议本身是一套“发布/订阅”模型但这个模型描述的只是客户端与客户端之间的通信。Broker服务器不但负责转发消息它自身也是一个持续运行的进程也需要对外暴露自己的健康状况。MQTT标准在很早期就约定了一个保留前缀$SYS专门存放这类“系统级状态数据”。这些主题的设计初衷是给运维人员和监控系统看的相当于Broker自己写了一本体检报告谁有权限谁就能翻。翻阅方式就是普通的订阅客户端订阅$SYS/#Broker会周期性推送相应字段。比如uptime字段返回的是服务器已运行的时间stats/connections.count返回的是当前建立的连接数。把这个机制理解成一个商场广播系统就行商场Broker用特定的喇叭频道$SYS主题播报自己的运营状态顾客客户端只能用收音机听这个频道不能抢过话筒对着它喊。“系统主题用于系统状态广播不用于承载业务消息”——这是它和普通主题的本质区别。1.3 理解$SYS对“无人超市”方案的价值很多入门教程会直接跳过$SYS因为业务功能不依赖它也能跑通。但如果你交上去的是一份完整的系统设计方案$SYS简直是答辩时的加分点。比如无人超市方案里客户需要知道店里有几台自助收银机在线、几个温度传感器在上报、Broker是否过载、某个时段消息吞吐是否在合理范围。这些指标从哪来你可以自己用设备端逻辑统计但更规范的方案是直接订阅$SYS系统主题在监控模块里做一层汇总。从运维角度看$SYS还能帮我们发现设备异常掉线。比如无人超市冷柜温度传感器每10秒上报一次数据正常情况下业务主题里肯定持续有消息一旦设备电池耗尽或者信号断开Broker这边会先表现为连接数下降对应的$SYS/brokers/{节点}/clients/disconnected/count就会增长。把系统主题里的异常指标做成告警规则比单纯盯着业务主题更早发现问题。所以我后来把$SYS理解成了“物联网系统的仪表盘”。2. 上游源头与字段全解$SYS主题的命名规则和常见分类2.1 一张表看清$SYS主题的整体面貌$SYS主题不是只有一两个而是一整套层级。以EMQX为例$SYS下面通常按节点、统计项、指标名来组织。整理成表格会清晰得多主题分段示例统计范畴数值含义$SYS/brokers/${node}/version基本信息Broker版本号$SYS/brokers/${node}/uptime基本信息节点启动后运行时长$SYS/brokers/${node}/datetime基本信息节点当前系统时间$SYS/brokers/${node}/sysdescr基本信息系统描述信息$SYS/brokers/${node}/clients/connected/count连接状态当前在线客户端数量$SYS/brokers/${node}/clients/disconnected/count连接状态当前离线客户端数量$SYS/brokers/${node}/clients/total/count连接状态累计连接客户端总数$SYS/brokers/${node}/clients/max连接状态历史最大在线连接数$SYS/brokers/${node}/topics/count主题状态当前存在的主题数量$SYS/brokers/${node}/subscribers/count订阅状态当前有效订阅数量$SYS/brokers/${node}/retained/count保留消息当前保留消息数量$SYS/brokers/${node}/messages/received/count消息统计累计收到的消息数$SYS/brokers/${node}/messages/sent/count消息统计累计发出的消息数$SYS/brokers/${node}/messages/publish/received/count消息统计累计收到的PUBLISH消息$SYS/brokers/${node}/messages/publish/sent/count消息统计累计发出的PUBLISH消息$SYS/brokers/${node}/messages/dropped/count消息统计累计丢弃的消息数$SYS/brokers/${node}/bytes/received/count网络流量累计接收字节数$SYS/brokers/${node}/bytes/sent/count网络流量累计发送字节数$SYS/brokers/${node}/store/messages/count持久化存储持久化消息数量$SYS/brokers/${node}/store/messages/bytes持久化存储持久化消息占用字节$SYS/brokers/${node}/stats/database/...内嵌数据库Mnesia、RocksDB等状态指标不同版本和不同Broker实现略有差异但整体逻辑都逃不出“基本信息、连接状态、主题状态、消息统计、流量统计、存储统计”这几个大类。我在设计方案里写监控模块时直接把这张表作为“系统监控指标清单”贴了进去导师一看就明白数据来源是正规的MQTT系统主题。2.2 节点名${node}到底怎么来的表格里为什么写着${node}因为在集群环境下同一个EMQX集群可能有多个Broker节点emqx192.168.1.10、emqx192.168.1.11。每个节点都维护一套自己的$SYS指标所以路径里必须带节点名来区分。单机部署时节点名通常是emqx127.0.0.1。我实验环境里订阅到的$SYS/brokers/emqx127.0.0.1/就是这一台Broker的状态数据。如果以后做了EMQX集群想拿到全集群视角就需要分别订阅每个节点的$SYS/brokers/{节点}/路径再做聚合或者用EMQX Dashboard直接看集群级汇总。2.3 几个高频字段的取值与含义分析光看字段名还不行实际用的时候得知道这些数值代表什么、单位是什么。就拿无人超市监控来说我重点盯的几个字段是uptime返回的是“某时长”比如1 hours, 23 minutes, 45 seconds。它反映Broker是否发生过重启。如果一天内uptime被清零好几次说明服务不稳定八成是配置有问题或服务器资源不足。clients/connected/count当前在线客户端数。无人超市里扫码枪、门锁、温湿度传感器、人脸识别摄像头都算独立客户端这个数字应该约等于“上电设备总数”。如果实际在线数量远低于配置的设备数量说明有设备掉线。messages/received/count与messages/sent/count收到的消息数和发出的消息数。在MQTT里一条从设备Publish出来的消息如果被多个订阅者接收Broker可能会按订阅者数量分发多条所以sent通常大于received。bytes/received/count和bytes/sent/count网络流量累计值。在无人超市场景中摄像头识别的Payload如果很大这里会快速增长。观察这个字段能帮你评估有没有必要对图片压缩后再上传。messages/dropped/count被丢弃的消息。这个很关键当客户端离线时若没有设置持久会话和队列Broker无法投递的消息就会累积为丢弃计数。数值快速上涨说明要么QoS设置有问题要么订阅端消费能力不足。2.4 EMQX各版本之间$SYS路径的差异与兼容我在网上查资料时发现有些博客写的路径是$SYS/broker/...有些是$SYS/brokers/...起初还以为自己记错了。后来对照了文档才弄清楚EMQX 4.x及更早版本使用$SYS/brokers/${node}/...EMQX 5.x继续沿用brokers而Mosquitto这类Broker则使用$SYS/broker/...的单数形式。版本差异的正确应对方式是在你实际部署的Broker上订阅$SYS/#完整通配符看一眼以实际输出为准不要在方案文档里照抄网上的旧路径那会成为答辩时被问倒的漏洞。我自己的教训是第一次用Mosquitto做实验时照着EMQX的路径去订阅结果什么都收不到。后来订阅$SYS/#才发现它输出的前缀是$SYS/broker/...比如$SYS/broker/uptime。所以无论用哪个Broker第一步永远是“通配订阅、观察全貌”而不是靠记忆硬写路径。3. 做实验把系统主题从自己的Broker里“抓”出来3.1 实验环境Docker版EMQX MQTTX理论说再多不如亲手订阅一次。我做实验的环境很简单一台Windows电脑装了Docker DesktopEMQX 5.x镜像通过Docker运行MQTTX桌面客户端用来订阅和发布消息启动EMQX时我用的是这样的命令docker run -d --name emqx \ -p 1883:1883 \ -p 8083:8083 \ -p 8084:8084 \ -p 18083:18083 \ emqx/emqx:5.8.3端口说明1883MQTT/TCP端口普通客户端连接用8083MQTT/WebSocket端口浏览器里跑MQTT.js时用8084MQTT/WebSocketTLS端口18083EMQX DashboardWeb管理后台端口如果你本机没有Docker也可以下载EMQX的Windows安装包或直接跑Linux包。Docker的好处是可重复搭建、删了重来不污染系统。实验完以后用docker rm -f emqx就能一键销毁。3.2 三步订阅$SYS/#连接过程不复杂但每一步都有容易出错的地方。第一步打开MQTTX新建连接。客户端ID随便填比如sys-monitorBroker地址填127.0.0.1端口1883。注意MQTTX里Host那一栏只需要填IP不要画蛇添足加tcp://前缀否则连接会报错。第二步新增订阅。订阅主题填$SYS/#QoS建议用0或1都行。这里#号代表多层通配符可以匹配$SYS下所有子主题。如果只想看客户端连接数也可以只填$SYS/brokers/emqx127.0.0.1/clients/connected/count。第三步点击订阅后MQTTX会立刻开始接收消息。由于EMQX默认每隔一段时间发布一次系统主题的快照消息会不断刷新。看到消息列表里滚动出现$SYS/brokers/emqx127.0.0.1/uptime、$SYS/brokers/emqx127.0.0.1/version这些主题就说明订阅成功了。3.3 把系统主题变成无人超市的实时看板订阅到原始数据不是终点实践里更常用的是把系统主题接入监控面板。无人超市的设计方案里我规划了这样一个链路温度传感器、扫码枪、门锁等设备通过MQTT连接EMQX一台专用“监控客户端”在真实项目中可以是Node-RED、Grafana插件或自写的Python脚本订阅$SYS/brokers/emqx127.0.0.1/#监控程序把clients/connected/count转换为“当前在线设备数”把messages/received/count的差值转换为“每秒消息吞吐量”最终在屏幕上显示成看板在线设备、消息速率、Broker运行时长这里的关键技巧是$SYS里的计数器是累计值要算“速率”必须自己取差值。比如messages/received/count第一次拿到1000010秒后再拿一次变成10050那么这10秒平均每秒收到5条消息。很多新手直接拿累计值画趋势图画出来一条永远往上涨的线看不出真实负载这就是没做差分处理。3.4 实验过程里我踩过的4个坑第一个坑在防火墙。EMQX在Docker里跑起来后我本机MQTTX连不上1883端口排查了半天才发现是Windows防火墙拦了入站连接。如果是你自己电脑上的客户端连Docker服务一般改成允许就行如果是局域网另一台设备调试还需要特别注意防火墙要放行对应的入站规则。第二个坑在Docker端口映射。有些人习惯只用-p 1883:1883然后浏览器端的MQTT.js就神秘地连不上。原因往往是WebSocket端口没有映射。所以启动容器时最好把需要的端口一次映射全别边调试边补端口那会浪费很多时间。第三个坑是EMQX版本路径不一样。我在网上看到别人写的路径是$SYS/broker/...照填后发现什么消息都收不到。解决办法前面已经说了直接用$SYS/#订阅看全貌以实际输出为准。第四个坑是匿名访问没打开。EMQX 5.x默认允许匿名访问但如果你按更严格的安全基线把它关了又没有提前创建客户端认证信息MQTTX虽然能建立TCP连接但MQTT握手阶段会报Not Authorized。实验环境里可以临时开启allow_anonymous true或者干脆建一个用户名密码养成好习惯。3.5 关于公众MQTT服务器演示千万别依赖它网上有很多提供免费测试的MQTT Broker比如broker.emqx.io、test.mosquitto.org。它们确实可以快速跑通Demo但这些公共服务器部署在海外、网络延迟和稳定性都不可控做课堂演示时随时可能翻车。更关键的是公共服务器不隔离业务数据任何人都能通过通配符订阅到你发的消息。无人超市方案里涉及摄像头识别结果、门锁控制指令这些数据不该裸奔在公共Broker上。我的建议是设计文档里写清楚“本地部署私有MQTT服务器”演示时也一定用自己的Docker版EMQX。这对答辩和真实落地都是加分项。4. 系统主题和业务主题怎么共处无人超市的MQTT Topic设计4.1 区分系统主题与业务主题的“楚河汉界”如果说$SYS主题是“服务器内部状态”那么业务主题就是“无人超市业务逻辑产生的数据”。两者在设计时必须分开不然会出现两类问题把业务数据发到$SYS开头的主题会导致Broker拒绝或者系统状态被污染把系统状态混进普通业务主题会让业务端收到一堆不该收的消息增加解析复杂度还会让订阅权限的管理失控所以在Topic设计规范里首先要确定各层级的边界。$SYS是MQTT标准保留前缀业务主题一律不要用$开头。$开头的主题还有协议层面的特殊性当客户端订阅#时不会匹配$SYS下的内容只有明确订阅$SYS/#才能收到系统主题消息。这其实是协议设计上给系统主题加的一层“保护”避免普通订阅者被系统消息淹没。4.2 从零开始规划无人超市业务主题树无人超市需要采集的商品扫码、人流统计、环境温湿度、门锁控制等数据全部要落到一套清晰的主题树上。我设计的主题树大致如下supermarket/ ├── area/ # 区域划分 │ ├── entrance/ # 入口区域进店人流 │ ├── shelf/ # 货架区域补货、重量传感器 │ └── cashier/ # 收银区域扫码、支付 ├── device/ # 设备类型 │ ├── counter/ # 客流统计摄像头 │ ├── scale/ # 货架重量传感器 │ ├── scanner/ # 扫码枪 │ ├── lock/ # 门锁 │ └── temp_humi/ # 温湿度传感器实际订阅和发布时我采用的完整主题格式是supermarket/entrance/counter/entrance-01/event进店客流事件supermarket/shelf/scale/shelf-02/weight货架重量上报supermarket/cashier/scanner/pos-01/scan扫码枪条码结果supermarket/cashier/lock/exit-01/command门锁下行控制指令supermarket/env/temp_humi/cold-area-01/report冷柜区域温湿度上报这套设计遵循了“场地/类型/设备实例/操作”的层级规律。第一层是项目或门店级的命名空间第二层是区域或功能模块第三层是设备实例最后一层是动作。这样做的好处有几个权限控制精细。可以只给扫码枪设备supermarket/cashier//...的发布权限而门锁控制设备只有supermarket/cashier/lock/exit-01/command的订阅权限。排查问题快。只看supermarket/shelf/scale/...就能定位到货架区域无需逐条分析消息内容。通配符使用方便。想监控整个收银区域订阅supermarket/cashier/#想监控所有温湿度计订阅supermarket/env/temp_humi/#。4.3 方案文档里的Topic设计规范表在自己的设计文档里我画了这样一张表来呈现Topic设计。这也是MQTT Topic设计规范里很推荐的呈现方式业务模块Topic方向数据类型QoS保留进店客流supermarket/entrance/counter/entrance-01/event上报JSON0否货架重量supermarket/shelf/scale/shelf-02/weight上报JSON1是扫码结果supermarket/cashier/scanner/pos-01/scan上报JSON1否门锁控制supermarket/cashier/lock/exit-01/command下发JSON1否温湿度上报supermarket/env/temp_humi/cold-area-01/report上报JSON0是QoS不是越高越好。普通事件类数据用QoS 0或1就够了门锁控制这类必须送达的指令用QoS 1QoS 2虽然最严格但开销大、性能差实际项目里很少对全部消息启用。保留消息Retain只用在“设备最新状态”需要被新订阅者立刻感知的场景比如货架重量和温湿度进店客流事件是瞬时的新订阅者不需要一上来就收到几条历史事件。4.4 为什么Topic要这么设计几个“面试级”理由很多人会问Topic直接用supermarket/cashier/pos1不行吗行但不够好。看一个具体场景。假设无人超市里有4个收银台扫码枪需要把所有条码发到后端库存系统。如果Topic是supermarket/cashier/pos1/scan库存系统要订阅4个主题如果收银台数量增加订阅列表也得跟着改。但如果设计成supermarket/cashier//scan一个通配符订阅就覆盖了所有收银台后续加设备不用改代码。这就是为什么MQTT社区普遍推荐“设备实例放第三层”因为第二层通配可以横向扩展一组设备。另一个理由是逆向前缀的好处。MQTT主题匹配是从最左侧开始逐层匹配的。如果把设备ID放在第一层比如pos1/cashier/scan那想统计整个超市的消息就不得不订阅#会把无关主题全拉进来。而把项目名或场地放在第一层订阅supermarket/#就能圈定这个门店的所有消息逻辑非常干净。还有一个细节是命名风格。主题层级最好只用小写英文字母、数字和短横线不要用中文、空格和下划线。MQTT规范其实没有强制限制但某些Broker和客户端对特殊字符的处理不一致一旦出现空格或中文排查起来极其痛苦。短横线和点号在语义上也不同社区规范一般不建议在Topic里用点号作为层级分隔因为点号在MQTT主题中并不是分隔符容易和某些系统自动生成的路径混淆。5. 权限与安全的边界哪些能做、哪些不能做、常见误解清单5.1 客户端对$SYS主题的操作权只有“订阅”系统主题的第一个“不能做”是发布。在MQTT协议里$开头的主题具有系统保留性质正常的客户端被要求“不应该向这类主题发布消息”。EMQX这类Broker在收到对$SYS/#的Publish请求时通常会直接拒绝或者丢弃。不只是发布即使是订阅也未必所有客户端都能订。默认情况下EMQX允许任意认证客户端订阅$SYS/#但在生产环境里更稳妥的做法是设置专门的订阅权限只允许监控后台的账号订阅系统主题普通业务设备一律禁止。这样能防止无关设备获取Broker运行细节也从权限层面隔离了系统数据和业务数据。5.2 三个关于$SYS主题的常见误解误解一“系统主题也是普通主题我可以往里发消息。”不对。$SYS前缀是协议层面的保留区面向的是“Broker到客户端”的单向广播。客户端向系统主题发消息轻则被忽略重则被Broker以协议错误断开连接。无人超市方案里如果设备代码误把$SYS当成普通主题用甚至会直接影响设备在线稳定性。误解二“系统主题没多大用监控直接查数据库就行。”实际操作中数据库里存的是业务数据解决的是“店里发生了什么事”系统主题解决的是“服务器本身健康状况如何”。两者互补。有经验的评审老师很可能会问一句“你怎么知道你的MQTT服务正常”如果回答“看业务消息还有没有上报”其实不够严谨——业务消息断了可能是设备问题未必是Broker问题。用$SYS/brokers/.../uptime和clients/connected/count来证明Broker本身可用更有说服力。误解三“$SYS主题消息不占资源可以随意全量订阅。”并不准确。$SYS主题的消息也需要Broker周期性采集和发布订阅者也占用连接和带宽资源。如果几百台设备全部订阅$SYS/#系统反而会被自己的状态消息淹没。正确姿势是只让专业监控端订阅普通设备不要碰系统主题。5.3 在EMQX里配置$SYS订阅权限的小实验如果想让“只有监控账号能订阅$SYS/#”可以按下面思路实验。EMQX 5.x支持基于Dashboard或配置文件的授权规则。你先创建两个用户monitor_user和sensor_user然后添加两条授权规则允许monitor_user订阅$SYS/#拒绝sensor_user订阅$SYS/#用MQTTX分别用这两个账号连接监控账号能收到系统主题消息传感器账号订阅时会被拒。这个过程能帮你直观理解“系统主题归系统”这个设计思路。最终在方案文档里我也确实把这条权限策略写进了安全设计小节$SYS只对运维监控端开放业务端与系统端完全隔离。5.4 给你的设计方案在安全方面加分的几条经验回看我这套无人超市方案最后被老师反复肯定的几条安全设计都有它们的价值所有设备连接使用独立的用户名密码不使用匿名访问区分设备角色权限门锁只能订阅指令主题扫码枪只能发布识别结果到指定主题防止横向越权$SYS/#只授权给监控服务设备端完全不可见业务Topic统一使用supermarket/作为第一层命名空间方便设置有效访问范围所有可疑的大流量主题单独隔离避免一个区域的异常数据拖垮整个Broker这些内容看起来不是功能但恰好是物联网系统从“能跑”走向“规范”的分水岭。尤其是系统主题的权限收口它是整张权限表里最容易忽视却又最能体现工程素养的一环。我在实际做这个项目时最后并没有让无人超市的业务端直接去订阅$SYS主题而是单独写了一个很小的监控模块用专用账号订阅$SYS/brokers/emqx127.0.0.1/#再把数据汇总成“在线设备数、消息吞吐量、Broker运行时长”这几个指标展示在方案演示页的角落。这样既利用了系统主题做运维可视化又没让系统状态混进业务消息流。如果你现在正在设计自己的MQTT方案我的建议是先把$SYS/#订阅通配符跑通一次看着那些自动刷新的连接数和消息计数你会对整个Broker的运行机制有更直观的理解。理解到位的程度直接决定了你方案文档里那段“运维设计”是凑字数还是真的有分量。一个小技巧在订阅$SYS主题时把QoS设为0就够了这些系统指标本身允许丢失不值得占用更高的QoS开销。
