MQTT QoS 0、1、2 深度教程:工业场景选型与 Mqttable 实测
从产线遥测、故障告警和生产工单出发,用 Mqttable 逐步观察 QoS 0、1、2 的握手、消息丢失与会话恢复,分清协议交付和业务执行。

一条温度采样没有到达看板,下一秒的新值也许就能补上。一次电机过温告警没有进入事件系统,值班人员可能错过排查线索。同一条生产工单执行两次,则可能让工序、计数或物料状态出错。这三种消息都可以经过 MQTT,但它们对丢失、重复和延迟的容忍度不同。
MQTT QoS 0、1、2 分别提供最多一次、至少一次和恰好一次的协议交付机制。选型不能只看这三个名称,还要看确认发生在哪一跳、会话状态能否保留,以及收到消息后的业务动作有没有自己的防重与回执。
本文用一条模拟产线贯穿讲解,并在隔离的 Mqttable Web Runtime 中完成发布、订阅和受控丢包实验。案例、温度、工单及 Client ID 都是教学材料,不是客户生产数据。截图展示协议行为,不代表真实设备动作、桌面安装包或生产部署通过验收。本文由 Mqttable 发布。
假设产线 line1 上的电机 motor7 有三类消息。
左右滚动表格,查看其余列。
| 消息 | 示例 Topic | 业务关注点 | 可以从哪种方案开始评估 |
|---|---|---|---|
| 周期遥测 | factory/line1/motor7/telemetry | 看板需要较新的温度;历史采样是否必须完整,要另行判断 | 可容忍少量缺口的实时看板,从 QoS 0 开始 |
| 故障告警 | factory/line1/motor7/alarms | 告警应进入事件系统,重复不能生成多张工单 | QoS 1 配合稳定的 event_id 和幂等入库 |
| 生产工单 | factory/line1/work-orders | 指令应被接收,重复或过期执行都需要防护 | 评估 QoS 1 或 2,同时实现 command_id、执行回执与有效期 |
这里没有“遥测必须用 0、控制必须用 2”的固定对应关系。用于追溯、计量或质量判定的采样,丢一条的代价可能很高;即使选择 QoS 1,也可能需要网关本地存储、补传和数据完整性核对。告警如果带有短有效期,还要防止恢复连接后把过时事件当成新告警。
工单使用 QoS 2,也不能省掉业务防重。一次 MQTT 交付结束后,发送程序再次发布相同的 command_id,属于另一条 Application Message。协议不会检查 JSON 里哪个字段是你的工单编号。
QoS 是发送者与接收者之间的一次协议交付约定。以下流程图描述一跳,不是 Publisher 经 Broker 到 Subscriber 的完整链路。定义与握手规则见 MQTT 5.0 的第 4.3 节。
Sender Receiver
PUBLISH, QoS 0, DUP=0 ->接收者不会为这条 PUBLISH 发送 PUBACK,发送者也不会通过 MQTT QoS 机制重投它。消息可能到达一次,也可能没有到达。QoS 0 的 PUBLISH 没有 Packet Identifier。
这不表示底层一定是“不可靠的 UDP”。普通 MQTT/TCP 依然使用 TCP 的有序传输与重传机制;QoS 0 省掉的是 MQTT 层的交付确认和对应重投状态。连接在关键时刻中断,或中间环节没有转发完整消息时,应用不能靠 QoS 0 判断接收者已经收到。
对只需要较新温度值的看板,旧消息恢复后集中补到,反而可能增加排队和陈旧数据。可以让 payload 带 seq 和采样时间,接收端识别缺口与数据年龄,而不是把“看板还有数值”当作采样完整。
Sender Receiver
PUBLISH, QoS 1, id=17 ->
<- PUBACK, id=17发送者在收到对应 PUBACK 前,把 PUBLISH 视为未确认。成功 PUBACK 返回后,这一跳的交付责任转移完成。接收者可能已经处理了消息,但 PUBACK 在返回途中丢失;会话恢复时,同一 PUBLISH 可能再次发送。
MQTT 5 的确认还可能携带失败 Reason Code。收到 PUBACK 或 PUBREC 时先看原因码,不能把“出现确认报文”直接写成“发布被接受”。本文正常握手使用成功确认;权限拒绝等失败场景可查 MQTT 错误码教程。
因此,QoS 1 适合“不能轻易漏掉,但可以识别重复”的事件。告警消息可以包含稳定的 event_id,事件系统以它作为唯一键。重复到达时返回既有记录或既有处理结果,不再创建第二张维修工单。
PUBACK 不是业务事务提交证明。客户端库可能自动发送它,而数据库写入、Webhook 调用或设备动作随后失败。业务可靠性还要由消费者自己的提交、重试和回执设计完成。
Sender Receiver
PUBLISH, QoS 2, id=21 ->
<- PUBREC, id=21
PUBREL, id=21 ->
<- PUBCOMP, id=21QoS 2 使用两阶段确认,接收者需要保存与 Packet Identifier 有关的状态,才能区分协议重投和新的交付。发送者收到 PUBREC 后,后续等待的是 PUBREL/PUBCOMP 的完成;不能把任何阶段丢失都写成“重新发整条业务消息”。
“恰好一次”针对这次协议交付,依赖双方遵守协议并保留必要状态。它不意味着整个工业系统的所有业务动作都只执行一次,也不检查两个 payload 是否相同。接收者把消息转交给数据库、队列或设备后,新的系统边界仍需要处理失败与重复。
在一跳的正常交付中,QoS 0 是一个 PUBLISH,QoS 1 是 PUBLISH/PUBACK 两个报文,QoS 2 是 PUBLISH/PUBREC/PUBREL/PUBCOMP 四个报文。这个计数不包含 CONNECT、订阅、心跳或 TCP 确认,不是吞吐基准。实际延迟还受到链路 RTT、在途窗口、持久化、负载及客户端实现影响。
Packet Identifier 用于关联同一协议流里的确认报文,完成后可以复用;Publisher 和 Broker 给另一跳使用的编号也不需要相同。不要把它存成全系统唯一的工单编号。
PUBLISH 的 DUP=1 表示这可能是一次协议重投,不能单凭它断定业务已经处理过。同样,DUP=0 也不能证明业务内容是新的:业务程序可以重新发布相同 JSON,作为新的 MQTT 消息发送。
Publisher Broker Subscriber
第一跳 -> 第二跳 ->Publisher 的发布 QoS 约束第一跳;Subscriber 的订阅选项影响 Broker 向它交付的第二跳。Broker 返回的 SUBACK 表示实际获准的订阅 QoS,不能只看表单里请求的数值。
对于本文使用的单一、非重叠订阅,下行 QoS 不高于发布 QoS 与获准订阅 QoS 的较小值。Broker 也可能降低授权 QoS,或者拒绝订阅。实验中发布 2、订阅获准 1 的消息以下行 1 到达;发布 1、订阅获准 2 的消息仍以下行 1 到达。订阅选项不能给第一跳没有的保障升级。
Publisher 收到 PUBACK,只能说明 Broker 对该次发布返回了确认,不能证明所有 Subscriber 已收到,更不能证明它们完成数据库事务。一次告警广播给三个系统时,三个系统是否接收、是否处理,是三个独立的结果。
你需要 Mqttable、一个自己拥有或获授权测试的 Broker,以及需要故障实验时使用的 Proxy。Mqttable 不内置 MQTT Broker。本教程用本机 Mosquitto,所有端口只监听 127.0.0.1;匿名访问仅用于一次性实验,不能照搬到生产。
没有测试 Broker 时,在新建实验目录中保存这份 Mosquitto 配置,再保留第二条命令的终端运行。如果端口已被占用,改用空闲端口,并同步修改 Proxy 上游,不能停止别人的 Broker 来腾位置。
cat > mosquitto.conf <<'EOF'
listener 18885 127.0.0.1
allow_anonymous true
persistence false
queue_qos0_messages false
max_queued_messages 1000
EOF
mosquitto -c "$PWD/mosquitto.conf" -v左右滚动表格,查看其余列。
| 对象 | 本文填写的值 |
|---|---|
| Mosquitto 上游 | 127.0.0.1:18885 |
| Proxy | 名称 QoS Lab Proxy,监听 127.0.0.1:38885,上游 127.0.0.1:18885,TCP |
| Mqttable Broker 配置 | QoS Factory Lab,mqtt,Host 127.0.0.1,Port 38885 |
| Publisher | 名称 publisher,Client ID qos-lab-publisher |
| Subscriber | 名称 subscriber,Client ID qos-lab-subscriber |
| 两个客户端 | MQTT 5.0,认证方式无,Clean Start 关闭,会话过期间隔 3600 秒 |
从 Broker 与连接参数 创建专用配置;在 Proxy 设置本机监听和上游后启动。不要用正在处理真实消息的工作区做丢包实验。

