前面通过对 rabbitmq 的分析,我们总结一张结构图放在这里(图引自),作为和 rocketmq 的对比。
图中的队列是 quorum 队列。每个 broker 上都会有不同的队列存在。因为 quorum 队列采用了raft 协议,所以队列分为领导者队列和追随者队列,基于 raft 协议来保障领导者队列和追随者队列之间的数据同步和自动选主。
但是 rabbitmq 虽然受众广泛,并且拥有许多种语言的客户端实现。但是因其 erlang 语言生态不是特别的热门,而一般大厂中真正落地都需要进行深度定制,所以本篇我们基于前面的 rabbitmq 的知识,来对比学习下由阿里巴巴出品的 rocketmq
,官方文档链接在此。
首先我们关注单个 Broker 内部的结构,如下图,引自
同 rabbitmq 一样,rocketmq 中的 topic 可以对应 rabbitmq 的 exchange,属于逻辑概念,是关联队列的规则。可以简单认为队列仍然是存储消息的地方。不过 rocketmq 有所不同,在逻辑上我们可以认为:
- topicA
- queue0
- queue1
- topicB
- queue5
但是实际每个 Broker 上,存储的地方只有一个,那就是图中的 CommitLog
,Commitlog 可以对比 mysql 中的binlog。同样是顺序写入,而且会产生多个 commitlog 文件(因为每个 commitlog 文件默认是1G大小),文件中的内容就是一条条消息内容。
这样就相当于多个topic的消息都混杂写入到了 commitlog 文件中。那么该如何区分呢?
答案就对应图中的 ConsumerQueue
,其保存了属于某个 topic 下的消息。但是仅仅保存某条消息在 commitlog 中的偏移量,以及消费者对本队列中的消息消费的偏移量等等。所以 consumerQueue 相当于 commitlog 的索引,真正存储消息内容的还是 commitlog。
到这里,我们就得出一个重要的结论:Broker 上存储消息内容的地方是 commitlog,所以集群环境下的可靠性也就是要同步 commitlog。这个结论是理解后续集群架构的重要依据。
同样的,消息可靠性先来关注单机 Broker。根据 rocketmq 文档可知,和 rabbitmq 一样,可以配置同步刷盘还是异步刷盘,异步刷盘是刷新到操作系统的 Page cache 中,实际是没有真正落入物理磁盘的。
如果是同步刷盘,则生产者发布一条消息,Broker 将其写入到 commitlog 文件后,消息就不会丢失。异步刷盘则可能存在落盘前断电丢失的危险,但是同步刷盘降低性能,需要结合实际场景进行选择。
rocketmq 的集群架构如下:(图片来源)
前文我们提到过 raft 集群中,读写只能从领导者节点读取,那么如何突破单领导者性能瓶颈呢?rocketmq 的架构设计告诉了我们答案。通过引入 NameServer
(类似 zookeeper),将 Broker 扩展成多个小集群。巧合的是,rocketmq 在 4.5版本之后,同样是引入了 raft 算法来实现Broker集群的。
所以图中 Broker Cluster 下的 Broker Master 1和2,实际上就是两个 raft 集群的 Leader。raft 引入的目的主要是:
- 在集群内同步 commitlog 内容
- 自动选主自动切换
集群内的交互方式我们已经熟悉了,就是 raft 那一套。那么集群外, producer、consumer、namerServer、和 broker cluster 是怎么配合的呢?
当 Broker 启动时,会主动向所有的 nameServer 注册(注册 Broker 的IP信息,Broker上的topic信息等),这样 nameServer就知道了 Broker-topic-queue 的联系了。
注意,这里之所以要向所有的nameServer都注册,是可以让每个 nameServer 上都存储全部的注册信息,从而避免 nameServer 之间的数据同步。又由于注册这个动作并不是经常性的,所以也不会造成性能上的影响。
那么当生产者准备向一个 topic 写入消息时,首先要知道和哪个 Broker 通信。所以生产者会先从 NamerServer 上获取到该 topic 在哪些 Broker 上有存储。又由于 Broker 内部需要建立 commitlog 和 consumerQueue 的联系,所以生产者会轮询选择该 topic 下的一个队列进行发送,发送时已知该队列处于哪个 Broker 上。
消费者同样是去 namerServer 上获取 Broker-topic-queue 之间的联系,从而去连接到对应的 Broker 上,进行队列的消息消费。
队列模型与发布-订阅模型
理解了单机和集群的架构之后,我们再来对比一下 rabbitmq 和 rocketmq 关于两种消息模型的实现,即:队列模型和发布-订阅模型。并以此来理解 rocketmq (或者说是 kafka)中的关键概念:消费者组和消费位置。
队列模型:
生产者发送消息到(某个)队列中,消费者们从队列中消费消息。但是消费者之间是竞争的关系,即对于消息M1,只能由一个消费者消费掉。这就是典型的队列模型。
而发布-订阅模型中,生产者往一个主题发布消息,任何订阅该主题的消费者 都能收到 该消息内容。这就是发布订阅模型。那么 rabbitmq 实现发布-订阅模型就是提出了 exchange
的概念,通过不同类型的 exchange,将消息发送到 不同的队列。但是这就相当于一条消息被物理地复制了多次。
为了实现 发布-订阅模型,并且避免一条消息被复制多次,rocketmq 引入了 消费者组
的概念。
首先是消息分发机制,和之前一样,一个 topic 下的消息被分发到不同的 queue(consumerQueue)中。那么针对同一个 queue ,可以有多个属于不同消费者组的消费者来消费,并且相互不影响。也就是说同一消费者组内的消费者仍然是竞争关系,不同消费者组之间的消费者都可以独立消费该队列的全部消息数据。
这样,每个消费者组就能收到发往该主题的全量消息,而消费者组内的消费者,只能收到该主题的部分消息。消费者组内部成员就可以起到对 topic 消息进行负载均衡的作用。而消费者组之间互不影响的特性,也可以带来灵活性。
所以,我们同样来看一下 rocketmq 是如何实现队列模型的,如下图:
实际是利用同一消费者组,内部的消费者只能收到部分消息(即某一个队列的消息)特性来实现。
而实现发布-订阅模型,则是一个队列可能同时被多个消费者组消费,而消费者组之间的消费进度互不影响。如下图:
重平衡(rebalance)
由于引入了消费者组的概念,而组内成员之间消费是互斥的。所以当组内成员变更时,就需要重新为各个成员分配要消费的队列,以此达到一种负载均衡的效果。这种机制,就是大名鼎鼎的重平衡机制。
不过 rocketmq 中的实现相对来说简单一些。重平衡可以拆分为三大步:
- 判断并发起
- 计算平衡结果
- 执行
首先是计算平衡结果的算法,可以简单认为就是轮流分配机制。比如一个topic下有10个队列,该topic的消费者组中有3个消费者,那么经过平衡算法的计算,最终会是 3,3,4 的队列分配结果。
然后重平衡的发起方是消费者,因为消费者每隔一段时间会判断是否需要开始重平衡。判断的机制就是消费者定时获取到当前 topic 对应的队列信息和消费者信息,并经过平衡算法,将新分配结果和之前的分配结果进行比较。比如该消费者当前消费的队列是 c1,c2,然而重平衡后是 c1。则会进入执行阶段。
执行阶段就是只拉取最新分配的队列信息。
这样每个消费者都是决策者的思路,简化了重平衡的实现难度。
rocketmq 在实际应用中,由于是阿里出品,所以可以阿里系的产品对接会更方便。比如 rocketmq + canal等。此外,rocketmq 的 tag 特性(对 topic 的进一步拆分),也让 rocketmq 在电商等领域应用广泛。
那么,下一篇,我们会对比学习 rocketmq 和 kafka。