Skip to content

36 云原生:业界MQ的计算存储分离是如何实现的?

你好,我是文强。

在功能篇,我们分析了消息队列中多个主要功能的技术实现。从这节课开始,我们将结合 云原生ServerlessEDA(Event-driven Architectures)存算分离分层存储数据集成 等一些业界较新的技术架构理念,来讲一下消息队列如何与这些架构理念结合,以及结合后会具备哪些实际价值。

这节课我们就重点学习一下消息队列的存算分离架构。

什么是存算分离架构

首先,你得清楚什么是存算分离架构。

存算分离中的“存”是指存储层,“算”是指“计算层”。简单理解“计算”就是功能相关的实现,“存储”是指数据落地持久化存储。消息队列中的存储层是指包括存储结构设计、消息存储格式、数据分段等具体的数据存储功能。计算层是指包括协议解析、事务消息、延时消息等主要消耗计算资源(如CPU)的功能模块。

跟存算分离相对的是存算一体,“分离”和“一体”是指计算层和存储层是否在一台机器上。

如下图所示,这是存算一体的消息队列的架构。从单机维度看,计算层和存储层都在同一个Broker中,由多台Broker组成一个集群。我们在 第15讲 讲过有状态服务和无状态服务,这种存算一体的架构就是典型的有状态服务。

存算一体架构的最大特点就是计算层和存储层没有明显的界限,从代码层面上看,计算层和存储层交互的操作就是文件的读写,比如我们在 第06讲 讲到的FileChannel.write、FileChannel.read。 它的主要优势是架构简单、开发实现成本较低。缺点是它是一个有状态服务,无法快速弹性地扩容。

而随着数据量越来越大,具备弹性快速扩缩容能力的消息队列集群可以极大地降低资源和人力成本,而 存算分离架构则是目前实现弹性消息队列集群的主要技术方案。

再来看下面这张图,图中分离了计算层集群和存储层集群。计算层集群主要负责消息功能类的操作,比如压缩解压、事务消息、死信消息等等。存储层负责数据的存储。

从数据流动来看,当数据写入到计算层集群,计算层会进行相关处理,然后通过网络协议将数据写入到存储集群。从功能上看,存储集群几乎没有任何逻辑,核心工作就是接收到数据,然后存储。

存算分离架构的优点是计算层为无状态,因此计算层的扩缩容就很方便。缺点是架构变复杂,代码实现难度也提升很多,日常的运维、研发的学习成本也会相应提高。另外计算层和存储层的交互从本地调用变为了网络协议的调用,性能上会有一些下降。

所以可以看到,存算分离和存算一体架构有各自的优缺点。那消息队列真的需要存算分离的架构吗?关于这个问题每个人都有自己的看法和判断,我讲一下自己的观点。

我们真的需要存算分离架构吗

我个人的判断是:存算分离是消息队列架构中的可选项,而不是必选项。为什么这么说呢?

存算分离架构最大的好处就是 集群变得更加弹性。从终态来说,没有存算分离,消息队列架构就无法 Serverless 化,也就无法做到快速扩缩容。从成本结构的角度来看,没法快速扩缩容,那么就无法提高集群的利用率,也就无法很好地降低成本。

那既然如此,它应该是一个必选项,为什么还是一个可选项呢?

我认为核心原因是: 用户诉求的多元化。在我多年负责消息队列云产品的经历中,我最大的感受就是用户是多元的,从而导致诉求也是多元的。也就是说,不是所有的客户对弹性和成本都有很强的诉求。

我们来看一下以下几类常见的客户:

  1. 某个中长尾企业客户,业务规模不大,可以说很小。他们对消息队列的需求是稳定、能用、免运维。因为规模不大,弹性和成本不是他们的痛点。
  2. 某个大型企业,大部分业务的流量都不高,而且一般按部门划分使用。所以他们的核心诉求跟第一类客户很像,只是多了一些成本的诉求。
  3. 某个私有化部署或自建集群的客户,他们的要求是能满足业务场景,集群稳定、运维简单、不要出问题。
  4. 某个大型企业的核心业务,流量比较大,也有弹性的需求。从业务角度看,稳定性、出问题的恢复速度是他们的核心诉求。弹性和成本是很重要,但那是第二梯队考虑的事儿。
  5. 某个云服务提供商,提高产品竞争力是核心诉求,而成本是产品竞争力的核心。

