关于运维:RocketMQ-端云一体化设计与实践

3次阅读

共计 6561 个字符,预计需要花费 17 分钟才能阅读完成。

作者 | 悟幻

一体化背景

不止于散发

咱们都晓得以 RocketMQ 为代表的音讯(队列)起源于不同应用服务之间的异步解耦通信,与以 Dubbo 为代表的 RPC 类服务通信一起承载了分布式系统(服务)之间的通信场景,所以服务间的音讯散发是音讯的根底诉求。然而咱们看到,在音讯(队列)这个畛域,近些年咱们业界有个很重要的趋势,就是基于音讯这份数据能够扩大到流批计算、事件驱动等不同场景,如 RocketMQ-streams,Kafka-Streams、Rabbit-Streams 等等。

不止于服务端

传统的音讯队列 MQ 次要利用于服务(端)之间的音讯通信,比方电商畛域的交易音讯、领取音讯、物流音讯等等。然而在音讯这个大类下,还有一个十分重要且常见的音讯畛域,即终端音讯。音讯的实质就是发送和承受,终端和服务端并没有实质上的大区别。

一体化价值

如果能够有一个对立的音讯零碎(产品)来提供多场景计算(如 stream、event)、多场景(IoT、APP)接入,其实是十分有价值的,因为音讯也是一种重要数据,数据如果只存在一个零碎内,能够最大地升高存储老本,同时能够无效地防止数据因在不同零碎间同步带来的一致性难题。

终端音讯剖析

本文将次要形容的是终端音讯和服务端音讯一体化设计与实际问题,所以首先咱们对面向终端的这一大类音讯做一下根本剖析。

场景介绍

近些年,咱们看到随着智能家居、工业互联而衰亡的面向 IoT 设施类的音讯正在呈爆炸式增长,而曾经倒退十余年的挪动互联网的手机 APP 端音讯依然是数量级宏大。面向终端设备的音讯数量级比传统服务端的音讯要大很多量级,并依然在快速增长。

个性剖析

只管无论是终端音讯还是服务端音讯,其本质都是音讯的发送和承受,然而终端场景还是有和服务端不太一样的特点,上面简要剖析一下:

  • 轻量

服务端个别都是应用很重的客户端 SDK 封装了很多性能和个性,然而终端因为运行环境受限且庞杂必须应用轻量简洁的客户端 SDK。

  • 标准协议

服务端正是因为有了重量级客户端 SDK,其封装了包含协定通信在内的全副性能,甚至能够弱化协定的存在,使用者毋庸感知,而终端场景因为要反对各类庞杂的设施和场景接入,必须要有个标准协议定义。

  • P2P

服务端音讯如果一台服务器解决失败能够由另外一台服务器解决胜利即可,而终端音讯必须明确发给具体终端,若该终端解决失败则必须始终重试发送该终端直到胜利,这个和服务端很不一样。

  • 播送比

服务端音讯比方交易系统发送了一条订单音讯,可能有如营销、库存、物流等几个零碎感兴趣,而终端场景比方群聊、直播可能成千上万的终端设备或用户须要收到。

  • 海量接入

终端场景接入的是终端设备,而服务端接入的就是服务器,前者在量级上必定远大于后者。

架构与模型

音讯根底剖析

实现一体化前咱们先从实践上剖析一下问题和可行性。咱们晓得,无论是终端音讯还是服务端音讯,其实就是一种通信形式,从通信的层面看要解决的根底问题简略总结就是:协定、匹配、触达。

  • 协定

协定就是定义了一个沟通语言频道,通信单方可能听懂内容语义。在终端场景,目前业界宽泛应用的是 MQTT 协定,起源于物联网 IoT 场景,OASIS 联盟定义的规范的开放式协定。

MQTT 协定定义了是一个 Pub/Sub 的通信模型,这个与 RocketMQ 相似的,不过其在订阅形式上比拟灵便,能够反对多级 Topic 订阅(如“/t/t1/t2”),能够反对通配符订阅(如“/t/t1/+”)

  • 匹配

匹配就是发送一条音讯后要找到所有的接受者,这个匹配查找过程是不可或缺的。

在 RocketMQ 外面实际上有这个相似的匹配过程,其通过将某个 Queue 通过 rebalance 形式调配到生产组内某台机器上,音讯通过 Queue 就间接对应上了生产机器,再通过订阅过滤(Tag 或 SQL)进行精准匹配消费者。之所以通过 Queue 就能够匹配生产机器,是因为服务端场景音讯并不需要明确指定某台生产机器,一条音讯能够放到任意 Queue 外面,并且任意一台生产机器对应这个 Queue 都能够,音讯不须要明确匹配生产机器。

