做物联网这几年,MQTT算是我打交道最多的协议。不管你是搞嵌入式、写后端,还是做上位机,只要你的项目里出现过设备上报、指令下发,大概率都绕不开它。而说到MQTT,有一个特别容易被人忽略、却又特别重要的东西,就是系统主题。我记得第一次在Mosquitto的日志里看到一长串以$SYS开头的主题时,还以为是哪个设备发上来的业务数据,查了半天才发现,这是broker自己在“汇报工作”。
今天这篇就专门聊MQTT的系统主题,也就是以$SYS开头的这类特殊主题。它和普通业务主题有本质区别,但很多人不知道它的存在,或者知道了也只是听说过,并不清楚它能干什么、怎么用。我会把原理、常见用法、实操步骤和踩坑经验一次性讲清楚。无论你是刚接触MQTT的新手,还是已经在生产环境里跑着几百台设备的老手,这篇文章应该都能给你一些参考,尤其是那些正准备搭建MQTT服务器、或者正在做设备监控方案的兄弟,建议仔细看看。
1. MQTT系统主题到底是什么:$前缀背后的身份差异
1.1 系统主题的“特殊身份”
先抛一个很多人实测踩过的现象:你用一个客户端订阅#通配符,本意是“把所有消息都收下来”,结果发现$SYS开头的主题一条都收不到。为什么?因为MQTT协议规范里明确规定了,以$字符开头的主题名称,不会被通配符#和+匹配。
这条规则的核心原因就是:$前缀给系统主题开辟了一块“保护区”。broker希望自己维护的这种内部状态主题,必须由订阅方显式指定才能拿到,而不是谁随便用一个#就能把所有东西一股脑订阅走。你要是真想订阅系统主题,必须显式地订阅$SYS/#这个过滤器,或者写具体的完整主题路径,比如$SYS/broker/uptime。
这里的$SYS就是一个“系统命名空间”。它本身不是一个主题,而是一个主题层级的前缀。broker会把运行指标、客户端连接数、消息统计这类信息,实时或者定时地发布到$SYS/下面的各个具体主题上。任何普通客户端只要权限允许,都可以去订阅这些主题,从而获取broker的运行状态。换句话说,$SYS是MQTT服务器自己对外暴露状态的一扇窗户。
1.2 系统主题与普通主题的本质区别
普通业务主题,比如devices/001/temperature,是设备或应用自己决定的,内容是业务数据,发布频率和格式完全由你的系统设计决定。而系统主题正好相反:主题路径由broker实现决定,内容由broker自动生成,发布节奏也由broker控制。你没法往$SYS里随便塞数据,因为那是broker的地盘。
另一个重要区别是保留消息(retained message)的使用。系统主题里的很多指标,比如$SYS/broker/version、$SYS/broker/uptime,通常会被设置成保留消息。这样新订阅的客户端一上来就能立刻收到当前值,而不是像普通消息那样要等下一个发布周期。这个设计对监控场景特别友好,后面讲实操的时候我还会再提。
还有一点要注意:$SYS并不是协议强制要求每个broker都实现的主题,它更多是事实标准。市面上主流的MQTT broker,比如Mosquitto、EMQX,基本都会提供一套类似的$SYS主题树,但具体路径和字段会有差异。所以你在网上查资料时,看到不同broker的$SYS例子对不上号,别慌,这是正常的。
1.3 系统主题都包含哪些信息
以最常见的Mosquitto为例,它的$SYS主题树大概包含这么几类信息:
| 主题 | 含义 |
|---|---|
$SYS/broker/version | broker版本号 |
$SYS/broker/uptime | broker启动后运行了多少秒 |
$SYS/broker/timestamp | broker当前系统时间戳 |
$SYS/broker/clients/connected | 当前已连接的客户端数量 |
$SYS/broker/clients/disconnected | 当前处于离线状态但保留了会话的客户端数量 |
$SYS/broker/clients/total | 曾连接过的客户端总数(含已离线) |
$SYS/broker/clients/maximum | 历史最大同时连接数 |
$SYS/broker/messages/received | broker累计收到的消息数 |
$SYS/broker/messages/sent | broker累计发送的消息数 |
$SYS/broker/messages/dropped | 被丢弃的消息数 |
$SYS/broker/messages/retained/count | 当前保留消息的数量 |
$SYS/broker/subscriptions/count | 当前有效的订阅关系数量 |
$SYS/broker/bytes/received | 累计接收字节数 |
$SYS/broker/bytes/sent | 累计发送字节数 |
$SYS/broker/load/messages/received/1min | 最近1分钟平均每秒收到的消息数 |
这些主题的取值基本都是字符串形式的数字或文本,比如$SYS/broker/clients/connected的值可能是42,$SYS/broker/uptime的值可能是12345 seconds。你直接用普通的MQTT客户端订阅就能读出来,不需要额外认证,前提是broker没有针对$SYS做ACL限制。
2. 为什么监控MQTT服务器一定要盯系统主题
2.1 broker运行状态:看它是不是还活着
做设备接入久了你会发现,很多“设备不上报”的问题,根子不在设备端,而在broker端。broker一旦假死、卡顿或者悄悄重启了,所有设备都会跟着掉线。这时候如果你能第一时间看到$SYS/broker/uptime的值,就能立刻判断broker是不是刚重启过——uptime一下子变成一个很小的数字,基本就可以断定broker重启了。
同样的道理,$SYS/broker/version可以用来确认你线上跑的到底是哪个版本。跨版本升级之后,如果这个主题的值没有变成新版本,说明你的升级根本没生效,或者启动时加载的配置文件不对。这类问题用业务日志排查反而慢,看系统主题一眼就明白了。
$SYS/broker/load/messages/received/1min这类指标,能反映broker当前的消息吞吐压力。如果这个值长期接近broker的极限,再加上$SYS/broker/messages/dropped持续增长,说明你的架构很可能需要横向扩容,或者业务侧消息量已经超过单节点能力了。有了这些数据,你在跟团队讨论扩容方案时就不是拍脑袋,而是拿数字说话。
2.2 连接与消息统计:判断设备在线率和链路质量
设备在线率是物联网项目里最核心的运维指标之一。$SYS/broker/clients/connected能直接告诉你当前有多少个客户端连接着broker。如果你们采购了1000台设备,配置的期望连接数是800,但这个主题的值长期只有300,那就说明有很大一部分设备没连上来,问题大概率在网络侧或者设备固件侧。
$SYS/broker/clients/disconnected这里要特别说明一下,它统计的并不是普通设备,而是“持久会话(persistent session)”的客户端。这类客户端即使离线,broker也会为它保留会话状态。所以这个值高不一定代表设备有问题,也可能是有大量客户端设置了cleanSession=false但迟迟不重连。这种情况如果积累太多,会占住broker的会话资源。
再配合$SYS/broker/messages/received和sent的差值,还能看出broker有没有在“丢消息”。正常情况下,收到的消息和发送的消息总量应该基本相当,如果received远远大于sent,就要警惕是不是有大量订阅关系不匹配、或者有消息因为权限问题被丢弃了。这类问题光靠业务日志很难发现,但系统主题会把趋势直接摆在你面前。
2.3 $share共享订阅:另一个容易被忽视的系统前缀
聊系统主题,很多人只记得$SYS,却忘了还有一个以$开头的特殊前缀:$share。它和$SYS性质不太一样,它不是broker发布状态,而是给订阅方用的“共享订阅”语法。
比如说,你的后台有3个消费者实例同时订阅devices/+/data,如果直接用普通订阅,一条设备消息会被3个实例同时收到,这在“每个设备消息只需要一个消费者处理”的场景下就是重复消费。解决办法就是改成共享订阅:$share/consumer_group/devices/+/data。三个实例都订阅这个过滤器,broker会把消息在它们之间做负载均衡,每条消息只推给其中一个实例。
为什么我要在讲系统主题时提它?因为很多人设计Topic时,看到$开头的主题就当成系统主题,容易搞混。其实$share是订阅侧的语法,不是真正的主题名,它只影响broker的派发逻辑。你在做MQTT topic设计规范时,最好先把$SYS、$share这类前缀和普通业务主题在文档里明确区分开,不然团队协作时很容易踩坑。
3. 实操:从搭建到采集,完整跑通系统主题
3.1 先搭一个带$SYS的MQTT服务器
实操部分,我用Mosquitto来做示例,因为它轻量、配置简单,在Linux上一条命令就能装好,也非常适合用来验证系统主题的各个细节。当然,用Docker就更省事了。下面这条命令就能起一个最基本的broker实例:
docker run -d --name mqtt-broker -p 1883:1883 eclipse-mosquitto:2如果是裸机安装,Ubuntu/Debian系可以直接:
sudo apt update sudo apt install -y mosquitto mosquitto-clients sudo systemctl status mosquitto默认情况下,Mosquitto会每隔10秒钟发布一次$SYS主题的数据。这个间隔可以通过配置文件里的sys_interval参数控制,单位是秒。如果设成0,就表示完全关闭$SYS发布功能。配置文件一般位于/etc/mosquitto/mosquitto.conf,你需要在里面加上类似这样的配置:
listener 1883 allow_anonymous true sys_interval 10sys_interval不要设得太小,否则broker每次发布系统主题都要额外消耗资源和带宽;也不要设得太大,不然监控数据就会不够实时。我自己一般习惯设成10到30秒。
3.2 用命令行工具订阅系统主题验证
装好客户端工具后,直接在终端订阅$SYS/#,就能把broker当前发布的所有系统主题都打出来:
mosquitto_sub -h 127.0.0.1 -p 1883 -t '$SYS/#' -v注意这里的-v参数,它会在输出里带上主题名,否则默认只打印消息内容。运行后等几秒钟,你就能看到类似这样的输出:
$SYS/broker/version mosquitto version 2.0.18 $SYS/broker/uptime 86 seconds $SYS/broker/clients/connected 1 $SYS/broker/clients/total 1 $SYS/broker/messages/received 2 $SYS/broker/messages/sent 2如果你想单独验证某个指标,可以直接订阅完整主题名:
mosquitto_sub -h 127.0.0.1 -t '$SYS/broker/uptime' -v这时候再用另一个终端发布几条普通消息,比如:
mosquitto_pub -h 127.0.0.1 -t 'test/topic' -m 'hello mqtt'回到订阅$SYS/broker/messages/received的那个终端,你就会发现收到的消息数增长上去了。这一步能非常直观地让你理解系统主题和业务消息之间的关系。
3.3 用paho-mqtt做定时采集与入库
命令行只能看个直观效果,真要放到生产环境当监控用,还是得写程序去采集。这里我用Python的paho-mqtt库写一个最精简的采集示例,把几个关键系统指标订阅回来,然后打进日志或者时序数据库。
import time import paho.mqtt.client as mqtt TOPICS = [ "$SYS/broker/version", "$SYS/broker/uptime", "$SYS/broker/clients/connected", "$SYS/broker/clients/total", "$SYS/broker/messages/received", "$SYS/broker/messages/sent", "$SYS/broker/messages/dropped", "$SYS/broker/subscriptions/count", ] results = {} def on_connect(client, userdata, flags, reason_code, properties=None): if reason_code == 0: for t in TOPICS: client.subscribe(t) print("connected and subscribed") else: print("connect failed, reason_code =", reason_code) def on_message(client, userdata, msg): key = msg.topic.replace("$SYS/broker/", "") results[key] = msg.payload.decode() print(msg.topic, "->", results[key]) client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2) client.on_connect = on_connect client.on_message = on_message client.connect("127.0.0.1", 1883, 60) client.loop_forever()需要注意,paho-mqtt不同版本对回调函数的签名要求不完全一样,新版建议显式传入CallbackAPIVersion.VERSION2,回调里多了reason_code字段。你跑上面的代码,如果用的是2.x版本,应该能正常输出。
实际项目中,我不会直接print,而是把解析后的结果写入InfluxDB、Prometheus或者直接用Python内置的sqlite3落本地库。核心思路都是一样的:订阅$SYS相关主题,解析数值型字段,按时间戳存储,再配合定时任务做聚合告警。
3.4 把系统主题变成监控面板的数据源
采集到数据后,最常用的做法是接入Grafana这类可视化工具,把clients/connected、messages/received、bytes/sent这些指标画成曲线。如果你用的采集程序直接写了InfluxDB,给Grafana配上数据源,几分钟就能出一个实时监控面板。
我自己的习惯是保留三张核心图:第一张是连接数变化曲线,重点看有没有设备批量掉线的毛刺;第二张是消息收发速率,重点看有没有流量突增导致的堆积;第三张是uptime,用于快速识别broker重启事件。这三张图基本覆盖了日常运维里90%的broker健康检查需求。
另外,系统主题也能用来做“存活探测”。如果你的监控程序本身就是个MQTT客户端,那它可以定时订阅$SYS/broker/uptime,一旦超时收不到消息,就说明broker可能挂了或者网络不通。这比单纯ping IP要靠谱,因为它是从MQTT协议层面验证的,能排除“端口通但broker进程卡死”的假活情况。
4. 踩坑实录:系统主题使用的常见问题与排查技巧
4.1 通配符#为什么订阅不到$SYS
这个问题我几乎每次带新人都会遇到。你明明订阅了#,想着把所有消息都收到,结果$SYS一个都没进来。这不是broker坏了,而是MQTT协议规范里明确要求的:以$开头的主题名,不能用#或+这种从第一层开始就用通配符的过滤器去匹配。你只能通过显式包含$SYS字样的过滤器去订阅,比如$SYS/#。
排查方法也很简单,先确认你的订阅过滤器是不是写了$SYS/#,再看broker有没有开启sys_interval。如果两者都正常,那就看是不是broker本身的$SYS主题名前缀和你预期的不一样。比如EMQX集群环境里,系统主题前缀可能是$SYS/brokers/${node}/...,不是简简单单的$SYS/broker/...,这个要尤其注意。
4.2 sys_interval设太大或直接禁用了
生产环境里,有时候你会发现明明代码是对的,但怎么都收不到系统主题数据。这时候就要去broker配置文件里查sys_interval这个参数。很多团队为了追求性能,会把sys_interval设成0,变相关闭了$SYS发布。这是完全合法的配置,但它会直接让你的监控方案失效。
我自己遇到过一种情况是,线上的broker配置了多个listener,但sys_interval只在其中一个listener上生效,导致另一个端口上的客户端订阅不到$SYS。所以排查的时候,不光要看sys_interval有没有设成0,还要确认你连接的listener和配置的listener是不是同一个。类似这种问题,用mosquitto_sub -t '$SYS/#' -v在目标端口上先跑一遍,是最快的验证方式。
4.3 权限控制没做,把内部信息全裸奔出去了
系统主题里包含着broker版本、连接数、消息量这些内部信息。如果你的broker允许匿名访问,那任何人都能订阅$SYS/#,把你服务端的运行情况摸得一清二楚。这在公网环境里等于把底裤亮给人家看。轻则暴露技术栈,重则给攻击者提供判断服务负载、寻找漏洞版本的线索。
所以,生产环境的broker一定要配置ACL。最简单的做法是:给监控程序单独开一个只读账号,只允许它订阅$SYS/#,业务账号一律禁止触碰系统主题。Mosquitto的ACL文件里可以这样写:
user monitor topic read $SYS/#然后在mosquitto.conf里启用这个ACL文件:
acl_file /etc/mosquitto/acl.file用了ACL之后,监控程序用monitor账号连接,业务设备用各自的业务账号连接,两边互不干扰。这是个很小的改动,但能避免很多安全上的尴尬。
4.4 业务Topic用$SYS开头,给自己埋雷
有些项目早期没规范,开发者图方便,把业务主题直接命名为$SYS/devices/001/data,想着“这样看起来有系统感”。结果一发到线上就抓瞎:要么订阅通配符收不到,要么跟broker自身的系统主题冲突,甚至有些broker会直接拒绝客户端往$SYS前缀发布消息。
我的建议很简单,业务主题设计规范里必须写死一条:任何业务主题都不得以$开头。$前缀是系统保留区,普通设备消息请用类似iot/{projectId}/{deviceType}/{deviceId}/data这样的结构。这样既不会和系统主题冲突,也能让日志和权限管理更清晰。如果你在定topic规范,可以顺便在文档里加一节“保留前缀说明”,把$SYS和$share写进去,后面团队协作会少很多沟通成本。
4.5 不同broker的系统主题差异
最后说一个很容易让人困惑的点:$SYS主题在不同broker实现里并不完全统一。Mosquitto用的是$SYS/broker/...这种扁平结构;EMQX在集群模式下会在第二层带节点名,比如$SYS/brokers/emqx@127.0.0.1/...,而且它的指标维度比Mosquitto多不少。还有一些云厂商的托管MQTT服务,可能直接不开放$SYS,或者把相关指标只通过HTTP API暴露。
所以,网上搜到的$SYS主题清单,不一定能直接套到你的broker上。最靠谱的做法是,拿到一个broker实例后,先用mosquitto_sub -t '$SYS/#' -v把当前实际存在的主题全部拉出来看一眼,以现场结果为准。这一点在做跨平台迁移时特别重要,不要想当然地以为换了个broker,监控脚本还能原封不动跑通。
5. 给初学者的几个实用建议
如果你刚开始做MQTT相关的东西,我建议你先别急着把$SYS当成监控的银弹。先把普通订阅、发布、保留消息、遗嘱消息这几个基础概念玩熟,再回来碰系统主题会顺很多。因为系统主题本质上就是一批“特殊的保留消息”,你对MQTT消息机制理解越深,就越能看懂$SYS的数据到底是怎么来的。
另外,在我的实际经验里,$SYS是排查问题的一把好手,但它不是唯一的手段。broker自身的日志、客户端日志、网络抓包,这三样东西配合着看,才是一个完整的排查链路。系统主题告诉你“发生了什么”,日志和抓包告诉你“为什么发生”,两边的信息合起来,问题定位速度能快不少。
最后再分享一个小技巧:给$SYS/broker/uptime单独配一个监控规则,只要这个主题连续两到三个采集周期没有新消息,就触发告警。这条规则能抓住很多“broker假死但TCP连接还在”的诡异问题。我后来发现,这类问题用业务层心跳反而很难察觉,因为TCP长连接没有断开,应用层也不报错,就是消息不流动。而系统主题一旦停更,问题就立刻暴露了。