MQTT实战 - Topic设计12条军规:90%的消息混乱都源于命名错误(三)
01、先说两个真实的故事
故事一:某医院设备维保项目,改个名返工三周。
前年我们接手了一个医院设备维保项目,做设备接入层的重构。打开原有的Topic列表,我是懵的:
/device/001/temperature
Device/002/Temperature
sensor/room1/data
/buildingA/floor3/room305/sensor1/data
dev_003/CMD
五种风格,三种大小写,两个以 / 开头。更离谱的是,有的Topic用了驼峰,有的用了下划线,还有的直接把设备的位置信息编进了。
结果是什么?
第一周就出了三个故障: 第一个故障,设备上报到 Device/002/Temperature,但后端订阅的是 device/002/temperature,大小写不匹配,消息全丢。排查了三个小时才发现问题;第二个故障,运维用 # 订阅了全量Topic做监控,结果被海量消息淹没,Broker内存直接飙到90%;第三个故障最隐蔽——设备侧的Topic里嵌了机房位置 /buildingA/floor3/room305/sensor1/data,后来园区调整,设备从3楼搬到了5楼,Topic全量失效,需要所有设备重新烧录固件。
这三个故障的根因是同一个:Topic设计没有规范。
故事二:智能电表公司,上线4个月就在讨论“要不要迁移到Kafka”。
去年帮一家做智能电表的公司做架构Review。项目上线4个月,开发团队已经在讨论“要不要整体迁移到Kafka”。
我打开他们的EMQX管理后台,看了一眼Topic列表,当场就知道了问题在哪:
/device/123/report
device/456/data
/report/device/789
data/temp/111
temp/222
meter/333/readings
/334/energy
8台设备的Topic,8种命名风格。
再往下看订阅关系——后端服务订阅了 #(全量通配),前端也订阅了 #,告警服务还是 #。三个消费者收到完全相同的消息流,各自在业务层做字符串匹配来过滤“这是不是我关心的消息”。
Broker的发布/订阅机制形同虚设,MQTT被用成了TCP私有协议。
这不是个例。我见过的MQTT项目里,至少90%的Topic设计在第一个月就埋下了返工的坑。而Topic一旦被设备端烧进固件、被多个微服务写死在代码里,改起来就是一次“协议版本大迁移”——比换协议还痛苦。
Topic是MQTT消息的路由标识,它同时承担了四个角色:
-
路由寻址: Broker靠它决定把消息投给谁
-
权限边界: ACL靠它控制“谁能订阅哪一段”,是安全的第一道闸
-
业务建模: 设备分层、租户分层、业务分层,全都映射在它上面
-
数据管道: 规则引擎靠它做过滤、转发、落地时序库
这四件事里,任何一件做错,后面都要返工。而它们全部依赖最初那个命名决定。
下面这12条军规,是我花了三个月,把公司所有项目的Topic体系从混乱拉到规范后的完整总结,分五组:先定骨架、再守边界、然后立规矩、接着防误用、最后能落地。照着做,你的 MQTT 架构能撑到 10 万设备不混乱。
02、前置小知识(动手前先对齐三条协议事实)
1.Topic 大小写敏感:device/001/data 和 Device/001/Data 是两个完全不同的主题。
2./ 是层级分隔符;+ 代表单层级通配,# 代表多级通配,且通配符只能用于订阅、不能用于发布。
3.发布消息绝对不能携带 + 或 #,Broker 会把它当普通字符,下游永远匹配不上。
03、第一组 · 定骨架:先把树形语义想清楚
军规1:Topic是层级树,不是字符串,层级从一般到具体
Topic是用 / 分隔的层级树,本质是一棵路由表的键,每一层都回答一个路由维度。设计第一件事是给它稳定的语义骨架,从左到右、由粗到细。
踩过的坑:
接手某医院设备维保项目时,看到的Topic是 /device/001/temperature、Device/002/Temperature、sensor/room1/data、dev_003/CMD 这种拍平的一坨字符串,没有层级。订阅端只能靠 like 模糊匹配,权限也无法按前缀下发。后来做多租户时,所有设备ID撞车,整个消息体系改不动。
推荐范式:
v1/{tenant}/{product}/{deviceId}/{direction}/{type}
-
v1: 协议版本
-
tenant: 租户
-
product: 产品
-
deviceId: 设备
-
direction: 流向 up 上行 / down 下行
-
type: 类型 telemetry 遥测 / event 事件 / cmd 指令 / config 配置 / ack 应答
正确示例:
v1/t01/sensor_th/vdev_0001/up/telemetry
v1/t01/sensor_th/vdev_0001/up/event
v1/t01/sensor_th/vdev_0001/down/cmd
反例:
temp
plc_temperature_alarm_2024
dev1_data
data1
反例中这种“拍平的一坨字符串”,没有层级,订阅端只能靠模糊匹配,权限也没法按前缀下发。
原因: 一个合格的 Topic 必须能自解释。Topic层级应该回答“这条消息是什么、从哪来、到哪去”三个问题。每一层只表达一个语义,不要在一层里塞两个含义(比如 meter-tenantA)。
军规2:层级控制在 3~7 层,并把它当协议锁死
-
太浅(1~2层): 信息量不够,无法按维度订阅和鉴权
-
太深(8层往上): Broker通配符匹配开销陡增、可读性崩坏、ACL规则写不完。
把3~7层当协议锁死,写进团队lint:超出直接拒绝发布。 Topic层级控制在3~7层,推荐5~6层,超出就要重新审视设计。
踩过的坑:
有个项目Topic设计成 company/bld1518/floor3/room305/hvac/type-a/hvac719/temperature/status/realtime——10层。每次排查问题,光看Topic就要数半天。Broker处理Topic匹配时层级越深开销越大,AWS IoT Core甚至规定Topic中正斜杠最多7个。
推荐6层标准结构:
{版本}/{租户}/{产品}/{设备}/{方向}/{类型}
正确示例:
v1/tenant_a/meter/DEV0001/up/telemetry
反例:
v1/cn/shanghai/factory02/workshopA/production_line03/sensor/vdev001/up/telemetry ← 10层
原因: 太浅信息量不够,无法按维度订阅;太深匹配开销陡增、可读性崩坏、ACL规则写不完。把3~7层当协议锁死,写进团队lint。 层级过深会导致Topic难以阅读和维护,增加Broker的匹配开销,而且容易在某一层引入不必要的细节信息。
军规3:上行/下行分开,数据Topic和指令Topic物理隔离
数据和指令必须用不同前缀区分,上行和下行分道走。
踩过的坑:
有个项目设备用同一个Topic既上报数据又接收指令,结果后端和设备的消息互相干扰,消息循环推送,Broker直接被打挂。
推荐结构:
v1/{tenant}/{product}/{deviceId}/up/telemetry // 设备上报
v1/{tenant}/{product}/{deviceId}/up/event // 告警事件
v1/{tenant}/{product}/{deviceId}/up/ack // 指令应答
v1/{tenant}/{product}/{deviceId}/down/cmd // 平台下发指令
v1/{tenant}/{product}/{deviceId}/down/config // 平台下发配置
正确示例:
v1/t01/sensor_th/vdev_0001/up/telemetry
v1/t01/sensor_th/vdev_0001/down/cmd
反例:
meter/123/data // 既上报又下发,方向混杂
原因:数据 Topic 和指令 Topic 的生命周期与安全策略完全不同——数据是设备发布的,指令是设备订阅的。混在一起,ACL权限控制没法做,消息循环也没法防。方向清晰了,消费端才能精确订阅,Broker才能精确路由。
04、第二组 · 守边界:什么进Topic,什么进Payload
本组核心原则:稳定的身份标识进Topic,所有易变数据进Payload。
下面军规4是总原则,军规5、6、7是这个原则在不同数据类型上的具体应用。
军规4:身份标识进Topic,业务数据进Payload(核心原则)
设备ID既不要顶在最前、也不要塞在最后:
-
顶在最前:订阅方无法用通配符按产品聚合
-
塞在最后:语义混乱且无法区分产品类型
把会变的(设备ID)放到固定的那一层,不变的维度(环境、类型)作为锚点层,通配符和ACL才有的放矢。
身份标识(租户、产品、设备ID)必须放在Topic路径里; 业务数据(温度、湿度、状态值等)放到Payload。
踩过的坑:
很多项目为了Topic简洁,把所有设备ID都塞进JSON消息体内,Topic统一为 device/data。结果Broker的ACL鉴权是基于Topic路径的,无法解析JSON内部字段,导致无法做权限隔离。一旦业务代码漏洞,就会出现跨设备越权。
推荐做法:
设备ID放在固定层位(如第4层),不要顶在最前,也不要塞在最后。
正确示例:
v1/t01/sensor_th/vdev_0001/up/telemetry
反例:
device/data // 所有设备共用一个Topic,ID藏在json内
原因: Payload 是业务数据载体;Topic 负责路由、鉴权, Broker的ACL基于Topic路径,无法解析Payload。身份进Topic,权限才能精确到设备级。两者职责要严格分开。
军规5:时间戳、数值、批次号、位置信息绝不进Topic(易变数据应用)
Topic 只承载稳定、可枚举的路由身份与消息类型,时间戳、数值、批次号、位置等易变高基数数据一律放进 Payload。
踩过的坑:
有项目把温度值和时间戳都写进了Topic:device/v001/up/temperature/24.3、device/v001/up/report/1756888888。结果每次上报Topic名字都变,订阅完全失效,Broker路由表被打爆。还有设备把位置 floor3 编进Topic,后来设备从3楼搬到5楼,Topic全量失效,所有设备重新烧录固件。
规范:
以下四类数据绝不进Topic,一律进Payload:
-
时间戳、数值、批次号(时间序列数据)
-
位置、楼层、产线编号(易变属性)
推荐做法:
Topic只代表路由身份与消息类型,所有动态值放进JSON Payload。
正确示例:
Topic: v1/t01/sensor/vdev001/up/telemetry
Payload: {"ts":"2024-06-01T08:00","value":36.5,"location":"floor5"}
反例:
device/v001/up/temperature/24.3 ← 温度值进Topic
device/v001/up/report/1756888888 ← 时间戳进Topic
factory/bld1518/floor3/room305/hvac/telemetry ← 位置进Topic
原因: 任何会持续新增的枚举值(时间、数值、批次、位置)都属于数据,不属于Topic。数据可以放开,但是Topic要收敛、可枚举。动态值进Topic会导致Topic数量无限膨胀,订阅端无法通配,离线消息无法重放。位置等高基数动态段一旦变化,所有相关Topic全部失效,需要重新烧录固件或重新配置。若位置属于稳定资产拓扑而非易变属性,可谨慎保留;但只要会搬动、变更,就应放Payload。
军规6:不要按指标拆Topic,聚合上报
Topic 应对齐“设备/实体”这一稳定路由维度,把多个测点作为 Payload 字段聚合上报。
踩过的坑:
一个设备几十个测点就产生几十个Topic,设备上万台就是几十万Topic,Broker内存和订阅匹配压力暴涨。
规范:
普通测点统一聚合到一个遥测Topic,多个测点全部放进JSON Payload;个别关键告警事件才单独拆event主题。
推荐做法:
Topic: v1/t01/sensor/vdev001/up/telemetry
Payload: {"temperature":24.5,"humidity":48,"voltage":3.31}
反例:
Topic: device/v001/temperature
Payload: {"temperature":24.5}
Topic: device/v001/humidity
Payload: {"humidity":48}
Topic: device/v001/voltage
Payload: {"voltage":3.31}
原因: 按指标拆 Topic 会把 Topic 数量放大为“设备数 × 测点数”,导致 Broker 订阅匹配、内存和权限管理压力暴涨,会拖垮Broker。聚合上报是物联网平台的标准实践。
军规7:PII严禁进Topic(安全边界)
Topic 是路由元数据,必须在设计阶段就把 PII 排除在 Topic 之外;用户身份必须用 UUID 等不可识别标识替代,敏感数据只放 Payload,并通过权限、加密、脱敏控制泄露面。
踩过的坑:
有项目把手机号直接写进Topic:user/13800138000/device/001/data。后来做数据导出给第三方分析时,手机号直接暴露在了Topic日志里。更严重的是,Broker的监控面板、日志系统、链路追踪都会记录Topic名称,PII就这样泄露到了各个角落。
规范:
Topic中禁止出现任何PII(手机号、身份证号、邮箱、姓名等),用UUID替代。
推荐做法:
user/uuid-a1b2c3/device/001/data
正确示例:
user/uuid-a1b2c3/device/001/data
反例:
user/13800138000/device/001/data
user/zhangsan/device/001/data
原因: Topic会在Broker日志、监控面板、链路追踪、消息队列等各个环节留下痕迹,PII一旦进入Topic,泄露面极广。这条规则既是“什么进Topic”的应用,也是安全底线。
05、第三组 · 立规矩:字符集与系统保留段
军规8:只用小写字母、数字、连字符,禁大小写混用与特殊符号
Topic 每一层的字符集必须全项目统一为“小写字母 + 数字 + 连字符”,禁用大写/驼峰、下划线、空格、中文、特殊符号以及前导/尾随斜杠。
踩过的坑:
设备端发 Device/001/temperature,后端订阅 device/001/temperature,因为大小写敏感,消息全丢,排查了整整一个下午。还有项目用空格、中文,在不同SDK上解析异常。
规范:
- 每个Topic层级只使用小写字母(a-z)、数字(0-9)和连字符(-)。
- 不要用大写字母,不要用驼峰命名(如 deviceTemp),不要用下划线(如 device_temp)。
- 不要用空格、中文、特殊符号
- 禁止以斜杠开头或结尾
推荐做法:
warehouse/sensor/temp-001/telemetry
反例:
Device/001/temperatureData ← 驼峰
device/001/temperature_data ← 下划线
设备/温湿度01/上报 ← 中文
/device/001/data ← 前导斜杠
原因: 大小写敏感意味着 Device 和 device 是两个不同 Topic;空格、中文、特殊符号会在不同客户端、嵌入式 SDK、日志和监控链路里编码错乱。前导/尾随斜杠会引入空字符串层级,干扰通配符匹配和 ACL 前缀授权。下划线在部分 Broker 的 ACL 规则里与通配符转义存在冲突,部分嵌入式 SDK 处理也不一致,因此统一用连字符最安全。
军规9:系统消息留保留段,与业务物理隔离
$开头是 MQTT 服务端保留的命名空间(如$SYS),客户端业务消息不得占用;平台自身的设备注册、上下线、OTA 通知等系统消息应使用统一且不与业务冲突的前缀单独分层,与业务 Topic 物理隔离,避免和 Broker 内部 Topic 混淆,便于监控、ACL 授权和运维排障。
踩过的坑:
很多团队自己搞了一套“系统Topic”,和Broker内部Topic混在一起,运维排查时分不清哪些是系统、哪些是业务。
规范:
用统一前缀把“系统消息”和“业务消息”物理隔离。业务消息禁止以 $开头;系统消息使用固定前缀,如 sys/、meta/,不要占用$SYS。
推荐做法:
$SYS/broker/... # Broker自身监控指标(协议保留,业务别用$开头)
_meta/device/register # 设备注册、上下线等平台元数据
sys/ota/notify # 系统级:固件升级通知
meter/123/temp/report # 业务级:电表温度上报
正确示例:
sys/ota/notify
_meta/device/register
反例:
/system/device/123/online ← 和Broker内部Topic容易混淆
原因: 系统消息与业务物理隔离,监控和告警逻辑才清爽。遗嘱(LWT)、上线通知这类生命周期事件也建议单独一层。$SYS 由 Broker 保留,EMQX 等 Broker 已通过 $SYS 提供连接数、消息吞吐量等监控数据,业务系统 Topic 应与 $SYS 完全隔离;同时注意军规8的字符集约束,_meta 应统一改为 meta,避免下划线。
06、第四组 · 防误用:通配符与权限
军规10:通配符只属于订阅端,生产环境禁止订阅全局
发布端禁止通配符(字面量发布),订阅端严格限制+/#使用,生产消费者严禁宽泛订阅#以防过载与数据泄露,#仅限管理调试等受控场景并配ACL与限流。
踩过的坑:
运维同事为做全量监控,用 # 订阅 Broker 上所有消息。设备量涨到 5 万台后,Broker CPU 飙到 95%,消息延迟从毫秒级变成秒级;# 覆盖全量 Topic,连系统主题和多租户消息也可能被一并消费,造成严重数据安全风险。还有有发布端把 + 或 # 写进 Publish Topic,Broker 把它当普通字符,下游订阅永远匹配不上,且报错极隐蔽。
规范:
-
发布(PUBLISH):绝对字面量,零通配符。 发布 Topic 中不得出现 + 或 #。
-
订阅(SUBSCRIBE):才允许 + 和 #。 + 只匹配单层,应限制在必要层级;# 匹配多层,必须放在最后一层。
-
生产后端消费者禁止订阅全局 #,也禁止 v1/# 这类过宽的多层通配订阅。
-
只允许在管理工具、调试场景,以及跨集群桥接、协议转换、全局审计等受控场景使用,并必须配合 ACL、限流、独立消费者/集群。
-
ACL 中可按订阅 Filter 语义使用 +/#,但授权范围要收紧,不能变相放开全量订阅。
推荐做法:
// 合法:用 + 限定层级
v1/t01/sensor/+/up/telemetry
meter/+/temp/report
反例:
# ← 生产消费者禁止
v1/# ← 可能打挂消费服务
原因: 通配符订阅的性能弱于普通主题订阅,会消耗更多 Broker 和消费者资源。全局 # 会覆盖全量业务层级,高基数下消费者被海量消息淹没,Broker CPU 和延迟恶化,并带来多租户数据全量泄露风险。发布端带 +/# 不会按通配符路由,而是被当作普通字符,导致下游订阅永远匹配不上且排查困难。因此发布必须零通配符,订阅慎用 +,生产环境禁用全局 #。
军规11:ACL必须和Topic配套,默认拒绝、显式允许
Topic 设计须与“默认拒绝、显式允许”的 ACL 策略同步落地,严格限定设备仅能发布自身 up 前缀、订阅自身 down 前缀,以防被攻破设备窃取数据或伪装下发指令引发生产事故。
踩过的坑:
我们有个项目,Topic设计得很规范,但ACL策略没跟上。结果一台被攻破的设备用 factory/+/+/+/telemetry 订阅到了整个工厂所有设备的数据。更危险的是——如果设备能发布 down 开头的主题,恶意设备就能伪装成平台,向其他设备下发控制指令。
规范:
ACL要做到默认拒绝、显式允许,权限粒度精确到Topic层级。核心原则是上行下行最小权限:设备只允许发布 up 前缀、只允许订阅 down 前缀,拒绝设备发布任何 down 开头的 Topic。
推荐ACL策略:

正确示例:
{allow, {user, "device-001"}, publish, ["v1/t01/sensor_th/device-001/up/#"]}.
{allow, {user, "device-001"}, subscribe, ["v1/t01/sensor_th/device-001/down/#"]}.
{deny, all}.
反例:
无ACL,任意订阅
设备可发布 down/#,可伪装平台下发指令
设备可订阅 factory/+/+/+/telemetry,越权读取全厂数据
原因: ACL 是 Topic 安全的最后一道防线。没有最小权限,恶意设备能伪装成平台向其他设备下发指令,酿成安全事故;宽泛通配订阅会让一台失陷设备读取整个租户/工厂的数据。ACL 必须默认拒绝、显式允许、按层级精确授权,并与 Topic 命名骨架同步设计、同步评审、同步上线。
07、第五组 · 能落地:把规范变成代码
军规12:规范要代码化 + 版本前缀,别只写在文档里
Topic规范须从文档约定升级为编译期约束与运行期校验,不合规即抛异常。
踩过的坑:
规范只写在 Wiki 里,三个月后新同事照样乱写。业务迭代时直接硬改老 Topic,导致全部设备一次性瘫痪。
规范:
在用枚举 + Builder把命名规则锁死,让开发想写错都写不出来。发布前再做层数、字符集、是否含通配符的校验,不合规直接抛异常。在最外层加版本前缀 v1/v2。
推荐代码:
/// <summary>
/// MQTT Topic统一构建器,强制命名规范,避免手写字符串出错
/// </summary>
public static class MqttTopicBuilder
{
/// <summary>设备上报遥测数据</summary>
public static string Telemetry(string version, string tenant, string product, string deviceId)
{
var topic = $"{version}/{tenant}/{product}/{deviceId}/up/telemetry".ToLowerInvariant();
ValidateTopic(topic);
return topic;
}
/// <summary>平台下发控制指令</summary>
public static string Command(string version, string tenant, string product, string deviceId)
{
var topic = $"{version}/{tenant}/{product}/{deviceId}/down/cmd".ToLowerInvariant();
ValidateTopic(topic);
return topic;
}
/// <summary>Topic合法性校验</summary>
public static void ValidateTopic(string topic)
{
if(string.IsNullOrWhiteSpace(topic))
throw new ArgumentException("Topic不能为空");
if(topic.StartsWith("/") || topic.EndsWith("/"))
throw new ArgumentException("Topic禁止以斜杠开头或者结尾");
if(topic.Contains('+') || topic.Contains('#'))
throw new ArgumentException("发布Topic不允许包含通配符 + #");
if(topic.Any(char.IsUpper))
throw new ArgumentException("Topic必须全部小写");
}
}
正确示例:
v1/meter/123/temp/report
v2/meter/123/temp/report
反例:
硬编码字符串拼接
硬改老Topic的Payload格式
原因: 文档是软的,代码是硬的。规范只有变成 lint、类型和发布前校验,才能真的活下来。枚举 + Builder 把合法层级和字符集收窄到类型系统里,编译期就挡住大写、特殊符号、非法层级和通配符;运行期校验兜住动态拼接和外部输入。版本前缀支持灰度和平滑迁移,避免全部设备一次性瘫痪,也让新旧协议可以并行共存、分批升级固件。
08、附一:可直接套用的Topic命名模板
标准物联网平台(6层):
v1/{tenant}/{product}/{deviceId}/up|down/{type}
多租户SaaS平台(7层):
{租户}/{环境}/{站点}/{设备类型}/{设备ID}/{数据流}/{指标名}
工业物联网(ISA-95,7层):
{企业}/{工厂}/{车间}/{产线}/{设备类型}/{设备ID}/{数据流}
智能家居(简化版,5层):
{家庭ID}/{房间}/{设备类型}/{设备ID}/status|control|telemetry
09、附二:开箱即用的完整Topic集合
// ===== 设备上行(设备 → Broker → 后端)=====
v1/{tenant}/{product}/{deviceId}/up/telemetry // 遥测(多测点聚合)
v1/{tenant}/{product}/{deviceId}/up/event // 告警/故障/事件
v1/{tenant}/{product}/{deviceId}/up/status // 在线状态上报
v1/{tenant}/{product}/{deviceId}/up/ack // 指令执行应答
// ===== 平台下行(后端 → Broker → 设备)=====
v1/{tenant}/{product}/{deviceId}/down/cmd // 控制指令:开关、重启
v1/{tenant}/{product}/{deviceId}/down/config // 参数配置下发
10、附三:上线前12项自检清单

