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策略:

1

正确示例:

{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项自检清单

2

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

3

协议是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

posted @ 2026-10-17 19:59  IT规划师  阅读(55)  评论(0)    收藏  举报