配置图说明本实验如何申请可恢复会话,是否真的恢复,要在重连 CONNACK 中检查 Session Present。
在 Subscriber 添加三个准确的 Topic,依次使用 QoS 0、1、2:
factory/line1/motor7/telemetry QoS 0
factory/line1/motor7/alarms QoS 1
factory/line1/work-orders QoS 2关闭 No Local,不创建通配符或重叠订阅,发布时关闭 Retain。先等待三个成功 SUBACK,再发布消息。具体控件与步骤见 发布与订阅教程。
Mqttable Trace 记录 MQTT 报文。QoS 2 的重复 PUBLISH 可能在 Trace 中出现多行,但协议客户端只向应用交付一次;不能把这些行直接当作业务执行计数。
本实验另用 mosquitto_sub 作为独立消费者 qos-lab-observer,经同一 Proxy 订阅三个 Topic,以 JSON Lines 记录应用接收。它与 Mqttable Subscriber 是两个独立会话,计数只证明这个消费者的应用交付,不代表两者共享 Packet Identifier,也不证明真实设备动作。
mosquitto_sub -h 127.0.0.1 -p 38885 -V mqttv5 \
-c -x 3600 -i qos-lab-observer -q 2 \
-t 'factory/line1/motor7/telemetry' \
-t 'factory/line1/motor7/alarms' \
-t 'factory/line1/work-orders' \
-F '%j' > observer.jsonl-c 启用持久会话请求,-x 3600 设置 MQTT 5 的会话有效期;-F '%j' 记录实际接收 QoS、Retain 与 payload。参数含义见 Mosquitto subscriber 手册。跨机器或使用 Remote Runtime 时,127.0.0.1 指向运行该程序或 Runtime 的机器,不是必然指向显示页面的电脑。
在连接页的发送栏选择 qos-lab-publisher,Topic 填 factory/line1/motor7/telemetry,展开载荷并选择 JSON,填写:
{ "sensor": "motor7", "seq": 1, "temperature_c": 62.4, "phase": "baseline" }选择 QoS 0,保持 Retain 关闭,再发送一次。