从上面的用户画像来看,1、2、3 类型的用户数量基本占了80%~90%;4类型的客户,弹性和成本是很重要的一个点,但是他们也愿意投入更多成本来提高稳定性;5类型的客户,弹性和成本是他们的核心诉求。

所以有个有趣的现象是,你去观察一些技术文章、大会分享,提存算分离概念最多的一般都是云服务提供商。为什么呢?原因就是他们有这个需求,而一些小公司,更多是聚焦在功能和使用层面,对存算分离没有那么刚需。从理论上来看呢,这也符合二八原则,即大部分的客户其实并不需要存算分离架构带来的好处,或者因为规模限制,根本用不到存算分离的优势。

所以我们上面才会说存算分离是可选项,而不是必选项。或者说合理的架构是: 既可以存算一体也可以存算分离的可插拔的存储结构

接下来我们拆解一下如果要实现存算分离的架构,技术上都是怎么做的?

实现存算分离架构的技术思考

从功能上来看,存储层的核心功能就是保证数据高性能和可靠的存储,本身没有太多复杂的计算逻辑。所以存算分离架构的核心就是 选择合适的存储层引擎

因为不同存储层引擎的特性不一样,所以基于不同引擎设计出来的系统架构也完全不一样。那么如何选择合适的存储层引擎呢?

如何选择合适的存储层引擎

先来看下图,从业界的主流组件来看,可选择的存储层引擎主要有对象存储、分布式存储服务、虚拟云盘三类。

  • 对象存储 是指各个云厂商提供的商业化的对象存储服务,比如AWS的S3、腾讯云的COS等。
  • 分布式存储服务 是指一些专门用来提供分布式的数据存储的组件,比如HDFS、BookKeeper等等。也有一些公司会自己开发分布式文件系统,比如阿里的盘古、腾讯的CFS等等。
  • 虚拟云盘 是指云厂商提供的在线云盘服务。

对象存储最大的特点是,它具备分布式可靠存储的能力且存储成本较低。缺点是读写方式不够灵活,流式读写性能较低。所以在消息队列这种需要高性能、高吞吐的场景中,比较难以大规模在实时流数据场景中使用。但是因为它具有非常明显的成本优势,业界还是在持续探索对象存储在实时流场景中的使用,以求通过技术手段发挥其成本价值。

分布式存储服务的优点是具备分布式存储能力,读写性能也较高。缺点是存储集群本身会有一些稳定性和可靠性问题。从技术上看,稳定性和可靠性问题可以通过技术和运维优化来解决。

云盘的特点是数据在远端是多副本可靠存储的,天然支持分布式可靠存储的能力。它的缺点是云盘需要先绑定节点,同一时间只允许一台 Broker 写入数据到云盘。当 Topic 的分区迁移、Leader 切换的时候,需要将云盘从老的 Broker 节点卸载,再挂载到新的 Broker 节点,在这个过程中服务是停止的。从技术上看来,这个缺点几乎是不可解决的。

因为 分布式存储服务一般会提供多语言的流式写入的API进行数据读写,读写性能较高,比较适合消息队列的数据特点。所以从业界落地的角度来看,分布式存储服务用得比较多。比如 Pulsar 的存储层使用的是 BookKeeper,RocketMQ 5.0 的存储层用的是原先的 Broker 集群。

完成了存储层引擎选择之后,接下来我们来看看存储层和计算层的设计实现。这里我们主要分析 存储层中分区存储模型的设计计算层弹性无状态的写入 这两个关键部分。

从前面的课程可以知道,分区是消息数据的最小单位。所以在分层的架构中,首先要解决的就是计算层分区维度消息数据的存储问题,即分区数据在存储层是如何存储的。

存储层:分区存储模型的设计

我们在 第05讲 讲到了存算一体架构中的消息数据的存储模型,讨论了在单机维度,所有分区的数据都在一个“文件”,还是每个文件都是独立的“文件”。这在存算分离的架构中也是同样需要考虑的。

