RocketMQ的组件和其关系

1,052 阅读8分钟

前言

RocketMQ是一种消息队列中间件,包括服务端程序(Server)和客户端程序(Client)。Server包括充当注册中心角色的NameServer、存储与分发消息的核心组件Broker。Client包括我们使用过程中经常接触到的发送消息客户端Producer、订阅消息进行消费的客户端Consumer。

作为分布式中间件,需要保证高可用,一般各组件需要集群部署,这是来自Apache RocketMQ官网的一张关系图。下面就根据这张图详细讲讲各组件的作用和它们之间的关系。

NameServer集群

NameServer作为Borker集群的注册中心,被设计的非常轻量级,集群部署保证高可用,各节点之间彼此独立,没有主从概念。所有数据内存存储,不进行数据的持久化。

NameServer作为注册中心存储的东西:

  1. Map<String /*cluster name*/, Set<String> /*broker names*/>,Broker集群对应的BrokerName集合,可以看出一个NameServer集群可以实现多个Broker集群的存储。
  2. Map<String /*broker name*/, BrokerData /*brokers data*/>,BrokerData为Broker Master-Slave集群信息(所属cluster,机器地址等)
  3. Map<String /*Broker Addr*/, BrokerLiveInfo /*心跳信息*/>
  4. Map<String /*topic name*/, List<QueueData> /*message queues data*/>,QueueData中存储着BrokerName,读写队列个数,同步标记
  5. Map<String /*Broker Name*/, List<String> /*FilterServer*/,用于消息的过滤,不详细介绍

从前面的图中可以看出所有Broker与NameServer之间保持连接,交互路由信息,也就是维护上面所说的NameServer所存储的东西。

我们平时申请阿里云RocketMQ实例时,所拿到的NameServer地址就是NameServer集群的地址,拿到后我们所负责的Client(包括Producer和Consumer)便从NameServer进行Topic、Broker等信息的发现,以连接Broker进行消息的发送和接收。

以NameServer为核心看RocketMQ工作的大概过程:

  1. RocketMQ的Server集群启动时,首先启动NameServer
  2. 然后启动Broker,Broker向NameServer进行注册,维护了上面说的前三项数据。
  3. 在发消息或消费消息前我们需要创建Topic(可以通过参数控制在那些Broker上分别创建多少队列),Topic创建完成后,NameServer所存储的第四项数据也就维护成功。
  4. 启动Producer,生产消息并确定Topic和Tag,Producer从NameServer的第四项数据(Topic的队列信息)确定要和那些Broker建立连接,将消息生产到对应的Broker的对应Queue上
  5. 启动Consumer,确定需要消费消息的Topic和Tag,Consumer从NameServer的第四项数据(Topic的队列信息)确定要和那些Broker建立连接,消费对应Queue上的消息

Broker集群

Broker集群是RocketMQ的核心组件,主要有以下作用:

  1. Broker负责存储消息,并将消息根据Topic和QueueId信息分发到对应的队列ConsumeQueue上
  2. 存储Topic、SubscriptionGroup、ConsumeOffset等核心数据;

首先,从这张图可以看出Broker接受Producer生产消息到分发队列给Consumer进行消费的过程:

首先,最左侧,生产消息的第一步,CommitLog:存储着所有的实际消息体,文件形式,对于所有的Topic、Queue的消息均存储在一起,保证了生产消息时Broker使用顺序写的方式持久化,保证效率,可以存储为多个文件,默认每个文件大小1G。

向右,ConsumeQueue,也就是消费队列,从图中可以看出,Broker首先在CommitLog中存储消息,然后将消息通过具体的Topic和QueueId把其Offset和Size分发到ConsumeQueue再存储,ConsumeQueue相当于消费消息的一个索引(体积小,提高读的效率),Consumer便可以与ConsumeQueue打交道。一个Topic可以存在多个ConsumeQueue(可以在不同的Broker上,每个ConsumeQueue只能被一个Consumer消费),可以提高消息消费的效率并保证高可用;Broker存储每个Topic的每个ConsumeQueue的详细信息为文件,存储了对应消息在commitLog文件的Offset,可以定位到实际消息。

在部署了RocketMQ的机器上可以看到Broker中存储的文件:(目录一般在~/store/)

commitLog目录和consumequeue目录分别存储着上面介绍的CommitLog和ConsumeQueue。

可以看出Broker还存储着一些重要的文件,在config目录:

  1. topics.json存储了该Broker的所有Topic信息,包括名称、队列个数等。
  2. subscriptionGroup.json存储了所有Group的信息,消费者启动需要指定Group(消费组),同一个Group的多个Consumer会对消费的消息进行负载均衡,也就是不会重复消费。
  3. consumerOffset.json存储了每个Topic对应每个Group的每个ConsumeQueue当前消费完成的Offset;关系到消息如何保证不丢失、减少重复,详细的工作原理放到下一篇。

文章后面附录给出了这三个文件的样例,可以进行参考。

Client集群

我们日常使用RocketMQ时直接打交道的角色RocketMQ中的Producer集群和Consumer集群,也就是Client。

