MQTT Quality of Service 描述一条消息在客户端与 Broker 之间如何投递。它不是整个系统的“质量评分”,数字更高也不代表一定更适合。
正确的选择始于业务问题:如果消息丢失、重复、延迟或被处理两次,会发生什么?
QoS 作用于每一跳
发布侧与订阅侧的 QoS 独立协商。
发布者可以用 QoS 1 发送,而订阅者只请求 QoS 0。Broker 会按该订阅可用的较低等级投递。同样,QoS 2 也无法让数据库事务或物理执行器天然只执行一次,它只约束当前 MQTT 网络链路。
应分别分析完整路径:
设备 → Broker → 消费者 → 数据库或执行器
每个箭头和每个处理步骤都有不同的失败方式。
上图只展示发布侧链路。Broker 会针对每个匹配的订阅者发起独立投递,实际等级不高于发布 QoS 与订阅 QoS 中的较低者。发布者收到确认,并不能证明订阅者、数据库、API 或执行器已经完成业务处理。
QoS 0:至多一次
QoS 0 发送 PUBLISH 后不等待确认。消息可能到达一次,也可能丢失。
它适合:
- 高频数据;
- 可由下一条新值替代;
- 偶尔丢失成本很低;
- 更看重低延迟和低开销。
例如每秒一次的温度、实时光标位置或高频调试指标。
QoS 0 并不等于“完全不可靠”。健康的 TCP 连接在保持连接时本身有顺序和可靠性。QoS 0 只是不会在连接中断后由 MQTT 继续恢复未完成的投递。
QoS 1:至少一次
QoS 1 要求接收 PUBACK。发布者未收到确认时可能重新发送,因此消费者必须允许重复消息。
QoS 1 很适合作为以下场景的默认起点:
- 告警;
- 状态变化;
- 带幂等键的任务与指令;
- 不希望出现缺口的测量;
- 可以安全去重的事件。
载荷应支持幂等:
{
"eventId": "01JZ8M7V2BR5H0R9BC2T2N2K0A",
"deviceId": "pump-042",
"type": "pressure.threshold.exceeded",
"occurredAt": "2026-07-27T10:42:18Z",
"value": 9.8,
"unit": "bar"
}
消费者在执行副作用前记录 eventId。同一事件再次到达时,可以正常确认但不重复执行。
QoS 2:协议层恰好一次
QoS 2 使用四步交换:PUBLISH、PUBREC、PUBREL 和 PUBCOMP。握手结束前,两端都要保存状态。
只有在以下条件成立时才值得承担这项成本:
- 应用层很难安全处理重复;
- Broker 和相关客户端都正确支持 QoS 2;
- 已测量新增的延迟、带宽、内存和重连状态;
- 确实需要 MQTT 当前链路的“恰好一次”。
QoS 2 仍无法让非事务副作用天然只执行一次。如果消费者启动电机后、保存完成状态前崩溃,MQTT 握手无法判断物理动作是否发生。业务幂等仍不可缺少。
可复现的 MQTT QoS 0、1、2 实验
RunMQTT QoS 实验把上述协议说明变成可重复的本地检查。实验固定使用 Eclipse Mosquitto 2.0.22、MQTT.js 5.15.2、MQTT 5,并为每次运行生成唯一 Topic。匿名 Broker 只绑定到 127.0.0.1:18883,它是用完即弃的测试设施,不是生产配置。
可以下载实验说明、执行脚本、Compose 文件和 Mosquitto 配置,也可以在仓库根目录直接执行:
docker compose -f public/examples/mqtt-qos-lab/compose.yaml up -d
pnpm mqtt:qos-lab
docker compose -f public/examples/mqtt-qos-lab/compose.yaml down
实测结果
以下观察记录于 2026-08-09,Node.js 版本为 24.14.0。它们验证报文流与失败行为,不是延迟、吞吐量或 Broker 对比基准。
| 场景 | 观察结果 | 能说明什么 |
|---|---|---|
| 正常 QoS 0 发布 | 发布者发送 PUBLISH;订阅者收到 QoS 0 | 当前链路没有 MQTT 确认 |
| 正常 QoS 1 发布 | 发布者发送 PUBLISH 并收到 PUBACK;订阅者收到 QoS 1 | 发布侧完成一次至少一次交换 |
| 正常 QoS 2 发布 | PUBLISH → PUBREC → PUBREL → PUBCOMP;订阅者收到 QoS 2 | 发布侧完成 QoS 2 状态机 |
| 持久会话订阅者离线 | 离线时发布 QoS 0 和 QoS 1;恢复会话后只有 QoS 1 到达 | Broker 按当前持久会话配置排队了 QoS 1 |
| 消费者失败并重复发送事件 ID | 三次独立 QoS 1 发布、一次注入失败,最终业务副作用只执行一次 | 稳定事件 ID 可以跨重试和重复保护业务副作用 |
最后一个场景刻意把同一业务事件发送三次。因为它们是三次独立发布,所以三个 MQTT DUP 标志都是 false,但载荷包含同一个 eventId。第一次处理在应用副作用前失败,第二次成功应用,第三次被去重。这说明业务幂等必须依赖稳定业务标识,不能依赖 MQTT DUP 标志。
另一轮针对一次性 RunMQTT Provider Core 的测试见托管 MQTT Broker 实测。它除了记录相同的 QoS 报文交换,还覆盖 TLS/WSS 连接耗时、持久会话恢复、Topic Policy 允许/拒绝行为和一轮受控 QoS 1 消息样本。
实验边界与失败注入
本地结果只证明这组固定客户端与 Broker 配置的行为,不能证明所有 Broker 都具有相同的队列上限、过期策略、持久化耐久性或重连行为。决定 QoS 前,应使用生产客户端版本与 Broker 设置重复实验。
MQTT.js 可能在异步业务处理提交数据库前,就已经确认收到 MQTT 报文。因此,处理器崩溃并不保证 Broker 一定重新投递。应把持久工作放入事务、Inbox 或幂等任务边界,并测试进程在接收后、提交前被终止的情况。测试网络重试时,应在发布者收到 PUBACK 前或 QoS 2 握手完成前中断连接,重连后再检查 Packet ID 和 DUP 标志。
MQTT QoS 对比:0、1 与 2
MQTT QoS 0 与 1 的主要取舍是恢复能力和开销。QoS 0 发送后不等待 MQTT 确认;QoS 1 会持续重试直到收到确认,因此消费者必须允许重复。消息丢失的影响大于重复到达时,应选择 QoS 1。
MQTT QoS 1 与 2 并不是“可靠”和“更可靠”的简单区别。QoS 1 使用两步报文交换并依赖应用幂等;QoS 2 使用四步协议握手,避免当前 MQTT 链路上的重复投递。QoS 2 仍无法让下游副作用恰好执行一次,因此多数生产系统使用 QoS 1 加事件 ID 或操作 ID,反而更简单也更完整。
下面的表格只能作为起点,最终选择还需在真实网络、消息速率、客户端库和 Broker 配置下验证。
| 消息 | 建议起点 | 原因 |
|---|---|---|
| 每秒传感器采样 | QoS 0 | 下一条值可以替代丢失采样 |
| 最新状态同步 | QoS 1 + 保留消息 | 状态重要,重复无害 |
| 告警事件 | QoS 1 | 需要到达,可按事件 ID 去重 |
| 配置更新 | QoS 1 + 版本号 | 只应用更新的版本 |
| 远程指令 | QoS 1 + 操作 ID + 过期时间 | 避免过期与重复执行 |
| 财务或安全关键动作 | 不应仅按表决定 | 必须设计端到端事务与安全模型 |
QoS 与离线会话
根据 Broker 与会话设置,持久会话的订阅者离线时,QoS 1 或 2 消息可以排队。这很有用,但也可能在设备恢复后投递已经过时的工作。
指令消息应增加:
- 消息过期时间;
- 指令或操作 ID;
- 目标设备身份;
- 创建时间;
- Schema 版本;
- 确认主题或结果事件。
同时限制 Broker 队列。无限离线队列不是可靠性,而是没有边界的积压。
只应排队那些断线后仍有业务价值的消息。新的状态快照通常可以替代旧遥测,过期指令则应直接丢弃。需要同时测量单个客户端和整个设备群的重连积压,避免正常恢复过程压垮 Broker 或下游消费者。
测试失败,而不只是成功
有效的 QoS 测试应在关键时刻中断网络:
- 正常连接时发布;
- 确认返回前断线;
- 使用同一会话重连;
- 观察消息是否重试;
- 确认消费者正确处理重复;
- 确认过期指令不会执行。
还要测试大量客户端同时重连。单台设备很小的 QoS 状态,放大到设备群后可能非常可观。
推荐默认策略
高频、可替代的遥测从 QoS 0 开始;重要事件与指令从 QoS 1 开始。在考虑 QoS 2 前,先把消费者幂等做好。
关于 MQTT 5 的会话与过期变化,请阅读 MQTT 3.1.1 到 MQTT 5 迁移指南。使用主题设计指南明确权限与消息含义,并在生产设备启用持久会话前检查 MQTT 安全指南。MQTT 教程提供了可直接执行的发布与订阅命令。公共 MQTT Broker适合一次性的连通性检查;要控制断线与会话条件,请使用上面的隔离本地实验。
