关于RocketMQ那些你可能不知道的性能优化!

800 阅读9分钟

我正在参加「掘金·启航计划」

惆怅东栏一株雪,人生看得几清明。

RocketMQ Binder集成

上回书说到Spring Cloud Stream,一个Spring提供的简单易用的消息编程模型,也算是一种规范吧,其内部是基于发布/订阅模型实现。如果不熟悉,咱可以翻翻上回书哈。

RocketMQ也是根据我们上面说的Spring Cloud Stream的规范做了相应的自己的实现,这里咱们就不展开细说了,有兴趣大家可以去翻翻源码。RocketMQ 由于对应的Binder类实现,可以说是集成了消息发送,消息订阅。为了方便大家去阅读源码,我简单的跟大家说一下。

集成消息发送

RocketMQMessageChannelBinder类根据规范完成RocketMQMessageHandler的创建和初始化,RocketMQMessageHandler 是消息处理器MessageHandler(上期有提到)的具体实现,其主要作用是转化消息格式并且发送消息,此类中持有RocketMQTemplate对象,RocketMQTemplate是对RocketMQ客户端API的封装。

是不是已经开始有点晕了?

简单来说,这个handler是一个很关键的类,它就是包含消息发送的逻辑处理,解析Message对象头中的参数,调用上面说的RocketMQTemplate中不同的消息发送接口。无论你是发送的顺序消息,事物消息,定时消息还是普通消息,都是由Message对象头(Header)中的属性决定,在我们业务代码创建Message的时候去设置。

划重点咯,如果大家有兴趣,可以把RocketMQMessageHandler这个类当作一个入口,就可以很轻松看到整个消息发送的流程。

image-20221019223321664.png

集成消息订阅

同样,由RocketMQMessageChannelBinder类根据协议完成RocketMQInboundChannelAdapter(简称adapter)的创建和初始化,顾名思义,这个adapter适配器需要适配Spring Framework中的重试和回调机制,它的作用是订阅消息并且转换消息格式。其中的RocketMQListenerBindingContainer对象是对RocketMQ客户端API的封装。

RocketMQ提供了两种消费模式,顺序消费和并发消费,不管是哪种消费模式,其对应的监听器收到消息后都会回调RocketMQListener。RocketMQListener 也是SpringBoot中已支持的RocketMQ组件,在adapter中创建和初始化RocketMQListener的实现类。上面两种消费模式的监听器在监听到消息的时候,会先回调RocketMQListener的onMessage()方法,然后在调用adapter父类中的sendMessage()方法将消息发送到DirectChannel。

以上就是RocketMQ Binder集成的大致过程,我们来总结一下:

  • Spring Cloud Stream 提供了简单易用的消息编程模型,内部是基于发布/订阅模型来实现的。

  • Spring Cloud Stream 的Binder提供标准协议,不同的消息中间件都可以按照标准协议接入。

  • Binder提供bindConsumer和bindProducer接口协议,分别用于构造生产者和消费者。

RocketMQ集群管理

在分布式服务(SOA)的架构中,我们是不允许任何中间件是单点的,服务发现机制也是必备的。咱们的服务实例会有多个,数量也是动态变化的,随时可能存在服务挂掉再重启的可能性。服务发现的意义就在于其提供的服务管理能力,服务调用方在注册中心获取服务提供的的信息,从而进行远程调用。

RocketMQ架构设计

说到RocketMQ的架构设计,就不得不说一下它和Kafka的渊源了。Kafka也是一款高性能的消息中间件,在大数据的场景中经常会使用,但是因为Kafka不支持消息失败重试,定时消息,事物消息,它的顺序消息也有明显的缺陷的,在复杂的业务场景下难以支撑。所以淘宝的中间件团队参考Kafka设计了RocketMQ,所以两者在概念上会有一点点相似。

我们常见的消息中间件,比如RocketMQ,Kafka,RabbitMQ等都是基于发布订阅模型,Producer把消息发送到消息服务器,也就是消息中间件,然后Consumer通过订阅来从消息服务器订阅自己想要的消息。在整个过程中,Producer和Consumer都是客户端,而消息服务器是服务端。客户端和服务端都需要通过注册中心来感知对方的存在。

下面我们就由一张图来看看RocketMQ的架构设计:

image-20221019232302903.png

RocketMQ的架构主要分为四个部分,全部支持分布式集群部署:

  • Producer:消息发送者,主要负责把消息发送到Broker。

  • Consumer:消息消费者,主要负责从Broker订阅消费消息。

  • Broker:消息存储者,主要负责消息的存储,投递和查询,以及服务高可用的保障。

  • NameServer:服务管理者,主要负责管理Broker集群的路由信息。

多说一下,NameServer是一个简单的Topic路由注册中心,其角色类似于Dubbo中依赖的ZooKeeper,支持Broker的动态注册与发现,它主要由两个功能:

  • 服务注册:接收Broker集群的注册信息,保存下来作为路由信息的基本数据,还会提供心跳检测,检查Broker是否存活。

  • 路由信息管理:由于NameServer保存了Broker集群的路由信息。Producer和Comsumer通过NameServer可以知道Broker集群的路由信息,从而进行消息的投递与消费。