而在终端场景下,一条音讯必须明确指定某个接受者(设施),必须精确找到所有接受者,而且终端设备个别只会连到某个后端服务节点即单连贯,和音讯产生的节点不是同一个,必须有个较简单的匹配查找指标的过程,还有如 MQTT 通配符这种更灵便的匹配个性。

  • 触达

触达即通过匹配查找后找到所有的接受者指标,须要将音讯以某种牢靠形式发给接受者。常见的触发形式有两种:Push、Pull。Push,即服务端被动推送音讯给终端设备,主动权在服务端侧,终端设备通过 ACK 来反馈音讯是否胜利收到或解决,服务端须要依据终端是否返回 ACK 来决定是否重投。Pull,即终端设备被动来服务端获取其所有音讯,主动权在终端设备侧,个别通过位点 Offset 来顺次获取音讯,RocketMQ 就是这种音讯获取形式。

比照两种形式,咱们能够看到 Pull 形式须要终端设备被动治理音讯获取逻辑,这个逻辑其实有肯定的复杂性(能够参考 RocketMQ 的客户端治理逻辑),而终端设备运行环境和条件都很庞杂,不太适应较简单的 Pull 逻辑实现,比拟适宜被动的 Push 形式。

另外,终端音讯有一个很重要的区别是可靠性保障的 ACK 必须是具体到一个终端设备的,而服务端音讯的可靠性在于只有有一台消费者机器胜利解决即可,不太关怀是哪台消费者机器,音讯的可靠性 ACK 标识能够集中在生产组维度,而终端音讯的可靠性 ACK 标识须要具体离散到终端设备维度。简略地说,一个是客户端设施维度的 Retry 队列,一个是生产组维度的 Retry 队列。

模型与组件

基于后面的音讯根底一般性剖析,咱们来设计音讯模型,次要是要解决好匹配查找和牢靠触达两个外围问题。

  • 队列模型

音讯可能可靠性触达的前提是要牢靠存储,音讯存储的目标是为了让接受者能获取到音讯,接受者个别有两种音讯检索维度:
1)依据订阅的主题 Topic 去查找音讯;
2)依据订阅者 ID 去查找音讯。这个就是业界常说的放大模型:读放大、写放大。

读放大:即音讯按 Topic 进行存储,接受者依据订阅的 Topic 列表去相应的 Topic 队列读取音讯。

写放大:即音讯别离写到所有订阅的接受者队列中,每个接受者读取本人的客户端队列。

能够看到读放大场景下音讯只写一份,写到 Topic 维度的队列,但接受者读取时须要依照订阅的 Topic 列表屡次读取,而写放大场景下音讯要写多份,写到所有接受者的客户端队列外面,显然存储老本较大,但接受者读取简略,只需读取本人客户端一个队列即可。

咱们采纳的读放大为主,写放大为辅的策略,因为存储的老本和效率对用户的体感最显著。写多份不仅加大了存储老本,同时也对性能和数据精确一致性提出了挑战。然而有一个中央咱们应用了写放大模式,就是通配符匹配,因为接受者订阅的是通配符和音讯的 Topic 不是一样的内容,接受者读音讯时没法反推出音讯的 Topic,因而须要在音讯发送时依据通配符的订阅多写一个通配符队列,这样接受者间接能够依据其订阅的通配符队列读取音讯。

上图形容的承受咱们的队列存储模型,音讯能够来自各个接入场景(如服务端的 MQ/AMQP,客户端的 MQTT),但只会写一份存到 commitlog 外面,而后分收回多个需要场景的队列索引(ConsumerQueue),如服务端场景(MQ/AMQP)能够依照一级 Topic 队列进行传统的服务端生产,客户端 MQTT 场景能够依照 MQTT 多级 Topic 以及通配符订阅进行生产音讯。

这样的一个队列模型就能够同时反对服务端和终端场景的接入和音讯收发,达到一体化的指标。

  • 推拉模型

介绍了底层的队列存储模型后,咱们再详细描述一下匹配查找和牢靠触达是怎么做的。