11、附四:12条军规速查表

协议是MQTT的骨架,Topic是它的血管。报文格式再标准,血管乱接,整个系统一样会血栓。
Topic不是随便写的字符串,是接入层架构的一部分——前期设计多花一天,后期少熬无数个排障的夜晚。把这10条写进团队规范,第一天上线就守住,比事后花四个月做消息治理划算太多。
12、最后
我整理了一份《Topic命名规范模板》,包含:
-
12条Topic设计军规(分5组:定骨架 → 守边界 → 立规矩 → 防误用 → 能落地,每条含反例与正确写法)
-
4套可直接套用的命名模板(标准物联网平台 / 多租户SaaS / 工业ISA-95 / 智能家居)
-
通配符使用边界 + ACL最小权限策略表 + 12项上线前检查清单
-
8个常见反例汇总 + 开箱即用的完整Topic集合
-
我踩过的真实坑(大小写不匹配丢消息、通配符订阅打挂Broker、位置信息嵌入Topic导致搬迁失效)
私信「Topic命名规范模板」直接领取,无套路。
做物联网 MQTT 的朋友可以一起交流。后面我会在这个号持续更新这个系列,也会穿插一些软考架构师备考、技术管理转型、嵌入式学习的记录。
我是【IT规划师】,8 年 .NET 物联网后端,正在从纯后端往软硬融合架构师的方向走。这条路不容易,但值得。
注: 测试方法代码以及示例源码都已经上传至代码库,有兴趣的可以看看。https://gitee.com/hugogoos/Planner

浙公网安备 33010602011771号