RocketMQ 学习笔记

为什么要学习 RocketMQ?

相信无论是前端还是后端的同学,不管是在平常的自我充电、还是日常的工作中,对 RocketMQ、Kafka 等主流消息队列多多少少都有一定的了解。现在的服务端架构中,一个业务模块中的服务往往会根据一定维度拆分成多个服务。而消息队列大家可以理解为 多个服务、系统之间沟通的通道,如下图所示:

消息队列是沟通的桥梁

有了这个通道,系统 A 不再是直接与系统 B 交互,而是与消息队列交互。在异步场景下,通过消息队列这个通道交互,可以在产品侧获得更好的用户体验。在支持异步的同时,消息队列也能很好地支持系统间的解耦,而系统之间的松耦合能够给系统带来良好的扩展性、可维护性

消息队列作为现在技术架构中不可或缺的一部分,其重要程度不言而喻,应用也相当广泛。也正是因为这样,各个大厂都在相继自研消息队列,例如最初来自 LinkedIn 的 Kafka、来自阿里的 RocketMQ、以及来自 Yahoo 的 Pulsar

那面对如此多的消息队列,咱们为啥要选择 RocketMQ 呢?

第一点,开发语言优势。RocketMQ 使用 Java 语言开发,比起使用 Erlang 开发的 RabbitMQ 来说,有着更容易上手的阅读体验和受众。在遇到 RocketMQ 较为底层的问题时,大部分熟悉 Java 的同学都可以深入阅读其源码,分析、排查问题,甚至可以在社区版的基础上进行二次开发

第二点,丰富的高级特性。根据 RocketMQ 官方文档的列举,其高级特性达到了 12 种,例如顺序消息、事务消息、消息过滤、定时消息等,这些特性在后续我们都会进行讲解。RocketMQ 丰富的特性,能够为我们在复杂的业务场景下尽可能多地提供思路及解决方案。

第三点,良好的商业前景。比如阿里,在其生产环境中已经部署了数百个 RocketMQ 集群,上千个节点,如此大量地将 RocketMQ 应用于生产环境,足以说明 RocketMQ 是的确经得起残酷的生产环境考验的,并且能够针对线上环境复杂的需求场景提供相应的解决方案。

RocketMQ 不仅仅解决了阿里的内部需求,同时还被搬到了阿里云上,作为一个商业化的产品对外提供服务。在阿里云的产品叫作“消息队列 RocketMQ 版”,自从 2016 年开始商业化到现在,RocketMQ 商业化版已经具有相当大的规模。良好的商业前景,也反向推动着 RocketMQ 在业界的普及,两者相辅相成、相得益彰。

第四点,众多大厂背书。RocketMQ 现在被广泛应用于各个大厂的内部业务中,其诞生之地阿里自不必多说,历年来大家所熟悉的双十一促销,在阿里内部就是使用的 RocketMQ 来承载消息,面对如此庞大的流量洪峰,RocketMQ 交出了一份令人满意的答卷。再比如,RocketMQ 也是阿里交易链路中的核心产品。交易链路本已是核心中的核心,而这个核心又将 RocketMQ 当作核心链路中的核心,足以见对 RocketMQ 的重视程度。

同时,字节跳动内部不同的业务也都在大量地使用 RocketMQ,同样也是作为业务主流程中的核心组件,RocketMQ 在生产环境中保持着非常高的稳定性、可用性,非常良好地支撑了业务的运行与发展。

一门技术,有大厂背书意味着什么?

  • 意味着,这个技术经历过严酷的生产环境考验,非常可靠。
  • 意味着,你在开发中可能遇到的问题,大厂大概率都会、甚至已经遇到,并且大概率都会提供相应的解决方案。
  • 意味着,这门技术大概率会更加地流行,而更流行又会让社区越来越活跃,社区越活跃,相应业务场景“开箱即用”的解决方案就越多,形成良性循环。例如,Java 的社区、生态非常好,非常多的业务场景你甚至能直接找到封装好的工具。
  • 意味着,如果你能熟练掌握这门技术,而对应的公司恰好也在使用这门技术,可能会让你的求职更加顺利。