上图展现的是一个推拉模型,图中的 P 节点是一个协定网关或 broker 插件,终端设备通过 MQTT 协定连到这个网关节点。音讯能够来自多种场景(MQ/AMQP/MQTT)发送过去,存到 Topic 队列后会有一个 notify 逻辑模块来实时感知这个新音讯达到,而后会生成音讯事件(就是音讯的 Topic 名称),将该事件推送至网关节点,网关节点依据其连上的终端设备订阅状况进行外部匹配,找到哪些终端设备能匹配上,而后会触发 pull 申请去存储层读取音讯再推送终端设备。

一个重要问题,就是 notify 模块怎么晓得一条音讯在哪些网关节点下面的终端设备感兴趣,这个其实就是要害的匹配查找问题。个别有两种形式:1)简略的播送事件;2)集中存储在线订阅关系(如图中的 lookup 模块),而后进行匹配查找再精准推送。事件播送机制看起来有扩展性问题,然而其实性能并不差,因为咱们推送的数据很小就是 Topic 名称,而且雷同 Topic 的音讯事件能够合并成一个事件,咱们线上就是默认采纳的这个形式。集中存储在线订阅关系,这个也是常见的一种做法,如保留到 Rds、Redis 等,但要保证数据的实时一致性也有难度,而且要进行匹配查找对整个音讯的实时链路 RT 开销也会有肯定的影响。

牢靠触达及实时性这块,上图的推拉过程中首先是通过事件告诉机制来实时告知网关节点,而后网关节点通过 Pull 机制来换取音讯,而后 Push 给终端设备。Pull+Offset 机制能够保障音讯的可靠性,这个是 RocketMQ 的传统模型,终端节点被动承受网关节点的 Push,解决了终端设备轻量问题,实时性方面因为新音讯事件告诉机制而失去保障。

上图中还有一个 Cache 模块用于做音讯队列 cache,因为在大播送比场景下如果为每个终端设备都去发动队列 Pull 申请则对 broker 读压力较大,既然每个申请都去读取雷同的 Topic 队列,则能够复用本地队列 cache。

  • lookup 组件

下面的推拉模型通过新音讯事件告诉机制来解决实时触达问题,事件推送至网关的时候须要一个匹配查找过程,只管简略的事件播送机制能够达到肯定的性能要求,但毕竟是一个播送模型,在大规模网关节点接入场景下依然有性能瓶颈。另外,终端设备场景有很多状态查问诉求,如查找在线状态,连贯互踢等等,依然须要一个 KV 查找组件,即 lookup。

咱们当然能够应用内部 KV 存储如 Redis,但咱们不能假设零碎(产品)在用户的交付环境,尤其是专有云的非凡环境肯定有牢靠的内部存储服务依赖。

这个 lookup 查问组件,实际上就是一个 KV 查问,能够了解为是一个分布式内存 KV,但要比分布式 KV 实现难度至多低一个等级。咱们回忆一下一个分布式 KV 的基本要素有哪些:

如上图所示,个别一个分布式 KV 读写流程是,Key 通过 hash 失去一个逻辑 slot,slot 通过一个映射表失去具体的 node。Hash 算法个别是固定模数,映射表个别是集中式配置或应用一致性协定来配置。节点扩缩个别通过调整映射表来实现。

分布式 KV 实现通常有三个根本关键点:

1)映射表一致性
读写都须要依据上图的映射表进行查找节点的,如果规定不统一数据就乱了。映射规定配置自身能够通过集中存储,或者 zk、raft 这类协定保障强一致性,然而新旧配置的切换不能保障节点同时进行,依然存在不一致性窗口。

2)多正本
通过一致性协定同步存储多个备份节点,用于容灾或多读。

3)负载调配
slot 映射 node 就是一个调配,要保障 node 负载平衡,比方扩缩状况可能要进行 slot 数据迁徙等。

咱们次要查问和保留的是在线状态数据,如果存储的 node 节点宕机失落数据,咱们能够即时重建数据,因为都是在线的,所以不须要思考多正本问题,也不须要思考扩缩状况 slot 数据迁徙问题,因为能够间接失落重建,只须要保障要害的一点:映射表的一致性,而且咱们有一个兜底机制——播送,当分片数据不牢靠或不可用时进化到播送机制。

架构设计

基于后面的实践和模型剖析介绍,咱们在思考用什么架构状态来反对一体化的指标,咱们从分层、扩大、交付等方面进行一下形容。

  • 分层架构

咱们的指标是冀望基于 RocketMQ 实现一体化且自闭环,但不心愿 Broker 被侵入更多场景逻辑,咱们形象了一个协定计算层,这个计算层能够是一个网关,也能够是一个 broker 插件。Broker 专一解决 Queue 的事件以及为了满足下面的计算需要做一些 Queue 存储的适配或革新。协定计算层负责协定接入,并且要可插拔部署。

  • 扩大设计