先确认客户端、Topic 和 QoS;载荷中的 phase 用于把正常链路与后面的故障消息分开。
收起发送栏,在 Trace 查看 Publisher 发送、Subscriber 接收的 PUBLISH。两条消息的 Topic 和 payload 应一致;QoS 为 0,没有对应 PUBACK。本文的独立消费者实际记录到一条 seq=1。

两条 PUBLISH 是两个客户端视角的报文,不是应用执行两次。此次正常交付已被观察到,不表示下一条必然成功。
Topic 改为 factory/line1/motor7/alarms,选择 QoS 1,填写告警:
{
"event_id": "A-001",
"asset": "motor7",
"alarm": "over_temperature",
"temperature_c": 91.2,
"phase": "baseline"
}
event_id 是业务提供的事件标识,不由 MQTT Packet Identifier 替代。
发送一次后,在 Trace 分别检查 Publisher 的 PUBLISH/PUBACK,以及 Subscriber 的 PUBLISH/PUBACK。每一跳内部的确认报文对应同一个 Packet Identifier;两跳之间不要求编号一致。独立消费者的正常告警计数为一条。

Publisher 的 PUBACK 与 Subscriber 的 PUBACK 是不同会话里的确认,不能把前一个当作后一个的结果。
Topic 改为 factory/line1/work-orders,选择 QoS 2,填写:
{
"command_id": "WO-001",
"operation": "load_recipe",
"recipe": "R17",
"target": "line1",
"phase": "baseline"
}
这是一条教学工单,没有连接执行配方的工业设备。
发送一次后,每一跳应观察到 PUBLISH、PUBREC、PUBREL、PUBCOMP。在列菜单勾选 ID,通过 Client ID、方向和 Packet Identifier 把同一跳关联起来,不把两组握手混成一组。独立消费者正常接收一条 WO-001。