所以说,RocektMQ 有着不可多得的先发优势——所谓的大厂背书

学习原理类课程的痛点

看似我们看了非常多的模块、非常多的文件、非常多的代码,但是实际上等我们看完之后会发现,我们的收获就跟上面这个水杯里的水一样,空空如也。看到这是不是有那么些许的悲惨?花了这么多时间学习,投入这么多精力,完了还没有收获。

之所以出现上面这个情况,是因为我们在不熟悉一个框架的情况下,就深入到其业务代码中去阅读源码,看似思路很清晰、目的很明确,实则会深陷入代码实现的无限细节当中无法自拔。

我们可能刚刚开始看时能够保持清醒,但再往源码深入几层,随着其内部函数的调用跳来跳去,很快就会被“绕晕“的。

RocketMQ 核心的概念

Broker:消息队列的核心

从这里开始,我们需要把整个消息队列当成一个黑盒子来看待。

啥叫黑盒子?举个例子,自动贩卖机对我们来说,就是个黑盒子。因为从我们的视角来看,操作就是:给现金或者扫码线上支付,然后机器就会给到你对应的商品。具体里面是机器在自动操作,还是活人在半自动操作,我们不得而知,也并不关心。

参考这个例子,消息队列作为一个黑盒子应该是个啥样呢?或许应该是这样:

把 RocketMQ 看成是一个黑盒子

Producer(生产者)只管投递消息到消息队列中,而 Consumer(消费者)则负责从消息队列中消费消息,这是符合大家对消息队列最初印象的工作流程。至于其内部如何实现的,我们现在也看不到,因为它目前是个黑盒子,内部的相关实现是没有暴露出来的。

RocketMQ 是一个内部拥有多个组件、且组件之间相互配合协作良好的精密机器,我们看到的只是封装之后的“假象”。实际上提供服务的叫作 Broker,你可以把它理解成消息队列的一个实例

Broker:消息队列的实例

Broker 可以说是 RocketMQ 核心中的核心,简单来说它负责接收 Producer 发送过来的 Message 、并将其持久化起来,同时负责处理 Consumer 的消费请求。上图提到的完整工作流程:投递 Message、存储 Message、处理消费请求等,Broker 都参与了。

当然,任何的框架、服务,只要是单节点部署就会有这个问题。所以,为了保证整个架构的高可用,RocketMQ 会部署多台 Broker 组成一个集群对外提供服务,如下图所示:

部署多台 Broker,组成集群对外提供服务

而 Producer 生产的消息会分布式存储在这些 Broker 上。啥叫分布式存储呢?举个例子,大家应该都有存钱在银行,假设 A 银行、B 银行都有存,那么 A 和 B 银行中的存款全部加起来,就是你的总存款。

消息队列的消息也是同理,消息分散存储在多台 Broker 上,每台 Broker 实际上存储的消息内容是不同的,将这些 Broker 存储的消息全部加起来就是全部的数据,如下图所示:

所有 Broker 存储数据是全量的数据

有的同学看到这里可能又会有问题了:“既然每台 Broker 存储的消息不同,那如果某台 Broker 突然整个挂了,这部分消息不就丢失了吗?就这还高可用呢?”

别急,我们继续往下看。

Topic:消息的组织者

如果 RocketMQ 真是这样设计的,那的确会有问题。当然,实际上肯定不是这样搞的。在 RocketMQ 中有 Topic 的概念:表示一类或者说一大类消息的集合

举个例子,假设某个 Topic 中存的全是跟订单相关的,那么里面的消息就有可能是:订单已创建订单已更新订单已付款……诸如此类的消息。

并且,Topic 在 RocketMQ 底层设计中其实是个抽象概念。Producer 在生产消息时,会指定将 Message 投递到某个 Topic,Consumer 消费消息时,也会指定一个 Topic 进行消费,如下图所示:

逻辑上的概念 Topic

这个时候,对于 Producer、Consumer 来说,消息队列就是个黑盒。它们只关心投递 Message 到指定的 Topic,再从指定的 Topic 消费,其余的一概不关心。

但其实上面的图画得不是很准确,因为我们从上图中能够分析出一个结论:一个 Topic 只会存储在一台 Broker 上。你可以思考一下,真的是这样吗?

当然不是,这个结论是错误的。这里我们从两个方面论证一下。

  • 第一个方面,假设 Topic A 和 Topic C 的 Message 体量在业务上非常小,而 Topic B 的数据量非常大,那么就会导致 Broker 的负载、存储压力都更大,导致严重的数据倾斜问题
  • 第二个方面,这样的设计不具备高可用性,当 Broker B 意外宕机,也就意味着 Topic B 中的 Message 会全部丢失

所以,为了让大家更好地了解正确的底层原理,我们还需要引入新的组件来帮助我们解惑。

MessageQueue:对 Topic 的再次细分

我们简单举一个例子来了解 MessageQueue。假设现在有 Topic A 这个 Topic,它有 3 个 MessageQueue,ID 分别为 0、1、2,那么用图来表示大概就是这样:

MessageQueue:对 Topic 的再次细分

分散的 MessageQueue 一起组成了一个完整的 Topic。同 Topic 一样,MessageQueue 也是一个逻辑上的概念,你可以把它理解成对底层存储的抽象

你也许看到过说 Message 会被丢到 MessageQueue 当中,但实际上 Message 最终会被存储在 Broker 所在机器的磁盘上,那里才是 Message 的”最终归宿”,如下图所示:

Message 最终都会存储在 Broker 的磁盘上

了解了 MessageQueue 的概念之后,同一个 Topic 下的 Message 会分散存储在各个 Broker 上,一来能够最大程度地进行容灾,二来能够防止数据倾斜。这下“鸡蛋不在一个篮子里了”,数据被较为均匀地分摊了出去,出现数据倾斜的概率也大大降低了。

不过眼尖的同学可能发现了:“就算是引入了 MessageQueue 让数据分散存储了,Broker B 如果挂了,数据该丢还得丢啊,之前只丢一个 Topic 的 Message,现在倒好,3 个 Topic 的数据都会丢。“

没错,按照上面的架构图我们得到的结论的确是这样。但实际上,RocketMQ 4.5 版本之前提供了基于主从架构的高可用机制。即将 Broker 按照角色分为 Master BrokerSlave Broker主从之间会定期地进行数据同步,就像这样:

Broker 主从数据同步

Master 负责响应客户端的读写请求、存储消息等,而 Slave 只负责一件事:同步 Master 的数据。

而在 RocketMQ 4.5 之后,RocketMQ 提供了高可用集群的实现 —— Dledger。上面讲的主从架构虽然能够做到主从之间的数据同步,但 Master Broker 挂了还是会让集群、数据受损,并且需要人工将 Slave Broker 切换成 Master Broker,然后集群才能正常提供服务。

熟悉 Redis 的同学应该可以做个类比。Redis 中的主从架构也是需要手动地使用 slave of 命令来使一台 Redis 去 Follow 指定的 Redis。这种架构下 Master Redis 出现问题,仍然需要手动切换。而后续 Redis 有了 Sentinel,它是对 Redis 集群高可用的解决方案,它的一大特性就是能够做到在发生故障时自动进行故障转移

Dledger 也会在发生故障时进行自动切换,其使用 Raft 协议重新选举出一个新的 Broker 重新对外提供服务,不需要人工介入

我们言归正传,上面引出了 MessageQueue 的概念之后,我们看似已经解决了所有的问题。但很遗憾,这只是一个假象,从概念上的确自洽了,但如果我们把上面的架构代入到真实的开发场景下,就会很容易发现问题。

例如,这里有多台 Broker 组成一个集群在对外提供服务,当 Producer 来建立连接时,应该选择哪台 Broker 呢?

