什么是 QoS
使用 MQTT 协议的设备一般运行在网络受限的环境下,只依靠底层TCP 传输协议并不能完全保证消息的可靠到达。
因此,MQTT 提供了 QoS(Quality of service) 机制,其核心是设计了多种消息交互机制来提供不同的服务质量,来满足用户在各种场景下对消息可靠性的要求。
MQTT 定义了三个 QoS 等级,分别为:
- QoS 0,最多交付一次。
- QoS 1,至少交付一次。
- QoS 2,只交付一次。
其中,使用 QoS 0 可能丢失消息,使用 QoS 1 可以保证收到消息,但消息可能重复,使用 QoS 2 可以保证消息既不丢失也不重复。
QoS 等级从低到高,不仅意味着消息可靠性的提升,也意味着传输复杂程度的提升。
在一个完整的从发布者到订阅者的消息投递流程中,QoS 等级是由发布者在 PUBLISH
报文中指定,大部分情况下 Broker 向订阅者转发消息时都会维持原始的 QoS 不变。不过也有一些例外的情况,根据订阅者的订阅要求,消息的 QoS 等级可能会在转发的时候发生降级。
例如,订阅者在订阅时要求 Broker 可以向其转发的消息的最大 QoS 等级为 QoS 1,那么后续所有 QoS 2 消息都会降级至 QoS 1 转发给此订阅者,而所有 QoS 0 和 QoS 1 消息则会保持原始的 QoS 等级转发。
QoS 0 - 最多交付一次
QoS 0 是最低的 QoS 等级。
QoS 0 消息即发即弃,不需要等待确认,不需要存储和重传,对于接收方来说,不需要担心收到重复的消息。
为什么 QoS 0 消息会丢失?
使用 QoS 0 传递消息时,消息的可靠性完全依赖于底层的 TCP 协议。
而 TCP 只能保证在连接稳定不关闭的情况下消息的可靠到达,一旦出现连接关闭、重置,仍有可能丢失当前处于网络链路或操作系统底层缓冲区中的消息。这也是 QoS 0 消息最主要的丢失场景。
QoS 1 - 至少交付一次
为了保证消息到达,QoS 1 加入了应答与重传机制。
发送方只有在收到接收方的 PUBACK
报文以后,才能认为消息投递成功,在此之前,发送方需要存储该 PUBLISH 报文以便下次重传。
QoS 1 需要在 PUBLISH
报文中设置 Packet ID,而作为响应的 PUBACK
报文,则会使用与 PUBLISH
报文相同的 Packet ID,以便发送方收到后删除正确的 PUBLISH
报文缓存。
为什么 QoS 1 消息会重复?
对于发送方,没收到 PUBACK
报文分为以下两种情况:
PUBLISH
未到达接收方PUBLISH
已经到达接收方,接收方的PUBACK
报文还未到达发送方
对于CASE 1,发送方虽然重传了 PUBLISH
报文,但是对于接收方来说,实际上仍然仅收到了一次消息。
对于CASE 2,在发送方重传时,接收方已经收到过了这个 PUBLISH
报文,这就导致接收方将收到重复的消息。虽然重传时 PUBLISH 报文中的 DUP 标志会被设置为 1,用以表示这是一个重传的报文。但是接收方并不能因此假定自己曾经接收过这个消息,仍然需要将其视作一个全新的消息。
对于接收方,可能存在以下两种情况:
对于CASE 1,发送方由于没有收到 PUBACK
报文而重传了 PUBLISH
报文。此时,接收方收到的前后两个 PUBLISH
报文使用了相同的 Packet ID,并且第二个 PUBLISH 报文的 DUP 标志为 1,此时它确实是一个重复的消息。
对于CASE 2,第一个 PUBLISH
报文已经完成了投递,1024 这个 Packet ID 重新变为可用状态。发送方使用这个 Packet ID 发送了一个全新的 PUBLISH
报文,但这一次报文未能到达对端,所以发送方后续重传了这个 PUBLISH
报文。这就使得虽然接收方收到的第二个 PUBLISH 报文同样是相同的 Packet ID,并且 DUP 为 1,但确实是一个全新的消息。
由于我们无法区分这两种情况,所以只能让接收方将这些 PUBLISH 报文都当作全新的消息来处理。因此当我们使用 QoS 1 时,消息的重复在协议层面上是无法避免的。
极端情况下,Broker 从发布方收到了重复的 PUBLISH 报文,而在将这些报文转发给订阅方的过程中,再次发生重传,这将导致订阅方最终收到更多的重复消息。
以上,就是 QoS 1 保证消息到达,导致的消息重复。
QoS 2 - 只交付一次
QoS 2 解决了 QoS 0、1 消息可能丢失或者重复的问题,但相应地,它也带来了最复杂的交互流程和最高的开销。每一次的 QoS 2 消息投递,都要求发送方与接收方进行至少两次请求/响应流程。
- 发送方存储并发送 QoS 为 2 的
PUBLISH
报文启动一次 QoS 2 消息的传输,并等待接收方回复PUBREC
报文。这一部分与 QoS 1 基本一致,但响应报文从PUBACK
变成了PUBREC
。 - 当发送方收到
PUBREC
报文,即确认接收方收到了PUBLISH
报文,发送方将不再需要重传这个报文,并且也不能再重传这个报文。此时发送方删除本地存储的PUBLISH
报文,并存储并发送PUBREL
报文,通知接受方本次使用的 Packet ID 将被标记为可用。与PUBLISH
报文相同,所以也需要收到响应报文以确保PUBREL
报文到达对端。 - 当接收方收到
PUBREL
报文,可以确认本次传输流程中不会再有重传的PUBLISH
报文到达,因此回复PUBCOMP
报文表示确认当前 Packet ID 可以用于新的消息。 - 当发送方收到
PUBCOMP
报文,本次 QoS 2 消息传输正式完成。之后,发送方可以再次使用当前的 Packet ID 发送新的消息,而接收方再次收到使用这个 Packet ID 的PUBLISH
报文时,也会将它视为一个全新的消息。
为什么 QoS 2 消息不会重复?
QoS 2 与 QoS 1 相同,通过响应报文保证消息不会丢失。与 QoS 1 相比,QoS 2 新增了 PUBREL
报文和 PUBCOMP
报文的流程,以保证消息不重复。
首先回顾一下 QoS 1 消息无法避免重复的原因:使用 QoS 1 消息时,对接收方来说,回复完 PUBACK
响应报文后 Packet ID 就重新可用了,也不管响应是否确实到达了发送方。所以无法得知之后到达的,携带了相同 Packet ID 的 PUBLISH
报文是发送方没有收到响应而重传的,还是发送方收到了响应所以重新使用了这个 Packet ID 发送了一个全新的消息。
![[https://assets.emqx.com/images/81b397e0a44a6abd3adbe411d1219272.png?x-image-process=image/resize,w_1520/format,webp]]
所以,消息去重的关键就在于通信双方如何正确地同步释放 Packet ID,换句话说,不管发送方是重传消息还是发布新消息,一定是和对端达成共识了的。
QoS 2 增加的 PUBREL
流程提供了帮助通信双方协商 Packet ID 何时可以重用的能力。
QoS 2 规定,发送方只有在收到 PUBREC
报文之前可以重传 PUBLISH
报文。 一旦收到 PUBREC
报文并发出 PUBREL
报文,发送方就进入了 Packet ID 释放流程,不可以再使用当前 Packet ID 重传 PUBLISH
报文。同时,在收到对端回复的 PUBCOMP
报文确认双方都完成 Packet ID 释放之前,也不可以使用当前 Packet ID 发送新的消息。
因此对于接收方来说,以 PUBREL
报文为界限,凡是在 PUBREL 报文之前到达的 PUBLISH
报文,都必然是重复的消息;而凡是在 PUBREL 报文之后到达的 PUBLISH 报文,都必然是全新的消息。
一旦有了这个前提,我们就能够在协议层面完成 QoS 2 消息的去重。
不同 QoS 的适用场景和注意事项
QoS 0
QoS 0 的缺点是可能会丢失消息,消息丢失的频率依赖于你所处的网络环境,并且可能使你错过断开连接期间的消息,不过优点是投递的效率较高。
所以我们通常选择使用 QoS 0 传输一些高频且不那么重要的数据,比如传感器数据,周期性更新,即使遗漏几个周期的数据也可以接受。
QoS 1
QoS 1 可以保证消息到达,所以适合传输一些较为重要的数据,比如下达关键指令、更新有实时性要求的状态等。
但因为 QoS 1 还可能会导致消息重复,所以当我们选择使用 QoS 1 时,还需要能够处理消息的重复,或者能够允许消息的重复。
在我们决定使用 QoS 1 并且不对其进行去重处理之前,我们需要先了解,允许消息的重复,可能意味着什么。
如果我们不对 QoS 1 进行去重处理,我们可能会遭遇这种情况,发布方以 1、2 的顺序发布消息,但最终订阅方接收到的消息顺序可能是 1、2、1、2。 如果 1 表示开灯指令,2 表示关灯指令,我想大部分用户都不会接受自己仅仅进行了开灯然后关灯的操作,结果灯在开和关的状态来回变化。
QoS 2
QoS 2 既可以保证消息到达,也可以保证消息不会重复,但传输成本最高。
如果我们不愿意自行实现去重方案,并且能够接受 QoS 2 带来的额外开销,那么 QoS 2 将是一个合适的选择。金融、航空等行业场景下会更多地见到 QoS 2 的使用。