咱们都晓得音讯产品属于 PaaS 产品,与下层 SaaS 业务贴得最近,为了适应业务的不同需要,咱们大抵梳理一下要害的外围链路,在上下行链路上增加一些扩大点,如鉴权逻辑这个最偏业务化的逻辑,不同的业务需要都不一样,又比方 Bridge 扩大,其可能把终端设备状态和音讯数据与一些内部生态系统(产品)买通。

  • 交付设计

好的架构设计还是要思考最终的落地问题,即怎么交付。现在面临的现状是公共云、专有云,甚至是开源等各种环境条件的落地,挑战十分大。其中最大的挑战是内部依赖问题,如果产品要强依赖一个内部零碎或产品,那对整个交付就会有十分大的不确定性。

为了应答各种简单的交付场景,一方面会设计好扩大接口,依据交付环境条件进行适配实现;另一方面,咱们也会尽可能对一些模块提供默认外部实现,如上文提到的 lookup 组件,反复造轮子也是不得已而为之,这个兴许就是做产品与做平台的最大区别。

对立存储内核

后面对整个协定模型和架构进行了具体介绍,在 Broker 存储层这块还须要进一步的革新和适配。咱们心愿基于 RocketMQ 对立存储内核来撑持终端和服务端的音讯收发,实现一体化的指标。

后面也提到了终端音讯场景和服务端一个很大的区别是,终端必须要有个客户端维度的队列能力保障牢靠触达,而服务端能够应用集中式队列,因为音讯轻易哪台机器生产都能够,然而终端音讯必须明确牢靠推送给具体客户端。客户端维度的队列意味着数量级上比传统的 RocketMQ 服务端 Topic 队列要大得多。

另外后面介绍的队列模型外面,音讯也是依照 Topic 队列进行存储的,MQTT 的 Topic 是一个灵便的多级 Topic,客户端能够任意生成,而不像服务端场景 Topic 是一个很重的元数据强治理,这个也意味着 Topic 队列的数量级很大。

海量队列

咱们都晓得像 Kafka 这样的音讯队列每个 Topic 是独立文件,然而随着 Topic 增多音讯文件数量也增多,程序写就进化成了随机写,性能降落显著。RocketMQ 在 Kafka 的根底上进行了改良,应用了一个 Commitlog 文件来保留所有的音讯内容,再应用 CQ 索引文件来示意每个 Topic 外面的音讯队列,因为 CQ 索引数据较小,文件增多对 IO 影响要小很多,所以在队列数量上能够达到十万级。然而这终端设备队列场景下,十万级的队列数量还是太小了,咱们心愿进一步晋升一个数量级,达到百万级队列数量,咱们引入了 Rocksdb 引擎来进行 CQ 索引散发。

Rocksdb 是一个宽泛应用的单机 KV 存储引擎,具备高性能的程序写能力。因为咱们有了 commitlog 已具备了音讯程序流存储,所以能够去掉 Rocksdb 引擎外面的 WAL,基于 Rocksdb 来保留 CQ 索引。在散发的时候咱们应用了 Rocksdb 的 WriteBatch 原子个性,散发的时候把以后的 MaxPhyOffset 注入进去,因为 Rocksdb 可能保障原子存储,后续能够依据这个 MaxPhyOffset 来做 Recover 的 checkpoint。咱们提供了一个 Compaction 的自定义实现,来进行 PhyOffset 的确认,以清理已删除的脏数据。

轻量 Topic

咱们都晓得 RocketMQ 中的 Topic 是一个重要的元数据,应用前要提前创立,并且会注册到 namesrv 上,而后通过 Topicroute 进行服务发现。后面说了,终端场景订阅的 Topic 比拟灵便能够任意生成,如果基于现有的 RocketMQ 的 Topic 重治理逻辑显然有些艰难。咱们定义了一种轻量的 Topic,专门反对终端这种场景,不须要注册 namesrv 进行治理,由下层协定逻辑层进行自治理,broker 只负责存储。

总结

本文首先介绍了端云音讯场景一体化的背景,而后重点剖析了终端音讯场景特点,以及终端音讯场景撑持模型,最初对架构和存储内核进行了论述。咱们冀望基于 RocketMQ 对立内核一体化反对终端和服务端不同场景的音讯接入指标,以可能给使用者带来一体化的价值,如升高存储老本,防止数据在不同零碎间同步带来的一致性挑战。

正文完
 0