完整握手证明这次协议流程完成,不证明配方已装载,也不证明业务事务完成。判断消息 QoS 看 PUBLISH 行;PUBREL 固定头的保留位不表示业务消息降为 QoS 1。
只在上述隔离 Proxy 注入故障。本文的 Packet loss 丢弃的是指定 MQTT 报文,不是直接模拟丢失 IP 包;不能把结果解释为 TCP 本身没有重传。
每次只启用一个丢包规则,Loss 为 100%,Toxicity 为 100%,持续模式为 Always On。规则移除并恢复连接后,先用独立的 recovery 消息确认链路正常,再做下一项实验。故障期间不要反复点击发送,否则会把业务主动重发和协议重投混在一起。
在 Proxy 添加 Packet loss,方向 Downstream,只选择 PUBLISH。规则启用前两个客户端和独立消费者均已连接。

只丢弃 Broker 向客户端发送的 PUBLISH,保留连接与心跳,避免把“尚未连接”误当作 QoS 0 丢失。
发布一次 seq=2, phase=fault,随后删除这条规则,再发布 seq=3, phase=recovery。检查独立消费者的序号及接收时间,而不是只看 Publisher 是否显示发送成功。

独立消费者收到 seq=1 和 seq=3,没有收到 seq=2。恢复后新消息可达,不表示被丢弃的旧 PUBLISH 得到了 QoS 补发。
正常发布演示和 QoS 0 丢失实验使用 MQTT 5.0。本次固定版本的 Proxy 在按类型丢弃 MQTT 5 ACK 时没有达到目标,Broker 日志仍收到了确认;不能用规则配置成功冒充故障复现。所以下面两项 ACK 丢失实验使用 MQTT 3.1.1 接收端,Publisher 保持 5.0。
先断开 Subscriber,把它的 MQTT 版本改为 3.1.1,关闭 Clean Session 后重连。不要为 3.1.1 套用 MQTT 5 的 Session Expiry 参数。停止旧的独立消费者,再以相同 Client ID 启动以下进程,不要同时运行两个同名消费者:
mosquitto_sub -h 127.0.0.1 -p 38885 -V mqttv311 \
-c -i qos-lab-observer -q 2 \
-t 'factory/line1/motor7/telemetry' \
-t 'factory/line1/motor7/alarms' \
-t 'factory/line1/work-orders' \
-F '%j' > observer-v311.jsonl这些故障结果只证明这组 3.1.1 接收会话的行为。本文没有声称成功复现 MQTT 5 ACK 丢失;更换产品版本后,应重新检查双向报文与应用计数。
删除前一个规则,添加新的 Packet loss:方向 Upstream,只选择 PUBACK。这样 Publisher 收到的 Broker PUBACK 不受这条上行规则影响,而 Subscriber 和独立消费者返回给 Broker 的 PUBACK 会被丢弃。

注入位置在第二跳的确认方向,第一跳完成不等于第二跳已经完成。
发布一次 event_id=A-002-V311, phase=fault。先确认应用消费者已经接收,再移除丢包规则,停止再启动本实验的 Proxy,以受控断开、恢复 TCP 会话。保持原 Client ID 和关闭的 Clean Session;检查 CONNACK 的 Session Present,以及 Broker 重投 PUBLISH 的 Packet Identifier 和 DUP。

3.1.1 独立消费者在断线前收到一次 A-002-V311,恢复后又收到一次,同一 Packet Identifier 为 15。这里没有第二次业务发布;重复来自未确认交付的恢复。
本段实测 3.1.1 会话以 CleanSession 为 0 恢复未确认 PUBLISH。MQTT 5.0 第 4.4 节则要求在 Clean Start 为 0 且会话存在的重连中,重发未确认的 QoS 大于 0 的 PUBLISH 和 PUBREL,并沿用原 Packet Identifier。它不是“连接正常时每隔固定时间就重发”的统一规则。MQTT 3.1.1 的文字不能不加区分地套用到 5.0。参见 MQTT 5.0 与 MQTT 3.1.1 第 4.4 节。
重置上一个规则,添加 Upstream、100% 丢弃 PUBREC。发布一次 command_id=WO-002, phase=fault。Broker 已发送 PUBLISH,但没有收到第二跳的 PUBREC,因此这次交付仍未确认。

