c++ mqtt客户端_干货分享 | 基于RocketMQ构建MQTT集群系列(1)—从MQTT协议和MQTT集群架构说起...

568082e917151409a7bc8fa17ff02b11.png

本文从物联网和 MQTT 协议说起,介绍移动云推出的面向物联网业务场景的大云消息队列 E-MQTT 的架构设计,对 MQTT 集群化的连接管理、消息路由等普遍问题作出分析。

注:文中所有对MQTT协议的描述都是基于MQTT3.1.1。

一、从5G和物联网说起

2019年是 5G 商用元年,先是世界各大通信运营商开启了各自的 5G 试点工程,10月底,三大运营商正式上线了 5G 商用套餐。但 5G 对人类的影响绝不仅仅止步于手机通信,5G 的三大特性以及其三大类应用场景中的两个:海量连接通信业务和超可靠性、超低时延业务——都是物联网所需要的。随着 5G 商业化一步步成熟,必将会大大推动物联网的发展进程,诞生了十年有余的物联网或许也即将迎来真正的快速发展期。

说了这么多,和我们今天的主角 MQTT 有什么关系呢?

在物联网领域,例如智慧城市、无人驾驶、智能家居等,无论怎样的业务场景,都需要解决一件事情:怎样将机器、设备产生的数据安全、可靠、高效的传输到指定的地方(可能是其他设备,也可能是服务集群)。MQTT 协议解决了这个事情。

MQTT 协议已经成为物联网通信事实上的标准,是目前被广泛使用在物联网领域的应用层协议。此外,在边缘计算领域 MQTT 协议也被广泛使用,百度开源的 OpenEdge、华为开源的 KubeEdge 等边缘计算框架都有用到 MQTT。

它是一种消息队列,基于发布/订阅模式、提供应用层面消息可靠机制,它极为轻量,对客户端低要求,将传输量降到最低。

二、认识MQTT协议

2.1 HTTP协议和传统消息队列为什么不能胜任

基于 TCP/IP 的应用层协议很多,HTTP、FTP、SMTP 等,他们都是为解决某一类型的数据传输问题而设计的。另外,几乎所有的消息队列也都是基于 TCP/IP 构建的上层协议。这些应用层协议为什么不能直接用于互联网场景?为什么会有 MQTT 协议的出现和广泛使用?下面以HTTP 协议和传统消息队列的协议作比较,剖析它们之间的不同。

在此之前,我们先来看一下物联网下的应用场景是都是什么样的,有什么特点。

例如在一个工厂中,需要从数以千计、万计的设备采集数据,来了解各个设备的运行情况。根据对数据的解析,最后生成决策指令,进一步指导设备的行为。再例如在一个车联网系统中,在每辆车的关键部位安装各种传感器(MQTT 客户端),用来持续获取货车的状态数据(轮胎的胎压和温度、道路路况等),一辆车可能需要安装十几个传感器用来采集数据并上报。这些传感器设备在户外工作,资源十分受限,不可能有多大的 CPU、内存(即使技术上可能,恐怕成本也很难接受),网络带宽也受限且不稳定,比如基于蜂窝数据的 NB-iot,甚至是卫星网络。

4c92912a2800a5932e0de76af68200a2.png

(图片来源于网络[1])

从上面两个场景可以看出物联网应用的场景具有(但不限于)以下特点:

1、连接数巨大。服务端需要从数量众多的客户端(设备)中采集数据。或者需要下发指令到众多设备。

2、客户端资源受限。一般 CPU、内存比较小,带宽较窄且不稳定。

3、要求消息能主动推送至客户端,实时性较好。

下面我们先看网页浏览的标准——HTTP 协议,它的请求-响应模式非常符合人和机器打交道的场景。其实物联网早期,也有互联网巨头希望基于 HTTP 协议打造物联网通信标准,但是很快发现它的有些特性并不适合物联网场景。考虑如下:

1、HTTP 是基于请求-响应的,只能设备(客户端)主动向服务端发起拉取请求,服务端无法主动向设备端推送数据。如果要实现频繁的、通知类型的场景,就只能在设备端实现定时拉取逻辑,设备端的成本和数据的实时性都要大打折扣。

2、HTTP 谈不上轻量。HTTP 协议中数据的传输基本上是基于 JSON/XML 格式,在算力和存储受限的设备中实现数据的解析,难度很大。此外,每一个HTTP请求都带有消息头,HTTP的消息头信息很多, 包含 User-Agent、Accept、Host 等,还可以自定义。虽然对于手机、PC浏览器来说这样的数据量不算什么,但对于资源受限的设备而言却影响很大。

再来看传统的消息队列。传统消息队列所适用的场景大家都十分了解了:系统解耦、异步通信、削峰填谷。它做的事情和 MQTT一样,都是消息的路由转发,也都是基于发布/订阅模型的。