在存算一体架构中,这两种模型各有优劣,都有成熟产品使用。 但是在存算分离的架构中,基本都是每个分区一个“文件”的方案。主要是出于数据的读写性能考虑。在存算分离的架构中,我们是通过网络协议从存储层读取数据的。

来看下图,如果数据存储在一份文件中,则存储层在读取数据时就需要维护二级索引,并启动随机读,在性能上会有一定的降低。

所以合理的方案如下图所示,不同的分区在存储层有独立的“文件”存储,然后顺序读写不同的段文件。

分区存储模型的设计,严重依赖存储层引擎的选择,我们一会儿讲 RocketMQ 和 Pulsar 的分区存储模型时,你再结合这部分的内容,会理解得更加透彻。

接下来我们看一下计算层如何实现无状态的写入。

计算层:弹性无状态的写入

上面讲到,存储层是一个具备多副本可靠存储能力的分布式存储服务。所以如下图所示, 计算层中 Topic 的分区就不需要有副本的概念,数据的可靠存储可以交给存储层去解决。即计算层的 Topic 永远是单分区。

从数据流的角度来看,分区的数据写入到存储层,依赖存储层多副本存储的能力实现数据的可靠存储。从技术来看,副本并没有被省掉,只是将副本概念下沉到了存储层而已。

在上图中,Broker 接收到数据后,是需要转发到存储层存储的。在 第07讲 中,客户端数据写入到分区有 Metadata(元数据)寻址机制和服务端内部转发机制两种形式。在存算一体的架构中,推荐使用的是 元数据寻址方案。但是在存算分离的架构中,数据都需要转发写入到存储层,因为都需要再转发一次,所以服务端内部转发也变为了一种常用方案。

比如 Pulsar 当前的计算层 Broker 和分区也是绑定的,也需要经过寻址,所以 Pulsar 用的就是元数据寻址方案。RocketMQ 5.0 架构中,Proxy 是完全无状态的,每台计算层 Broker 收到数据后,转发写入到后端的存储层,所以RocketMQ 用的是服务端内部转发的方案。

从技术上看, 计算层和存储层之间的调用是比较重要的一个模块。计算层Broker 需要使用存储层引擎提供的SDK或者写入方式将数据写入到存储层。比如 Pulsar 是调用 BookKeeper 提供的SDK 将数据写入BookKeeper的。此时考验的就是编码的技巧和功力了,比如线程管理、线程安全、批量写入等等。这块是需要注意的,可以回顾一下 第18讲

另外, 计算层 Broker 对消息队列各个功能的支持,都是我们在进阶篇讲的方案,不会因为架构的变化有大的改变,但是在实现细节上会有些许差异,比如事务的实现,有兴趣的话你可以去研究下。

目前业界主流消息队列Kakfa、RocketMQ 、RabbitMQ都是存算一体的架构。RocketMQ 从5.0 版本开始,在往存算分离架构的方向演化,但本质还是存算一体的架构。而Pulsar 从一开始的设计就是存算分离的架构。

一个是从存算一体演化到存算分离架构,一个是一开始就是往存算分离架构设计的,两条技术路径演化是不一样的。接下来,我们就来分析一下 RocketMQ 5.0和 Pulsar 在存算分离上的架构的实现,看一下有什么异同点。

业界主流存算分离架构分析

我们先来看看 RocketMQ 存算分离的架构。严格意义上说,当前 RocketMQ 5.0 不是存算分离的架构,只是 代理(Proxy)模式

RocketMQ 5.0 架构分析

来看下面这张图,这是 RocketMQ 5.0 中添加了Proxy 组件后的架构图。

在上图中,gRPC Server 就是 Proxy 组件,NameServer 、Broker A1和A2的Master/Slave 就是原先的主从结构的 Broker 集群,gRPC Server 前面可以挂载负载均衡组件。MQAdmin 客户端和 RocketMQ 5.0 的客户端可以直连负载均衡或gRPC Server,进行读写、管控等操作。gRPC Server 收到请求后,再把请求转发到实际的 Broker 集群。

