RocketMQ之概要介绍
1. 物理部署结构
RocketMQ主要由4中集群构成:Name Server集群、Broker集群、Producer集群和Consumer集群。
Name Server集群:注册中心,Broker启动时要向Name Server注册,Name Server服务之间不通信,是一个几乎无状态节点。
Broker集群:Broker提供关于消息的管理、存储、分发等功能,是消息队列的核心。Broker分为Master和Slave,一个Master可以对应多个Slave,一个Slave只能属于一个Master。Master和Slave的对应关系通过指定相同的BrokerName,及不同BrokerId来定义。BrokerId为0表示Master,非0表示Slave。每个Broker与Name Server集群中的所有节点建立长连接,定时注册Topic信息到所有Name Server。
Producer集群:Producer与Name Server集群中的其中一个节点(随机选择)建立长连接。定期从Name Server中获取Topic路由信息,并向提供Topic服务的Broker(Master)建立长连接,定时向Broker发送心跳。Producer完全无状态。
Consumer集群:Consumer与Name Server集群中的其中一个节点(随机选择)建立长连接。定期从Name Server获取Topic路由信息,并且提供Topic的Broker(Master、Slave)建立长连接。
2. 逻辑部署结构
Producer Group
用来表示发送消息应用,一个Producer Group包含多个Producer实例。可以是多台机器,或一台机器多个进程。同一个进程内只允许一个Producer Group实例。一个Producer Group可以发送多个Topic消息,作用如下:
1)标识一类Producer
2)可以通过运维工具查询这个发送消息应用下有多个Producer实例。
3)发送分布式事务消息时,如果Producer意外宕机,Broker会主动回调Producer Group内的任意一台机器来确认事务状态。
Consumer Group
用来表示一个消费消息应用,一个Consumer Group下包含多个Consumer实例,可以是多台机器,或是多个进程。同一个进程内只允许一个Consumer Group实例。同一个Consumer Group的消费者必须要拥有相同的注册信息,即必须要听一样的Topic(并且Tag也一样)。消息消费模式可以分为广播和集群。集群模式下,各个消费者平均分配消息,某个消费者宕机,消息会分配到其他Consumer。
3. 消息发送
如图所示,TOPIC_A有5个队列,这五个队列可以是在一个broker上(broker-a queue0、broker-a queue1、broker-a queue2、broker-a queue3、broker-a queue4),也可以在五个broker上(broker-a queue0、broker-b queue0、broker-c queue0、broker-d queue0、broker-e queue0)。
消息的发送,默认是轮询发送的,每个队列接收平均的消息量。也可以根据业务执行发送到哪一个队列上。
4. 消息订阅
如图所示,如果有5个队列,2个consumer,那么第一个Consumer消费3个队列,第二个consumer消费2个队列,这样可以达到平均消费的目的。注意的是,consumer数量要小于等于队列数量。如果超过的话,多余的Consumer将不能消费消息。默认的分配策略是:AllocateMessageQueueAveragely。可以理解为平均分。
可以简单的看下,某个topic下的queue是如何分配到不同consumer中的。(代码有所删减)。首先会先获取这个Topic的所有队列(可能在不同broker下),然后获取所有消费者的cidAll
Set<MessageQueue> mqSet = this.topicSubscribeInfoTable.get(topic);
List<String> cidAll = this.mQClientFactory.findConsumerIdList(topic, consumerGroup);
...
if (mqSet != null && cidAll != null) {
List<MessageQueue> mqAll = new ArrayList<MessageQueue>();
mqAll.addAll(mqSet);
Collections.sort(mqAll);
Collections.sort(cidAll);
AllocateMessageQueueStrategy strategy = this.allocateMessageQueueStrategy;
List<MessageQueue> allocateResult = null;
try {
allocateResult = strategy.allocate(
this.consumerGroup,
this.mQClientFactory.getClientId(),
mqAll,
cidAll);
} catch (Throwable e) {
...
}
cid是在Consumer发送消息到nameserver时,会附带上去,当成consumer的一个标识。生成的代码如下,默认情况下(没有指定rocketmq.client.name配置,instanceName为DEFAULT),另外在Consumer启动的时候,会判断是否是集群模式,如果是的话,会把instanceName进行转换。如果instanceName为默认值的话,会把instanceName设置成pid。所以,默认cid=Ip@pid
//ClientConfig#buildMQClientId
public String buildMQClientId() {
StringBuilder sb = new StringBuilder();
sb.append(this.getClientIP());
sb.append("@");
sb.append(this.getInstanceName());
if (!UtilAll.isBlank(this.unitName)) {
sb.append("@");
sb.append(this.unitName);
}
return sb.toString();
}
//DefaultMQPushConsumerImpl#start
if (this.defaultMQPushConsumer.getMessageModel() == MessageModel.CLUSTERING) {
this.defaultMQPushConsumer.changeInstanceNameToPID();
}
//ClientConfig#changeInstanceNameToPID
public void changeInstanceNameToPID() {
if (this.instanceName.equals("DEFAULT")) {
this.instanceName = String.valueOf(UtilAll.getPid());
}
}
接着会对mqAll、cidAll进行排序,MessageQueue内部实现的排序为,先比较topic,再比较brokerName,最后比较queueId。cid的比较就是字母顺序了。(这很重要,也就是说能够保证,每次启动consumer分配的关系是一样的)。最后会调用分配策略AllocateMessageQueueAveragely,对当前Consumer进行分配。分配策略如下:
//AllocateMessageQueueAveragely#allocate
int index = cidAll.indexOf(currentCID);
int mod = mqAll.size() % cidAll.size();
int averageSize =
mqAll.size() <= cidAll.size() ? 1 : (mod > 0 && index < mod ? mqAll.size() / cidAll.size()
+ 1 : mqAll.size() / cidAll.size());
int startIndex = (mod > 0 && index < mod) ? index * averageSize : index * averageSize + mod;
int range = Math.min(averageSize, mqAll.size() - startIndex);
for (int i = 0; i < range; i++) {
result.add(mqAll.get((startIndex + i) % mqAll.size()));
}
AllocateMessageQueueAveragely的分配算法还是看起来还是比较困难的,通过一个例子来说明:8个队列进行分配,其中当前的consumer的cid为10.83.2.15@13674。排序后为最后一个cid,所以最终分配到的队列也就是为2个(第7、第八个)。3个consumer,8个队列的分配为:3、3、2。
List<MessageQueue> mqAll = new ArrayList<MessageQueue>();
mqAll.add(new MessageQueue("topicA","broker-a",0));
mqAll.add(new MessageQueue("topicA","broker-a",1));
mqAll.add(new MessageQueue("topicA","broker-a",2));
mqAll.add(new MessageQueue("topicA","broker-a",3));
mqAll.add(new MessageQueue("topicA","broker-b",0));
mqAll.add(new MessageQueue("topicA","broker-b",1));
mqAll.add(new MessageQueue("topicA","broker-b",2));
mqAll.add(new MessageQueue("topicA","broker-b",3));
List<String> cidAll = new ArrayList<String>();
cidAll.add("10.83.2.15@13674");
cidAll.add("10.200.110.172@12864");
cidAll.add("10.200.110.184@20131");
Collections.sort(mqAll);
Collections.sort(cidAll);
String currentCid = "10.83.2.15@13674";
AllocateMessageQueueAveragely allocator = new AllocateMessageQueueAveragely();
List<MessageQueue> allocateQueue = allocator.allocate("testAllocate",currentCid , mqAll, cidAll);
//[topic=topicA, brokerName=broker-b, queueId=2], [topic=topicA, brokerName=broker-b, queueId=3]
System.out.println(allocateQueue);
注:cid的生成可能在Doker下会有问题(多个consumer生成一样的cid)。详见:xie.infoq.cn/article/6ae…
5. Topic、Queue、Broker的关系
假设2个Master的broker集群(broker-a、broker-b),期望某个Topic下有8个队列。那么队列、Topic、Broker关系怎么分配呢?最简单的是,broker-a下有4个队列,broker-b下有4个队列平均分。当然也可以是broker-a有7个队列、broker-b下有1个队列。不论怎么样,每个broker上的队列都是从0开始计数的。也就是说期望集群下有8个队列,假设每个broker4个队列,并不是broker-a queue0-3、broker-b 4-7。而是broker-a queue0-3、broker-b queue0-3。(我觉得这点很重要,当初就理解错了)在实际使用队列时,实际上会用到MessageQueue对象,内部成员变量是topic、brokerName、queueId。
这点可以从代码中看出,QueueData就是某个Topic的信息,qds就是不同broker下,同一个topic的信息。Collections.sort(qds);会对qds进行排序,QueueData内部的排序是根据brokerName来的。最后可以得出,遍历不同的broker(Master),然后生成他们的MessageQueue对象,每个broker内部队列从0开始。
//QueueData成员变量。
private String brokerName;
private int readQueueNums;
private int writeQueueNums;
private int perm;
private int topicSynFlag;
//MQClientInstance#topicRouteData2TopicPublishInfo
...
List<QueueData> qds = route.getQueueDatas();
Collections.sort(qds);
for (QueueData qd : qds) {
if (PermName.isWriteable(qd.getPerm())) {
BrokerData brokerData = null;
for (BrokerData bd : route.getBrokerDatas()) {
if (bd.getBrokerName().equals(qd.getBrokerName())) {
brokerData = bd;
break;
}
}
if (null == brokerData) {
continue;
}
if (!brokerData.getBrokerAddrs().containsKey(MixAll.MASTER_ID)) {
continue;
}
for (int i = 0; i < qd.getWriteQueueNums(); i++) {
MessageQueue mq = new MessageQueue(topic, qd.getBrokerName(), i);
info.getMessageQueueList().add(mq);
}
}
}
6. 消费组、消费者和队列关系
假设有一个主题MyTopic,为主题创建5个队列,分布到两个Broker中,消费模式为集群消费。
| Broker | 主题 | 队列 |
|---|---|---|
| broker-a | MyTopic | Q0(queue-0)、Q1(queue-1) |
| broker-b | MyTopic | Q2(queue-0)、Q3(queue-1)、Q4(queue-2) |
每一个消费组就是一份订阅,消费主题MyTopic下所有队列的全部消息。(队列的消息不是消费掉就没了,这里的消费只是读取了数据,并没有删除,消费完消息还在队列里)
多个消费组在消费同一个主题时,消费组之间互不影响。比如两个消费组:G0和G1。G0消费了哪些消息,G1是不知道的,也不需要知道。消费组内部,一个消费组可以包含多个消费者的实例,多个消费者如何与5个队列对应,在消息订阅已经提到了。队列占用只是针对消费组内部来说的,对于其他的消费组来说没有影响。
每个消费组内部维护自己的一份消费位置,每个队列对应一个消费位置。消费位置在服务端保存(保存在config/consumerOffset.json),并且消费位置和消费者是没有关系的。每个消费位置一般就一个整数,记录这个消费组中,这个队列消费到哪个位置了,这个位置之前的消息都成功消费了。最后把消费位置整理成下面的表格
| 主题 | 消费组 | Broker | 队列 | 消费位置 |
|---|---|---|---|---|
| MyTopic | G0 | broker-a | Q0 | 234 |
| MyTopic | G0 | broker-a | Q1 | 23 |
| MyTopic | G0 | broker-b | Q2 | 87 |
| MyTopic | G0 | broker-b | Q3 | 0 |
| MyTopic | G0 | broker-b | Q4 | 888 |
| MyTopic | G1 | broker-a | Q0 | 4478 |
| MyTopic | G1 | broker-a | Q1 | 9832 |
| MyTopic | G1 | broker-b | Q2 | 7655 |
| MyTopic | G1 | broker-b | Q3 | 2348 |
| MyTopic | G1 | broker-b | Q4 | 3387 |
7. 读写队列
读队列数量和写队列数量可以不一致:手动创建读写队列默认8个,自动创建读写队列默认4个。
在producer中从broker中拉取topic的路由信息时,会根据写队列的数量生成MessageQueue数量。
//MQClientInstance#topicRouteData2TopicPublishInfo
for (int i = 0; i < qd.getWriteQueueNums(); i++) {
MessageQueue mq = new MessageQueue(topic, qd.getBrokerName(), i);
info.getMessageQueueList().add(mq);
}
在consumer中拉取消息,能够从哪些队列中拉取,会根据读队列的数据生成MessageQueue数量
//MQClientInstance#topicRouteData2TopicSubscribeInfo
for (int i = 0; i < qd.getReadQueueNums(); i++) {
MessageQueue mq = new MessageQueue(topic, qd.getBrokerName(), i);
mqList.add(mq);
}
1)假设有8个写队列,4个读队列。producer能写入队列数量为8个,但是consumer却只能消费前面4个。只有把读队列重新设置成8后,consumer才可以继续消费后4个队列的历史消息。
2)假设有4个读队列,8个写队列。producer能写入队列数量为4个,但是consumer能消费8个队列,只是后4个队列没有消息。
8. 参考资料
-
《rocketmq开发指南》
-
极客时间 《消息队列高手课》答疑解惑一
-
RocketMQ整体分析www.cnblogs.com/mantu/p/610…
-
rokcetmq源码 4.5.1