Producer 如何知道选择哪台 Broker?

目标 Broker 的 IP 地址是要 Hard Code 到项目的配置文件吗?如果真的这么配置了,那配置的这台 Broker 突然挂掉了呢?难道我们还需要去改项目的配置文件吗?这明显是不靠谱的做法,就算是做成动态配置不用重新发布服务,那也需要一定的时间修改、等待生效。万一它抽风凌晨挂了呢?真要是凌晨应该还好,怕就怕它在业务最高峰期突然挂掉。

所以,为了回答这个问题,我们还需要继续引入新的组件 —— NameServer。

NameServer:RocketMQ 集群的大脑

NameServer 用于存储整个 RocketMQ 集群的 元数据,就像 Kafka 会采用 Zookeeper 来存储、管理集群的元数据一样。可能有小伙伴第一次听到元数据,对元数据这个概念拿不准,这里简单给大家解释一下。

就拿我们人来举例子。有个同学叫张三,像他的姓名、性别、身高、出生年月日、学历是啥、从哪儿毕业、在哪儿上班、甚至左撇子还是右撇子,都算张三这个个体的元数据。

类比张三,NameServer 中存放的元数据大概就是:

  • 集群里都有哪些 Topic?
  • 这些 Topic 的 MessageQueue 分别在哪些 Broker 上?
  • 集群里都有哪些活跃的 Broker?

那 NameServer 都是怎么知道这些信息的呢?这些信息不会凭空出现在 NameServer 中,毕竟说到底它也只是个服务。当然是 Producer、Consumer、Broker 自行将数据注册到 NameServer 的

Broker 在启动时会将自己注册到 NameServer 上,并且通过心跳的方式持续地更新元数据。此外,Producer、Consumer 都会和 NameServer 建立连接、进行交互来动态地获取集群中的数据,这样就知道自己该连接哪个 Broker 了。

我们来看看集成了 NameServer 的架构会变成什么样:

NameServer:集群的大脑

这次看上去好像真的没啥问题了,Broker 动态地将自己注册到 NameServer 上,Producer、Consumer 也能够感知到集群的整个信息。

不过眼尖的同学又有发现了:“你这个 NameServer 是个单点啊,NameServer 可以说是 RocketMQ 集群的大脑,如果 NameServer 挂掉了,岂不是整个集群都将无法正常工作。“

没错。所以实际的生产环境中,会部署多台 NameServer 组成一个集群对外提供服务,如下图所示:

将 NameServer 进行多副本的部署

NameSever 可以理解成一个无状态的节点。

看到这你可能会问:“这里面不是存储了很多的元数据吗?咋还能算无状态的节点呢?”这是因为 Broker 会将自己注册到每一个 NameServer 上,每个 NameServer 实例上都有完整的数据,所以可以将这一堆的 NameServer 看成是一个无状态的大“单体”应用。这样的多副本部署保证了 RocketMQ 中扮演大脑角色的 NameServer 的高可用,进而保证了整个 RocketMQ 的高可用。

总结

对于 Topic,在 RocketMQ 的底层设计中它是一个逻辑上的概念。而面对用户(也就是我们)时,它却是一个实打实的需要关注的概念。生产时需要关心,消费时也需要关心。关于 Topic,你可以理解为 RocketMQ 将底层负责的存储、调度的相关设计给封装了起来,让我们在使用的同时不用去关心过多其他的概念。

对于 Broker,它将从 Topic 中细分出来的 MessageQueue 分布式地存储在多台实例上。而 NameServer 则是整个 RocketMQ 集群的大脑,它几乎啥都知道,并且 NameServer 通过多实例的部署保证了自身的高可用。

Producer 在发送消息前从 NameServer 中获取 Topic 的路由信息也就是发往哪个 Broker,Consumer 也会定时从 NameServer 获取 Topic 的路由信息。

posted @ 2023-02-05 14:53  Dazzling!  阅读(157)  评论(0编辑  收藏  举报