RocketMQ基本概念

说完了RocketMQ的架构设计,我们再来捋一捋它的基本概念,什么是Message,什么是Topic,什么又是Queue?

  • Message:消息,顾名思义,生产和消费数据的最小单位,每条消息必须属于一个Topic,RocketMQ中每条消息拥有一个唯一的MessageID,且可以携带具有业务表示的Key。

  • Topic:主题,表示的是一类消息的集合,每个主题都有若干条消息,我们可以试想一下发邮件的过程,多条邮件可以同属一个主题,只是标识出此邮件的分类信息。Topic是消息订阅的基本单位。

  • Queue:消息队列,组成Topic的最小单元,一个Topic可以有多个消息队列,Topic是逻辑上的概念,而Queue是物理存储,在Consumer消费Topic时底层实际拉取的是Queue的消息。试想我们一个类别的消息,可以有多个消息发送通道就可以理解啦。

  • Tag:为消息设置的标志,用于同一个主题下面区分不同类别消息,这个应该很好理解,比如同一个食物主题下,可以通过Tag来区分是菠萝还是黄瓜,可以根据不同的业务设置不同的Tag。

  • UserProperties:用户自定义的属性集合,属于Message的一部分。

  • ProducerGroup:同一类Producer的集合,这一类Producer发送同一类消息且发送的逻辑一致。值得一提的是,如果发送的是事物消息且发送者在发送消息后崩溃,则Broker会提醒同一个Group下的其他生产者实例以提交或回溯消费。

  • ConsumerGroup:同一类Consumer的集合,有生产者集合当然就有消费者集合啦,同样ConsumerGroup通常消费同一类消息且消费逻辑一致。消费者组使得在消费消息方面,实现负载均衡和容错变得更加容易,需要注意的是,同一消费者组下的消费者必须订阅同一个Topic。

为什么放弃Zookeeper选择NameServer

基本概念也说完啦,下面我们来简单聊一下,既然RocketMQ可以说是Kafka的改进,那为什么抛弃了Kafka使用的Zookeeper注册中心反而选择NameServer呢?

在Kafka中的服务注册与发现通常使用Zookeeper来完成的,RocketMQ其实早期也适用了Zookeeper来做集群的管理,但是后来放弃了转而使用自己开发的NameServer。

那为什么RocketMQ的团队会放弃Zookeeper,选择重复造轮子呢?

简单来说,在Kafka中,Master和Slave是在同一台Broker机器上,Broker机器上有多个分区,每个分区的Master/Slave身份是在运行的过程中通过选举出来的,Broker机器拥有双重身份,既有Master分区,也有Slave分区。

而在RocketMQ中呢,Master和Slave不在同一台Broker机器上,我们可以通过上面的架构图看出来,Broker的Master/Slave身份在Broker配置文件中事先定义好的,在Broker启动之前就已经定好了。

那这个差异会有什么问题呢?

Kafka的Master需要选举,而RocketMQ不需要。

问题其实就出在这个选举上,选举的原理就是少数服从多数,那么Zookeeper的选举机制就必须由Zookeeper集群中的多个实例共同完成,那么这样一来,集群中的多个实例必须相互通信。

当然通信也不是问题,那么如果集群中的实例很多,网络通信就会变得异常复杂且低效。

而NameServer的设计目标是让网络通信变得简单,从而使性能得到极大提升。

为了避免单点故障,NameServer也必须以集群的方式部署,但是集群中各个实例相互之间不会进行通信。NamerServer是无状态的,可以任意部署多个实例。Broker会向每一台NamerServer注册自己,因此每一个NameServer实例都可以保存一份完整的路由信息。值得提一下的是,NameServer与每一台Broker机器保持长连接,间隔30s从路由注册表中将故障机器移除,但不会通知Producer和Consumer。

那么问题来了,这样的条件下,Producer和Comsumer仍然从故障机器获取信息,Producer将消息发送到故障机器,而Comsumer订阅消息失败,如何处理?

其实RocketMQ为了简化NameServer的设计,这两个问题是在客户端来解决的,具体如何解决,后续我们聊高可用的时候,再来详细分析吧。

还有一个问题是,由于NamerServer之间不通信,在某个时刻,NameServer保存的路由信息可能不一致怎么办?

仔细想一下,其实这个问题对发送消息和消费消息不会有什么影响,且我们可以看出来,NameServer是CAP中的AP架构,对吗?

好啦,我们关于RocketMQ的架构分析以及基本概念都有了一定的了解,下一期我们来聊一聊RocketMQ是如何实现顺序消息的。

后记

在写上面的内容的时候,查了一些资料,看了一些源码,有一些代码就不贴出来了。

对了,一边看一边思考,效果更佳哦!