1

RocketMQ架构图

image.png

主要组件如下:
NameServer
NameServer集群,Topic的路由注册中心,为客户端根据Topic提供路由服务,从而为客户端获取对应的Broker进而向Broker发送消息。NamerServer将Topic信息保存在内存里。NameServer之间的节点不通信,NameServer集群里的路由信息采用的是最终一致性。NameServer对于RokcetMQ好比ZK对于Kafka。
Broker
消息存储服务器,分Master与Slave,Master承担读写操作,Slave作为一个备份,但也有可能承担读操作。每30s Master与Slave会向NameServer发送心跳包,心跳包里有Broker上所有的Topic路由信息。Broker会将Topic信息持久化。
Client
消息客户端,包括Producer与Consumer。一般情况下同一时间一个客户端只会连接一台NameServer,只有在异常的时候才会尝试连接另外一台。客户端每30s向NamerServer发起Topic的路由信息查询

消息订阅模型

RocketMQ消息消费模式采用的是发布与订阅模式。

  • Topic: 一类消息的集合,不同类型的消息归属于不同的主题
  • ConsumerGroup:消息消费组,一个消费单位的集合,消费组启动时需要订阅需要消费的Topic。一个Topic可以被多个消费组订阅,同样一个消费组也可以订阅多个主题。一个消费组有多个消费者。

消费模式
RocketMQ支持广播模式与集群模式。

  • 广播模式:一个消费组内的所有消费者都会处理Topic中的每一条消息,通常用于刷新内存缓存
  • 集群模式:一个消费组内的所有消费者共同消费一个Topic中的消息,即分工协作,一个消费者消费一部分数据,启动负载均衡。因为消费者间可以分工协作,就可以进行横向扩容,当前消费者如果无法及时处理消息时,可以通过增加消费者个数,提高消费能力,及时处理积压的消息。

消费队列负载算法与重平衡机制
默认情况下,同一个消费者同时可以分配多个队列,但一个队列同一时间只会分配给一个消费者。RocketMQ提供的常用分配算法有:

  • 平均连续分配:队列总数除以消费者个数,余数按消费者顺序分配给消费者。这个是默认分配算法
  • 轮流平均分配:轮流一个一个分配
  • 手动配置分配:直接使用配置进行分配
  • 一致性HASH分配:使用一致性HASH算法进行分配(没多大作用)
  • 机房分配:机房内优先就近分配
  • 指定机房中的队列:先指定机房,再使用平均连续分配选择机房里的队列
    这里说明下平均分配算法:
public class AllocateMessageQueueAveragely implements AllocateMessageQueueStrategy {

    @Override // mqAll : MessageQueue列表;cidAll:消费者列表
    public List<MessageQueue> allocate(String consumerGroup, String currentCID, List<MessageQueue> mqAll,
        List<String> cidAll) {
        ................
        //基本原则,每个队列只能被一个consumer消费
        int index = cidAll.indexOf(currentCID); // 当前消费者在cidAll中位置
        int mod = mqAll.size() % cidAll.size(); // 5%100 == 5 当messageQueue个数小于等于consume的时候,排在前面(在list中的顺序)的consumer消费一个queue,index大于messageQueue之后的consumer消费不到queue,也就是为0
        int averageSize = // 如果MessageQueue.size() < = customer的数量,则=1
            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()));
        }
        return result;
    }

}
# 场景1
假设 MessageQueue有q1,q2,q3,q4,q4,q6,q7,q8  cidAll有 c1,c2,c3

c1逻辑
index=0 mod=2  averageSize=(mod > 0 && index < mod ? mqAll.size() / cidAll.size() + 1) = (8/3+1)=3
range=Math.min(averageSize, mqAll.size() - startIndex)=min(3,8)=3   startIndex=index * averageSize=0×3=0   范围就是[0,3)

c2逻辑
index=1 mod=2  averageSize=(mod > 0 && index < mod ? mqAll.size() / cidAll.size() + 1) = (8/3+1)=3
 range=Math.min(averageSize, mqAll.size() - startIndex)=min(3,8)=3   startIndex=index * averageSize=1×3=3   范围就是[3,6)
 
c3逻辑
index=2 mod=2  averageSize=(mod > 0 && index < mod ? mqAll.size() / cidAll.size() ) = (8/3)=2  注意这里averageSize是没+1的,是因为index = mod,走这个逻辑mqAll.size() / cidAll.size()
range=Math.min(averageSize, mqAll.size() - startIndex)=min(3,8)=2   startIndex=index * averageSize + mod = 2×2+2 =6   范围就是[6,8)

最终结果:  c1:[q1,q2,q3]  c2:[q4,q5,q6]  c3: [q7,q8]