上面的架构图有点复杂,可以简化为下面这张图,就更好理解了。可以看到,RocketMQ 5.0 就是在原先 Broker 集群的基础上添加了一个 Proxy 组件。

所以从技术上看,当前的 Proxy 组件只是转发层,不处理任何计算和存储的逻辑。集群实际意义上的计算和存储逻辑,都是在 Broker 集群上完成的。这就是我们前面所说的,当前 RocketMQ 5.0 的架构不是真正意义上的存算分离架构的原因。更准确的说法是, RocketMQ 5.0 只是从当前存算一体的架构往存算分离架构演化走出了第一步。

在我看来,接下来 RocketMQ 要做的就是将当前 Broker 集群上的计算逻辑向上移动到 Proxy 组件里面,让 Proxy 组件来完成计算层逻辑。从而把当前的 Broker 集群完全变为一个存储层。此时结合我们上面讲到的存算分离的设计思路:

  1. 在存储引擎的选择上,RocketMQ 实际就是选择分布式存储服务的方案,只是这个分布式存储服务是 RocketMQ 当前已经有的 Broker 集群。
  2. 在分区存储模型的设计上,我个人判断,短时间还是大概率会沿用当前的分区存储模型。因为如果改动存储模型,从架构演化的角度来看,改动太大,开发成本太高。
  3. 无状态写入则是一个比较好实现的方案,当前 Proxy 的写入就是完全无状态的。数据随机写入任意一台Proxy,再转发到具体的Broker 集群。

所以你可以看到,RocketMQ 的存算分离架构是演化来的,而不是一开始就往这个方向设计。因此 RocketMQ 往存算分离架构演化的挑战非常大,因为它需要兼顾到当前架构中的功能和设计模型。简单说就是,有很多的历史包袱。接下来我们来看看 Pulsar 存算分离的架构。

Pulsar 存算架构分析

Pulsar 的架构从一开始就是往存算分离设计的。我们先来回顾一下在 第13讲 讲到过的Pulsar的架构图。

Pulsar 在设计的时候,就选择好了使用Apache BookKeeper来当作它的存储层。那为什么选择BookKeeper,不选择其他引擎呢?在我看来,有以下三方面原因:

  1. Bookeeper 设计的初衷就是用来高性能地存储分布式流日志的,而日志最大的特点就是顺序的Append模型,消息队列的消息数据特点也是顺序 Append 的,所以 BookKeeper 就很适合当作消息队列的存储层。
  2. BookKeeper 具备流式读写的能力,写入和读取的性能较高,并且具备分布式可靠存储的特性。
  3. 因为 Pulsar 社区的主要开发者是之前是维护 BookKeeper 的成员,比较熟悉BookKeeper,这可能也是其中一个原因。

以下还是我们在第13讲讲过的 Pulsar 消息数据底层的分区存储模型,来看下图简单回顾一下。

参考图示,Pulsar 计算层的分区都是单副本的,即没有副本的概念。每个 Pulsar 分区底层由多个 Ledger 组成,每个 Ledger 只包含一个分区的数据。每个 Ledger 有多个副本,这些 Ledger 副本分布在 BookKeeper 集群中的多个节点上。

从架构设计的角度,存储层 BookKeeper 跟 Pulsar 没有关系,它是一个独立的开源项目,消息队列相关的特性都是在计算层 Broker 中实现的。

Pulsar 在设计分区存储模型的时候,是根据 BookKeeper 已有的特性和概念来设计的。

比如说,BookKeeper 的最小存储单位是 Ledger,Ledger 里面由多个 Entry 组成,每个 Entry 可以理解就是一条数据。

基于 BookKeeper 的这些特性,Pulsar 分区模型的底层单位就是 Ledger,每条消息就是一个Entry,每个 Ledger 都是一个数据分段。这就是我们说的,分区存储模型的设计依赖于底层分布式存储引擎的选择的原因。当你选择了其他的分布式存储引擎,分区存储模型可能就是另外的实现。

再来看下面这张图,在计算层中 Pulsar 的分区是和某台Broker进行绑定的,可以简单理解这台Broker 就是分区的 Leader,分区数据的读写都是在这条 Broker 上完成的。客户端的生产消费请求发送到这台Broker后,Broker 会先经过计算逻辑的处理,再去BookKeeper节点读写数据。当某台Broker负载高时,就需要快速迁移分区降低Broker负载。因为计算层就都是无状态的,迁移起来就很快,直接修改元数据即可。