他的客户端通常是各个服务组件,部署在机房的物理服务器或者虚拟机上,网络一般是千兆网,甚至是万兆网。服务组件数量达到上百个已经规模很大了。基于这种应用场景的消息队列的架构设计:

1、是面向应用服务的,没有考虑支持物联网应用中的海量连接场景,一般不支持大连接。

2、在协议的设计方面并没有考虑窄带宽,减少网络流量。

3、传统消息队列的客户端很重,并不适合资源受限、低功耗的物联网设备。

所以,传统的消息队列不能满足物联网场景下的设备—服务端消息传递。

2.2 初识MQTT协议

MQTT(Message Queuing Telemetry Transport)协议是一个客户端-服务器架构的、基于发布/订阅机制的消息传输协议。它运行于 TCP/IP 之上,属于应用层协议。MQTT 最初由IBM 公司设计研发,于2014年正式成为 OASIS 官方标准。它轻量、简单、易于实现,客户端只需要很少的网络、CPU、内存资源。这些特性使其能够适应于很多场景,尤其是资源受限的物联网 IoT 通信。

先让我们看看 MQTT 协议都有哪些特性,以及是如何做到轻量的,为什么适用于物联网业务场景。

1、支持发布/订阅(PUB/SUB)模式这是所有消息系统的标配。PUB/SUB 模式很好的弥补了 HTTP 协议请求-响应模式的不足,能够满足需要主动推送消息的一些场景。而且由同步变为异步,也大大提高了效率。

2、消息实时推送到消费者。消息从服务端传输到客户端,有推(pull)和拉(push)两种模式。我们常见的消息队列中间件,比如 RocketMQ、Kafka、RabbitMQ、Pulsar 等都不是真正的 push 模式,而是客户端主动拉取或者是推拉结合的模式。这里不做赘述,大家可以去研究一下各自的代码。在 MQTT 协议中,消息是服务端主动推送到符合的客户端的,这种方式可以保证消息的实时性。

3、很小的传输消耗,使其适用于受限的网络环境(窄带宽、不稳定连接)MQTT 协议由固定报头(fixed header)、可变报头(variable header)、负载(payload)三部分组成。MQTT 协议将报头的大小控制在最小,例如和心跳相关的 PINGREQ、PINGRESP 报文,断开连接的DISCONNECT 报文都仅有2个字节的固定报头,没有可变报头和负载。其他报文也都是将数据传输量控制在最小。这使得 MQTT 协议很轻量。

4、 Small Footprint of Client。客户端代码所占用的存储很小。这也是 MQTT 轻量的另一种体现。代码量小,运行所需要的硬件资源就少,这使得MQTT客户端能够跑在一些低功耗的嵌入式设备里。

5、支持三个等级的QoS(服务质量)

QoS0(At most once):最多一次送达。意味着仅发布消息一次,消息不会存储,接收方也不会对消息进行ack,如果因为客户端不在线等原因导致消息没有收到,发送方也不会重发。这种情况下消息可能会丢失。

2eda4506e8a1681fc31e8a140241e8a8.png

QoS1(At least once):至少一次送达。意味着消息会被保证至少成功发布一次。发送方在接收方发来ack之前会将消息保存。这种情况一条消息可能会被发送多次。

6f4aaefbb2513876eb4a9c663151863f.png

QoS2(Exactly once):仅且一次送达。这个级别保证每条消息都被接收方成功接收一次,且只接收一次。这是最苛刻、也最低效率的传输级别,需要有2个来回的信息交互。

0614ec202e7f7f25f53a7982988cbf67.png

MQTT 协议规定的三种 QoS,从应用层面保证了消息的可靠性。即使在不稳定的网络环境中也不必担心消息的漏发、丢失。QoS2 的消息还能保证消息的重复发送问题。这个特性非常适用于网络不稳定的物联网场景。

6、有 Last Will 机制保证当连接异常断开时可以通知到各方。MQTT 协议定义了遗言机制,在连接建立时设定遗言消息。当此连接断开时,发布遗言消息用于通知其他相关各方。这十分有用,设备断开很多场景下不是主动断开的,遗言消息可以通知后端服务或其他设备进行后续处理。

7、持久化订阅、离线消息等其他特性。会在本系列后续文章详细解读。

关于 MQTT 更详细的内容请参考 MQTT 3.1.1 协议文档[2]。

三、大云消息队列E-MQTT的架构选型

在调研了常见开源 MQTT 的实现后,我们得到了下面的表格。

开源实现

语言

集群支持

消息持久化

评价

moquette

Java

不支持

仅H2

比之前的版本弱化很多

emqttd

Erlang

支持

开源版本不支持

功能比较齐全,  Erlang语言比较小众