# 场景2
假设MessageQueue有q1,q2,q3   cidAll有 c1,c2,c3,c4,c5
因为 mqAll.size() <= cidAll.size() 所以averageSize=1,每个消费者最多1个MessageQueue
c1逻辑  index=0  mod=3  averageSize=1  range=1 startIndex=index * averageSize=0×1=0    所以c1:[q1]
c2逻辑  index=1  mod=3  averageSize=1  range=1 startIndex=index * averageSize=1×1=1    所以c2:[q2]
c3逻辑  index=2  mod=3  averageSize=1  range=1 startIndex=index * averageSize=2×1=1    所以c3:[q3]

最终结果:  c1:[q1]  c2:[q2]   c3:[q3]

先算出生个消费者可以分配多少队列,然后再算出分配队列开始的序号。
这里需要说明下,如果Topic的队列个数小于消费者的个数,那有的消费者就无法分配到消息队列。也就是说Topic的队列数直接决定了最大消费者的个数,增加Topic队列个数对RocketMQ的性能不会产生影响,如果想提升消费性能可以增加消费者个数,这里就涉及到消费队列重平衡机制:在RocketMQ客户端中会每隔20S去查询当前Topic的所有队列与消费者个数,接着运用队列负载算法进行重新分配,然后与上一次分配结果进行对比,有变化则重新分配队列;没有则忽略。

消费进度
消费者消费一条消息后需要记录消费的位置,这样在消费端重启的时候,可以继续从上一次消费的位点开始进行处理新的消息。RocketMQ中,消息消费位点的存储是以消费组为单位的。

  • 集群模式下,消息消费进度存储在Broker端,每一个队列一个偏移量
  • 广播模式下,消费进度存储在用户本地的主目录

消费模型
RocketMQ提供了并发消费与顺序消费两种消费模型。

  • 并发消费:对一个队列中的消息,每一个消费者内部都会创建一个线程池,对队列中的消息多线程处理,所以偏移量大的消息比偏移量小的消息有可能先消费
  • 顺序消费:尽管一个消费组中的消费者会创建一个线程池,但针对同一个队列会加锁以保证有序性

需要注意下,消费失败在不同的模型下有不同表现:在并发模型下,消费失败会重试16次,每次间隔时间不一样(详情说明:RocketMq消费失败处理逻辑);在顺序消费模型下,一条消息消费失败会一直重试(准确来说是Integer.MAX_VALUE次)直到消费成功(详情说明:rocketmq 如何保证顺序消费),如果一直失败会导致消息积压,这一点一定要注意。

定时消息
定时消息是将消息发送到Broker,但消费端不会立即消费,而是到了指定延迟时间后才能消费。开源版本的RocketMQ不支持任意精度的定时消息。目前支持的延迟级别有:

1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h

消息过滤
消息过滤是指消费端可以根据条件对一个Topic中的消息进行过滤,只消费一个主题下满足条件的消息。RocketMQ目前主要的过滤机制是基于Tag与消息属性,其中基于消息属性的过滤支持SQL92表达式(sql92和sql99)。

部署

NameServer部署
一般是推荐部署三台做到高可用,NameServer之间不会进行通信,每台NameServer上都会有完整的路由信息,只要有一台可用就可以保证RocketMQ的正常运行。

Broker部署
Broker支持的集群部署方案有下面四种:

  • 多master模式。 一个集群无slaver全是master。部署简单,但其中一个master宕机后,此机器上的信息无法消费。
  • 多master多slaver-异步复制模式。 每个master都配置一个slaver,master与slaver间采用异步复制的方式,会有短暂的不同步情况。如果master宕机可从slaver上进行消费,但如果slaver没来得及复制则会有部分消息无法消费。
  • 多master多slaver-同步双写模式。 每个master都配置一个slaver,master与slaver间采用同步双写,只有都写成功了才返回响应。这种模式的性能要比异步复制模式低10%左右,且master宕机后无法将slaver自动切换为master,而是需要人工进行切换,这也正是dledger模式出现的原因。
  • Dledger模式。 每个master都配置两个slaver组成Dleger group,由dledger实现选举。一般由三台borker组成group,master宕机后,采用raft算法在剩下两台slaver里选举出master提供服务。

一般来说,把Master和Slave配置成同步刷盘方式,主从之间配置成异步的复制方式,这样来保证消息存储的高可用(也是集群+副本来保证的高可用)。

另外,读写分离也简单提一下。

RocketMQ的Consumer在拉取消息时,Broker会判断Master服务器的消息堆积量来决定Consumer是否从Slave服务器拉取消息消费。默认一开始从Master服务器拉群消息,如果Master服务器的消息堆积超过物理内存40%,则会返回给Consumer的消息结果并告知Consumer,下次从其他Slave服务器上拉取消息。

小结

这篇文章只是简单提了下RocketMQ里常用的一些组件与概念,后续再深入学习。

参考文章:
RocketMQ 实战与进阶

AllocateMessageQueueStrategy算法

4 种高可用 RocketMQ 集群搭建方案!

RocketMQ高可用设计之主从复制和读写分离


步履不停
38 声望13 粉丝

好走的都是下坡路