这是 QoS 2 的第一阶段确认丢失,不是 PUBCOMP 丢失;不同阶段对应不同的恢复报文。
记录故障期间的独立消费者计数,移除规则并受控恢复原会话,然后检查原 Packet Identifier 的重复 PUBLISH,以及后续 PUBREC/PUBREL/PUBCOMP。客户端实现向应用交付消息的时机,需要通过消费者记录核对,不能仅由收到 PUBLISH 推断。

3.1.1 独立消费者在故障期间记录 0 次 WO-002,恢复后记录 1 次;重投沿用 Packet Identifier 17。这是本文消费者和这次会话的实测结果,不是所有客户端库内部交付时机的统一说明。
这组实验让 Broker 保存下行未确认状态,Broker 始终保持运行。它没有证明 Mqttable 客户端重启后会恢复所有本地在途状态,也没有证明 Broker 重启或断电后的持久化恢复。持久会话、消息状态写盘和应用事务是不同条件。Mosquitto 本实验关闭了磁盘 persistence,但内存中的会话在客户端重连期间仍可保留;配置区别见 Mosquitto 手册。
完成故障实验后,移除所有 Toxic,把 Mqttable Subscriber 改回 5.0、Clean Start 关闭、会话过期间隔 3600 秒,并重新确认订阅成功。下面两项对照使用两个 Mqttable MQTT 5 客户端;独立消费者仍是单独的订阅会话。
给 Subscriber 添加 factory/line1/qos-downgrade,请求 QoS 1,确认 SUBACK 获准 1,再以 QoS 2 发布一条 phase=publish2-subscribe1。Trace 分别核对 Publisher 的四阶段握手和 Subscriber 的 PUBLISH/PUBACK。
再把这个准确订阅改为 QoS 2,确认 SUBACK 获准 2,发布 QoS 1、phase=publish1-subscribe2。下行仍是 QoS 1。独立消费者使用另一份 QoS 2 订阅,它的结果不能替代 Mqttable Subscriber 的下行判断。

同一 Topic 的两跳可以使用不同 QoS。本文没有重叠订阅,不把这个对照扩展为所有路由策略的说明。
恢复正常链路,向工单 Topic 以 QoS 2 主动发送两次完全相同的 payload:
{
"command_id": "WO-003",
"operation": "load_recipe",
"recipe": "R17",
"target": "line1",
"phase": "business-repeat"
}
独立消费者实际收到两条 WO-003。它们是两次新的业务发布,不是一次 MQTT 交付里的 DUP 重投。
接收程序需要用 command_id 判断是否已接受或完成该工单。对于写入数据库的动作,唯一约束、处理状态与业务写入应处于正确的事务边界;如果事务外还调用设备或第三方 API,仍需处理“动作完成了,但结果记录失败”的情况。简单的内存集合会在进程重启后丢失,不能被称作持久幂等。
抓取本机实验端口的双向流量,把文件导入 Mqttable PCAP 分析。在连接、流量和报文视图中选定同一客户端的会话,再关联 QoS、方向和 Packet Identifier。本文 PCAP 配图使用仅客户端侧的片段,并在报文视图过滤 Mqttable Subscriber,原始双侧抓包另行保留。