在内核具备了快速迁移的能力后,为了能够进行快速的负载调度,内核就需要具备自动化调度迁移的能力。所以 Pulsar 在内核中提供了自动化负载均衡的机制,即有一个主节点(可以理解就是我们讲过的Controller节点)不断地检测每台Broker的负载,然后根据一定的负载均衡策略执行 Topic 自动迁移,将负载高的节点上的分区迁移到负载低的节点。具体的迁移策略,就不展开了,有兴趣的同学可以去看一下官方文档 Broker LoadBalancing

同时,为了提高负载均衡和迁移的效率,Pulsar 引入了Bundle的概念。参考图示,Bundle是处于Namespace和Topic之间的一个概念,它是用来组织多个Topic的一个逻辑概念。即一个Namespace 有多个Bundle,一个Bundle里面有多个Topic。

那为什么要引入 Bundle 呢?引入Bundle又有什么好处呢?先卖个关子,这个就作为我们本节课的思考题了。

总结

存算分离架构不是银弹,它的核心优势是具备快速扩容能力,以及快速扩缩容能力带来的成本优势。但是相比存算一体的架构,存算分离架构的研发和运维成本很高。大部分客户用不到,甚至不需要存算分离架构带来的优势。所以从用户角度来看,存算分离架构应该是一个可选项,而不是必选项。

随着云服务的普及,像消息队列这种基础服务,最终会慢慢往云服务收归。站在云服务厂商的角度,弹性和成本将成为其核心竞争力。存算分离架构带来的集群弹性,在成本方面具有非常强的优势。所以存算分离对于这些厂商来说,是一个必须要走的路。

从技术上来看,选择一个可靠的远程存储是存算分离架构中最重要的点。目前业界远程存储主要有对象存储、分布式存储服务、虚拟云盘三种类型的选择。从消息队列的特性和业界的实现来看,分布式存储服务是用得最多的方案,比如BookKeeper、阿里的盘古等等。

在存算分离架构中,分区存储模型的设计是非常重要的一个点。但是从落地来看,一般是在选择好存储层引擎的基础上,再根据消息队列的存储特点进行设计。消息队列的存储特点建议回顾一下 第05讲

另外,在存算分离架构中,计算层的弹性有以下三个关键点:

  1. Topic 能够快速地迁移,不需要进行消息数据搬迁,这一点基于存算分离的架构就可以实现。
  2. 能够自动地迁移,即不需要人工介入,系统能够自动化地均衡迁移。这点要求集群内核具备自动化调度的能力。
  3. 能够高效地迁移,迁移过程是高效快速的,且对集群没有影响。这一点就很灵活,比如 Pulsar 中的 Bundle 就是起的这个作用。

从业界的进展来看,目前 RocketMQ 5.0 开始在往存算分离的架构演化,Pulsar 从设计开始就走的存算分离的架构方向。存算分离架构的主要痛点在于研发成本高、周期长、稳定性有待提升,还无法提供长期稳定的服务。

在我看来,虽然 Pulsar 是第一个存算分离架构的消息队列,但不能说它走的技术路径就是全对的。但是它本身的很多设计思想,很有先进性,很值得我们去参考。

思考题

Pulsar 的计算层为什么要引入 Bundle?引入Bundle又有什么好处呢?

欢迎分享你的思考,如果觉得有收获,也欢迎你把这节课分享给身边的朋友。我们下节课再见!

上节课思考闭环

因为Pulsar 是一个定位消息和流一体、发展速度很快的消息队列,所以我们并未在正文中进行总结。不过我们在表格中总结了 Pulsar 在功能层面的支持点,现在请你根据表格中的各个功能去学习一下 Pulsar 在这些功能上的使用和实现。

1. 顺序消息

2. 幂等

3. 定时/延时消息

4. 事务性消息

5. 死信队列

6. 消息查询

7. Schema

8. WebSocket

官方文档: https://pulsar.apache.org/docs/3.1.x/