Producer集群用于向RocketMQ生产消息,可以选择生产到的topic、tag和其他一些信息(如ConsumeQueue)。所谓顺序消息,便是通过在生产消息时将想要顺序消费的消息都生产到同一个ConsumeQueue,因为一个ConsumeQueue只能被一个Consumer消费,只要在Consumer端使用一个线程消费,便可以保证这一批消息按照生产的顺序进行消费。

Consumer集群用于消费Producer生产的消息,需要指定Group,可以订阅特定topic、tag。RocketMQ支持消息进行集群消费和广播消费,广播消费指的是同一个Group下的所有Consumer均可以同样收到所有订阅的消息;而集群消费指的是同一个Group下的所有Consumer对所订阅的消息进行负载均衡的消费,正常情况下不会收到相同的消息。我们常用的消费方式是集群消费。

在使用Group时,需要注意,同一个Group的不同Consumer必须订阅完全相同的Topic和Tag,不然,便会出现订阅不一致的情况,出现消息消费延迟、丢失等问题。原因可以简单分析下:因为一个ConsumeQueue只能被一个Consumer消费,那如果不同的Consumer订阅不同的tag,那一定会出现有的Consumer订阅的tag的消息被生产到另一个Consumer所消费的ConsumeQueue上,如果后一个Consumer没有订阅这个tag,那么这条消息便丢失了。

关于高可用

前面提到,RocketMQ作为一种分布式消息中间件,需要集群部署来保证高可用,下面就来简单分析下RocketMQ的Server端如何保证高可用。

首先,NameServer,前面提到NameServer集群部署,节点之间不分主从,彼此独立,无数据同步。首先,Broker的每台机器与NameServer集群的每台机器保持长连接,并定时向Broker发送心跳,NameServer会定时扫描将不健康的Broker清除掉。如果一台NameServer宕机,其他NameServer上仍存在对应的Broker信息。然后关于Client端,Producer、Consumer的每台机器与NameServer集群的其中一台机器保持长连接,定时拉取最新的Broker、Topic、Queue信息,如果一台NameServer宕机,Client将会连接其他的NameServer。也就是说,了NameServer无论针对Server端的Broker、Client端的Producer和Consumer,均保证了高可用。

再说RocketMQ的核心组件Broker,从文章开头RocketMQ官网的图可以看出,RocketMQ一般采用多主多从集群部署。主从之间进行数据同步,Master接受生产的消息,Master、Slave均可以提供消息的拉取进行消费。多个Master其实拥有不同的Broker-Name(可以联想NameServer存储的几个Map),不同的Master上可以存储相同Topic的消息和队列,进行数据的分片。如果一台Master宕机,只要对应Topic在多台Master上都有对应Queue,那么Producer可以继续向活着的Master生产消息;而宕机的Master上已经生产还没消费的消息,只要同步到了其Slave,其Slave仍然可以向Consumer提供消息的拉取消费能力。也就保证了Broker无论对Producer还是Consumer的高可用。

关于高可靠,下一篇讲如何保证消息不丢失时再进行统一的讲述。

附录

文件样例

Broker机器上config目录下的三个重要的文件

topics.json

{
	"dataVersion":{
		"counter":4,
		"timestamp":1613835664763
	},
	"topicConfigTable":{
		// 省略了许多默认就存在的topic
        // 我创建的topic
		"TopicTest":{
			"order":false,
			"perm":6,
			"readQueueNums":8,
			"topicFilterType":"SINGLE_TAG",
			"topicName":"TopicTest",
			"topicSysFlag":0,
			"writeQueueNums":8
		},
        // 每个消费组group对应一个重试队列,消费失败的消息通过这里重试
		"%RETRY%please_rename_unique_group_name_4":{
			"order":false,
			"perm":6,
			"readQueueNums":1,
			"topicFilterType":"SINGLE_TAG",
			"topicName":"%RETRY%please_rename_unique_group_name_4",
			"topicSysFlag":0,
			"writeQueueNums":1
		}
	}
}

subscriptionGroup.json

{
	"dataVersion":{
		"counter":2,
		"timestamp":1613835664758
	},
	"subscriptionGroupTable":{
		// 省略了许多默认就存在的Group
		"please_rename_unique_group_name_4":{
			"brokerId":0,
			"consumeBroadcastEnable":true,
			"consumeEnable":true,
			"consumeFromMinEnable":true,
			"groupName":"please_rename_unique_group_name_4",
			"notifyConsumerIdsChangedEnable":true,
			"retryMaxTimes":16,
			"retryQueueNums":1,
			"whichBrokerWhenConsumeSlowly":1
		}
	}
}

consumerOffset.json

{
	"offsetTable":{
        // topic结合消费组为key,value是一个map,key为queueid,value为offset,这里我发了4000条消息,8个队列,各500条,均已消费完成
		"TopicTest@please_rename_unique_group_name_4":{0:500,1:500,2:500,3:500,4:500,5:500,6:500,7:500
		},
        // 某个group的重试队列消费情况
		"%RETRY%please_rename_unique_group_name_4@please_rename_unique_group_name_4":{0:0
		}
	}
}

参考

Apache RocketMQ官网 Architecture: rocketmq.apache.org/docs/rmq-ar…

《RocketMQ实战原理与解析》杨开元