mosquitto

C/C++

官方版本通过桥接支持集群,不能算真正的集群。hui6075提供了支持集群的版本,通过自定义消息实现节点间转发

支持持久化到磁盘。

C语言编写,集群简单。为嵌入式而生,上云较为困难。

MqttWk

Java

支持。但集群功能很弱。使用Redis的订阅/发布当做消息总线

仅支持转存到Kafka

消息重试很差。不支持通配符+

Jmqtt

Java

不支持

RocksDB本地存储

不支持集群,不支持SSL


一个服务如果要上云,不得不考虑的就是集群化,能够横向扩展,才能满足不断增加的用户量。单个节点无论配置再好也很容易达到瓶颈。所以,不支持集群或者难以改造成集群的方案就要排除掉。且考虑到团队自身的技术栈,排除了 Erlang、C 等语言的实现版本。因为以上原因,并没有一款符合的开源实现来进行二次开发。

最初的想法是在 RocketMQ 当前版本,在不更改技术架构的基础上新增对 MQTT 协议的支持。但前面提到,RocketMQ 和 MQTT 的架构不同,RocketMQ 的技术架构不支持海量连接,RocketMQ 的消息传输协议独自设计,不能直接用于 MQTT。其计算和存储层不分离的架构使得支持其他协议很不方便。所以,最终 E-MQTT 没能采用上述模式,而是将RocketMQ 当做底层存储,自己开发实现了 MQTT 协议。

d1dc7c270b57d7fcaedf687c6a0b1485.png

图3-1 E-MQTT集群结构图

上图3-1展示了 E-MQTT 集群的整体结构。左侧是 RocketMQ 集群,用来作消息的后端存储。中间是实现了 MQTT 协议的 MQTT 服务集群,由若干个无状态的 MQTT 节点组成,我们管它就做 MQTT-Bridge 节点。右侧是一个集中化的存储,使用 Redis 的哨兵模式部署,用来存放连接信息和持久化的订阅数据。上面是 MQTT 客户端,使用 SDK 和 MQTT 集群通信。下面两个分别是鉴权中心和计量中心,都是采用 SofaJRaft 来保证集群数据的一致性。系统各部分之间的交互如下所示:

1、MQTT-Bridge 节点启动时将自己注册到 namesrv,这一点和 RocketMQ 集群的 broker注册自己到 namesrv 是类似的。

2、MQTT-Bridge 节点之间根据 namesrv 发现彼此的存在,并与其他节点分别保持一个长连接。这个长连接用来做消息的转发、订阅/取消订阅报文的集群内广播用。

3、MQTT-Bridge 节点和 RocketMQ broker 节点通信。MQTT 协议规定,QoS>0 的消息需要保证消息不丢。将 QoS>0 的消息保存在 RocketMQ broker,能保证即使 MQTT-Bridge 节点宕机,也不会丢失消息。

4、MQTT-Bridge 节点和 Redis。Redis 中存储每个连接的信息,包括是否是持久化连接、是否在线、所连接的 MQTT-Bridge 节点等。还存储持久化订阅的订阅数据。

5、每个客户端和 MQTT 集群中的其中一个节点保持长连接,靠心跳来维持。

6、MQTT-Bridge 节点和 IAM 集群、Measure 集群。建立连接、订阅/取消订阅 Topic、发布消息时,需要和 IAM 集群通信进行认证鉴权(当然 MQTT-Bridge 会有缓存机制,后续文章会详细说明)。Measure 集群会定时采集 MQTT-Bridge 节点的计量数据,用于计费。

四、MQTT集群化实现中的连接管理和消息路由

MQTT 协议的所有 feature,单节点实现起来并不难,但一旦做到集群里,就不那么容易了。需要考虑数据在不同节点间的访问,数据的一致性维护等。本节就 MQTT 集群设计中绕不过去的连接管理问题和消息在不同节点间的路由转发问题作出分析。

4.1 连接管理

如图4-1所示,每个 MQTT Client 都和一个 MQTT-Bridge 保持一个 TCP 长连接,并通过心跳(Client发送 PINGREQ,Server 端回复 PINGRESP 报文)保持这个长连接。这和RocketMQ 的机制一致。每个客户端都有一个唯一的标识符 ClientId。MQTT 协议规定:连接时,Server 端(即MQTT-Bridge)如果发现已经有连接报文中的 ClientId 所对应的 Client 存在,则 Server 端必须断开已有 Client。(MQTT-3.1.4-2)

4d8e9371c69bb8a51e6d006387cfa207.png

图4-1 长连接的建立、维持

单节点中,只需要使用哈希表等维护 ClientId 和连接的对应关系,就可以实现旧链接的剔除逻辑,如图4-2所示。

1d0fca4e34e3379804353c198032cbda.png