完整抓包可以帮助核对两跳及恢复过程;采集不完整时,“没有看到 PUBACK”只能说明缺少可见证据,不足以宣布 Broker 没有发送。
Proxy 前后存在两段 TCP 连接,丢包规则可能让 Proxy 收到报文但不向另一端转发。分析时必须明确观察的是客户端侧还是 Broker 侧;不能把两段抓包中的同一消息相加,当作应用收到两次。Trace 的日常关联方式见 Trace 教程。
实时遥测先定义数据年龄和可接受缺口。给每个测点带采样时间、序号和设备标识,接收端拒绝过旧值,并把“当前值可用”与“历史记录完整”分别显示。需要完整历史时,在采集端建立有边界的存储、补传与水位核对,不依赖一次 MQTT 发送。
告警要有稳定的事件身份。设备离线后重新上报同一事件,event_id 应保持不变;真正的新事件则需要新的标识。消费者可以对同一事件更新状态,但不要因协议重复到达再创建一张工单。若规则变化或设备重启会重置计数器,事件编号设计也要覆盖这些边界。
生产工单至少区分“已接受”和“已执行”。业务回执可以包含 command_id、结果状态、设备时间和错误原因,并由发送端设置等待时限与查询途径。MQTT 5 的 Response Topic、Correlation Data 可以帮助关联请求和响应,但不会替你完成工单状态机或设备执行。发送失败时,先查询既有工单状态,再决定是否重新下发。
离线排队还需要容量与有效期。订阅者的会话是否存在、Broker 的队列是否已满、消息是否过期、断电后状态是否仍在,都会影响最终结果。不能因为开启 QoS 1 就承诺无限离线补传。Retain 适合给新订阅者提供最近保留值,不是每个离线消费者的历史事件队列。
QoS 2 是否值得使用,取决于协议重复交付的代价、双方实现、资源预算和恢复需求。对于已有可靠幂等逻辑的告警与工单,QoS 1 往往也是合理候选;对需要协议层抑制重复交付的链路,可评估 QoS 2,但仍保留业务标识和回执。本文没有进行吞吐或延迟基准,因此不提供“QoS 2 慢几倍”的数值结论。
比如发送端下发 WO-003 后,消费者先验证目标、配方版本和有效期,持久记录已接受的工单,再返回 accepted。实际业务随后执行,完成后返回 succeeded,失败则返回 failed 并附原因。这个状态顺序是业务设计示例,不是本次模拟设备的运行记录。
{ "command_id": "WO-003", "status": "accepted", "target": "line1" }accepted 只表示消费者接受了工单,不能让看板显示“配方已装载”。发送端等待最终结果超时时,把状态标为待确认,通过同一 command_id 查询;不要立刻换一个工单编号重新下发。新编号会绕过原有防重逻辑,制造两条都合法的新命令。
消费者再次收到 WO-003 时,应按持久记录返回既有状态。若记录已完成,不再执行业务动作;若执行中,不应启动并行执行;若已过期,不因为网络刚恢复就重新接受。什么时候可以对失败工单重试、是否需要新的业务版本,应由工单规则定义,而不是由 DUP 位定义。
网关进程、Broker 和业务数据库各自重启时,保存状态的地方不同。验证方案时分别测试:MQTT 会话是否存在、工单记录是否存在、业务动作是否已经发生、结果回执是否可查询。本文只实测网络会话恢复,不能替代这些生产验收项。
不能单靠 QoS 2 保证。它约束 MQTT 协议交付,不把数据库事务与设备动作纳入同一个原子操作。使用稳定业务键、唯一约束与正确的事务边界,并处理业务主动重发和事务外副作用。
没有这个含义。Publisher 的 PUBACK 来自 Broker,通常甚至还没有涉及最终设备的业务处理结果。执行成功需要设备或业务消费者给出可关联的回执,发送端还需处理回执丢失与查询。
重复业务内容不一定来自同一 PUBLISH 的协议重投。发送程序重新发布同一事件,或者 Broker 为另一跳建立新的交付,都不能只凭第一跳的 DUP 判断。结合 Client ID、两跳方向、Packet Identifier 与业务 event_id 分析。
订阅 QoS 是允许的交付上限,不能把发布 QoS 0 自动升级成 2。先确认发布端实际 QoS,再看 SUBACK 的获准等级和该订阅下行 PUBLISH 的 QoS,不只检查订阅表单。
不可以。Retain 保存的是 Topic 的最近保留消息,用于后来的订阅交付;离线会话队列面向具体消费者的待交付消息。完整历史、过期控制和队列容量需要单独设计。
QoS 2 增加协议交互和状态维护,也不会替你解决所有业务可靠性问题。先确定消息是否允许缺口、如何去重、有效期多长,以及恢复后旧消息是否仍有价值,再选择 QoS。完成本文实验后,把同一组检查应用到自己的获授权测试 Broker,而不是直接在产线注入故障。