图4-2 单节点连接剔除

集群环境下需要保证 ClientId 在集群内的唯一性。当相同 ClientId 的客户端连接时,需要在集群内判断有无相同 ClientId 的连接已经存在,如图4-3所示。

4d2e2eb35f41c8f0ee44ffd6f0dfb178.png

图4-3 集群内ClientId唯一问题

图4-4给出的是一种集中化存储的方式,将连接信息存储在 Redis 中,这样每当有连接建立时,按照如下步骤执行:

1、在本节点内存中查找是否有相同 ClientId 的连接存在,如果有直接关闭旧的连接,重新创建新的连接,并更新到 Redis 中。如果没有则执行2。

2、访问 Redis,如果没有 key 为 ClientId 的记录存在,说明整个集群中没有相同 ClientId 的连接存在,直接创建新的连接。如果 Redis 中存在相应记录,则执行3。

3、从 Redis 中取出旧的连接信息(其中包括此连接所在的节点),notify 该连接所在的节点断掉连接。

4、在本节点节点创建新的连接。

45b9361b5664767f8c071d4b28cc9b6f.png

图4-4 集群连接管理

可以看到,虽然 Redis 的使用使 MQTT 集群依赖了外部的组件,但简化了逻辑。

4.2 消息路由

MQTT-Bridge 的作用是接收生产者发来的消息并推送给对它感兴趣(即订阅相应匹配的Topic)的消费者。单节点下,可以将每个客户端及其订阅的 Topic 存在哈希表里(或者将所有客户端的订阅构建成一个订阅树),根据 MQTT Topic 的匹配规则,找到要推送的客户端。

c2fd7b9c4de9c7fdbb7f5022a0d93c0c.png

图4-5 单节点消息路由

集群中,生产消息的客户端和消费消息的客户端有可能不是连接在同一节点上。这就需要解决一个问题:要将消息按需转发到相应节点。但是为了降低不必要的性能损耗,不能将消息广播给其他所有节点。但如何确定一个节点上的客户端订阅了相匹配的 Topic?

鉴于前面集群内连接管理的解决方案,最先想到的是使用 Redis 做集中化存储,如图4-6所示。

d453f0741ac412e7f9863a8e9444304b.png

图4-6 集群消息路由

客户端订阅的 Topic 为 key,MQTT-Bridge 节点为 value。对于每个客户端的订阅/取消订阅动作,都更新相对应的键值对。对每一个消息,从 Redis 取出所有 topic Set< MQTT-Bridge > 键值对,根据匹配规则,找出对应的 MQTT-Bridge 节点,进行转发。从Redis取出这些键值对,需要经过网络传输,序列化和非序列化,然后还要根据匹配规则找到对应的 MQTT-Bridge 节点,这些都是非常耗时的操作。而且比起连接的建立,消息的转发是非常频繁的操作,这会大大降低系统的吞吐量。

考虑到 Topic 的订阅操作在连接建立后进行,订阅/取消订阅也不是频繁的操作,可以考虑将一个节点中产生的订阅数据广播到其他节点,在所有的节点中都保留一份全量的订阅数据。如图4-7所示:

1、当 Client1 和 Client2 进行 Topic 的订阅/取消订阅操作时,将订阅数据构建到订阅树。同时将订阅事件广播到其他节点。

2、各个节点根据广播来的数据加入到本节点的订阅树中。如图中 N1 和 N2 的订阅树所示。

3、当某节点有消息到来时,根据消息的发布 Topic 和当前节点的订阅树,找到连接到本节点、并订阅了该消息的客户端,进行消息推送。同时找到需要转发的节点,转发消息。

6c9a033b26c30a1abbd2de1ed23c6631.png

      图4-7 集群消息转发

此方案当集群节点的个数较多时,订阅/取消订阅事件的广播会消耗一定的资源,需要考虑对性能的影响。

五、总结

本篇文章从 5G 和物联网的现状说起,通过和HTTP协议和传统消息队列的比较,带领大家初步认识了 MQTT 协议,相信各位同学已经了解了它的一些特性,以及为什么说它更适合物联网的业务场景。随后给出了 E-MQTT 的结构图,并就集群设计中的全局 ClientId 管理、消息路由管理的解决方案作出了分析。构建一个 MQTT 集群涉及的问题还有很多,后续文章会涉及到如何将 QoS>1 消息持久化到 RocketMQ 后端存储,如何利用 RocketMQ 的queueOffset 进行消费进度的管理,如何实现离线消息的存储和推送。敬请大家阅览拍砖。

作者简介:程向往,目前就职于中移(苏州)软件技术有限公司,中间件团队核心研发,RocketMQ 社区 contributor。对消息中间件、MQTT、分布式服务领域有丰富经验。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值