RocketMQ的组织架构和基本概念,Dledger高可用集群架构原理


1. MQ产品介绍

        

1.1 什么是MQ?为什么要用MQ?

        MQ:MessageQueue,消息队列。 队列,是一种FIFO 先进先出的数据结构。消息由生产者发送到MQ进行排队,然后按原来的顺序交由消息的消费者进行处理。

MQ的作用主要有以下三个方面:

  1. 异步:多个生产者发送完消息就可以去做别的事情了,这些消息由mq统一管理。提高系统的响应速度、吞吐量。
  2. 流量削峰:以稳定的系统资源应对突发的流量冲击。
  3. 解耦:服务之间进行解耦,才可以减少服务之间的影响。提高系统整体的稳定性以及可扩展性。另外,解耦后可以实现数据分发。生产者发送一个消息后,可以由一个或者多个消费者进行消费,并且消费者的增加或者减少对生产者没有影响。

1.2 MQ的缺点

  1. 系统可用性降低
    系统引入的外部依赖增多,系统的稳定性就会变差。一旦MQ宕机,对业务会产生影响。这就需要考虑如何保证MQ的高可用。

  2. 系统复杂度提高
    引入MQ后系统的复杂度会大大提高。以前服务之间可以进行同步的服务调用,引入MQ后,会变为异步调用,数据的链路就会变得更复杂。并且还会带来其他一些问题。比如:如何保证消费不会丢失?不会被重复调用?怎么保证消息的顺序性等问题。

  3. 消息一致性问题
    A系统处理完业务,通过MQ发送消息给B、C系统进行后续的业务处理。如果B系统处理成功,C系统处理失败怎么办?这就需要考虑如何保证消息数据处理的一致性。

1.3 几大MQ产品特点比较
在这里插入图片描述


2. rocketMQ组织架构

        本文介绍的是RocketMQ:RocketMQ是阿里巴巴开源的一个消息中间件,在阿里内部历经了双十一等很多高并发场景的考验,能够处理亿万级别的消息。2016年开源后捐赠给Apache,现在是Apache的一个顶级项目。RocketMQ的设计之初就是结合了 Kafa吞吐量高和Rabbit功能全面 的优点,摒弃他们的缺点,几乎适用于全场景。唯一的缺点就是客户端只支持java。

RocketMQ一般都是集群使用,集群的组件结构如下所示:

在这里插入图片描述
上述架构由以下这几个组件组成:
        

①:NameServer

         NameServer是一个非常简单的Topic路由注册中心,其角色类似Dubbo中的zookeeper,支持Broker的动态注册与发现。主要包括两个功能:

  1. Broker管理:NameServer接受Broker集群的注册信息并且保存下来作为路由信息的基本数据。然后提供心跳检测机制,检查Broker是否还存活;
  2. 路由信息管理:给Producer和Consumer提供服务获取Broker列表。每个NameServer将保存关于Broker集群的整个路由信息和用于客户端查询的队列信息。然后Producer和Conumser通过NameServer就可以知道整个Broker集群的路由信息,从而进行消息的投递和消费。

其实路由中心这样的功能,在所有的MQ中都是需要的。

  1. kafka是用zookeeper和一个作为Controller的Broker一起来提供路由服务
  2. RabbitMQ是由每一个Broker来提供路由服务
  3. RocketMQ比较特别,它把这个路由中心单独抽取了出来,并独立部署。

        NameServer通常也是集群的方式部署,各实例间相互不进行信息通讯。Broker是向每一台NameServer注册自己的路由信息,所以每一个NameServer实例上面都保存一份完整的路由信息。NameServer集群部署的好处在于:当某个NameServer因某种原因下线了,只要有一台服务节点正常,整个路由服务就不会有影响。Broker仍然可以向其它NameServer同步其路由信息,Producer,Consumer仍然可以动态感知Broker的路由的信息。

源码中整个NameServer的核心就是一个NamesrvController对象

   // 创建NamesrvController
   NamesrvController controller = createNamesrvController(args);
   
   // 启动NamesrvController
   start(controller);

这个controller对象就跟java Web开发中的Controller功能类似,都是响应客户端请求的。在NamesrvController对象的创建和启动时主要做了以下事情:

  1. 创建NamesrvController时,加载NamesrvConfig和NettyServerConfig。
    NamesrvConfig是NameServer自己运行需要的配置信息。NettyServerConfig
    包含Netty服务端的配置参数,默认占用了9876端口。可以在配置文件中覆盖。
    在这里插入图片描述
  2. 启动NamesrvController时,开启一个RemotingServer服务用来响应网络请求,并开启一个定时任务通过routeInfoManager这个功能组件定时扫描不活动的Broker。下图的代码存在于controller.initialize();方法中
    在这里插入图片描述

整个NameServer的结构是这样的:
在这里插入图片描述

        

②:Broker

         Broker其实就是RocketMQ服务器,负责存储消息、转发消息。Broker在RocketMQ系统中负责接收从生产者发送来的消息并存储、同时为消费者的拉取请求作准备。Broker也存储消息相关的元数据,包括消费者组、消费进度偏移和主题和队列消息等。

Broker Server是RocketMQ真正的业务核心,包含了多个重要的子模块:

  1. Remoting Module:整个Broker的实体,负责处理来自clients端的请求。
  2. Client Manager:负责管理客户端(Producer/Consumer)和维护Consumer的Topic订阅信息
  3. Store Service:提供方便简单的API接口处理消息存储到物理硬盘和查询功能。
  4. HA Service:高可用服务,提供Master Broker 和 Slave Broker之间的数据同步功能。
  5. Index Service:根据特定的Message key对投递到Broker的消息进行索引服务,以提供消息的快速查询。

而Broker Server要保证高可用需要搭建主从集群架构。RocketMQ中有两种Broker架构模式:普通集群Dledger高可用集群,两种集群的区别见下文!

Broker 源码的重点也是是围绕一个BrokerController对象,先创建,然后再启动。

    public static void main(String[] args) {
    	//创建并启动Broker
        start(createBrokerController(args));
    }

        在使用createBrokerController方法创建Broker时,同样也会加载并启动BrokerConfig、NettyServerConfig、NettyClientConfig、MessageStoreConfig这几项配置,Broker启动时会启动一堆Broker的核心服务,挑几个重要的如下:

this.messageStore.start(); 消息存储组件,接受Produce发来的消息并保存

这个messageStore消息存储组件尤为重要,主要完成以下两个功能,点击查看源码中的具体实现!!

  1. 负责接受Produce发来的消息并保存到commitLog文件中
  2. 维护ComsumeQueue和IndexFile对commitLog的消息索引。具体实现是:启动一个后台线程reputMessageService每隔1毫秒就会去拉取CommitLog中最新更新的一批消息,然后分别转发到ComsumeQueue和IndexFile里去

this.remotingServer.start();
this.fastRemotingServer.start(); 启动两个Netty服务

this.brokerOuterAPI.start(); 启动客户端,往外发请求
BrokerController.this.registerBrokerAll; 向NameServer注册心跳。

this.brokerStatsManager.start();
this.brokerFastFailure.start();这也是一些负责具体业务的功能组件

        根据以上组件的功能,BrokerController.this.registerBrokerAll方法在broker启动时执行,此方法会向NameServer注册自己的服务信息,通过CoundownLaunch来保证向所有的nameServer都注册成功!同时也会启动一个线程池,以10秒延迟,默认30秒的间隔持续向NameServer发送心跳。NameServer启动时也会开启一个定时任务,扫描不活动的Broker。具体观察NamesrvController.initialize方法
在这里插入图片描述
Broker的架构图如下:
在这里插入图片描述

        

③:生产者(Producer)

         Producer是消息发布的角色,负责生产消息,一般由业务系统负责生产消息。一个消息生产者会把业务应用系统里产生的消息发送到broker服务器。生产者中,会把同一类Producer组成一个集合,叫做生产者组,这类Producer发送同一类消息且发送逻辑一致。

RocketMQ提供多种消息发送方式:

  1. 同步发送
  2. 异步发送
  3. 单向发送

同步和异步方式均需要Broker返回确认信息,单向发送只管发,不需要Broker返回确认信息。

RocketMQ也有多种Producer

  1. DefaultMQProducer: 普通消息发送者,这个只需要构建一个Netty客户端,往Broker发送消息就行了。注意,异步回调只是在Producer接收到Broker的响应后自行调整流程,不需要提供Netty服务。
  2. TransactionMQProducer: 事务消息发送者,这个需要构建一个Netty客户端,往Broker发送消息。同时也要构建Netty服务端,供Broker回查本地事务状态。

生产者(Producer)的源码主要关注start()和send()方法

producer.start();  // 启动生产者,启动了生产者的一大堆重要服务。

//消息
Message msg = new Message("TopicTest" ,  //Topic 
    "TagA" ,  //Tag 
    ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) // 消息体
);

SendResult sendResult = producer.send(msg); 	//生产者发消息

         Producer需要拉取Broker列表,与Broker建立连接,这些流程其实都是在发送消息时建立的。因为在启动时,还不知道要拉取哪个Topic的Broker列表呢。所以对于这个问题,我们关注的重点,不应该是start方法,而是send方法。Send方法中,首先需要获得Topic的路由信息。这会从本地缓存中获取,如果本地缓存中没有,就从NameServer中去申请。
在这里插入图片描述
         获取路由信息后,会选出一个MessageQueue去发送消息。(注意:一个Topic下默认有4个MessageQueue,分别落在不同的broker上 )这个选MessageQueue的方法就是一个索引自增然后取模的方式。通过index自增与MessageQueue的个数取模,结果总会落在某一个MessageQueue上
在这里插入图片描述

生产者负载均衡策略

        又因为index自增,取模结果也会在第1-第4个MessageQueue之间轮询自增,所以Producer发送消息时,默认会轮询目标Topic下的所有MessageQueue,并采用递增取模的方式往不同的MessageQueue上发送消息,以达到让消息平均落在不同的queue上的目的。

在这里插入图片描述
        produce在发送Netty请求时,实际上是指定的MessageQueue,而不是Topic。Topic只是用来找MessageQueue。然后根据MessageQueue再找所在的Broker,往Broker发送请求。
在这里插入图片描述

        如果生产者要发送顺序消息,可以指定一个MessageQueueSmeelector。通过这个对象来将消息发送到自己指定的MessageQueue上。底层通过MessageQueue加锁的方式保证消息局部有序。

        Producer把消息发到了Broker,接下来就关注下Broker接收到消息后是如何把消息进行存储的,点击查看Broker存储消息详情!!!!!!

        

④:消费者(Consumer)

         Consumer是消息消费的角色,负责消费消息,一般是后台系统负责异步消费。一个消息消费者会从Broker服务器拉取消息、并将其提供给应用程序。获取消息并消费的形式有两种:拉取式、推动式。

  1. 拉取式消费的应用通常主动调用Consumer的拉消息方法从Broker服务器拉消息、主动权由应用控制。每次拉过来的消息是顺序的,这部分的顺序消费需要业务自己处理,不展开讨论。
  2. 推动式消费模式下Broker收到数据后会主动推送给消费端,推动式消费模式一般实时性较高

         消费者同样会把同一类Consumer组成一个集合,叫做消费者组,这类Consumer通常消费同一类消息且消费逻辑一致。消费者组使得在消息消费方面,实现负载均衡和容错的目标变得非常容易。要注意的是,消费者组的消费者实例必须订阅完全相同的Topic。RocketMQ 支持两种消息模式:集群消费(Clustering)和广播消费(Broadcasting)。

  1. 集群消费模式下,相同Consumer Group的每个Consumer实例平均分摊消息。
  2. 广播消费模式下,相同Consumer Group的每个Consumer实例都接收全量的消息。

消费者在consumer.start()启动时,会启动几个服务,其中最主要的就是拉取消息服务负载均衡服务
在这里插入图片描述
下面进入两个服务的run方法中,一探究竟

4.1 消费者负载均衡策略

负载均衡服务内部为了广播模式和集群模式制定了不同的逻辑。
广播模式下,由于Consumer要接受所有的消息,所以不存在负载均衡
在这里插入图片描述

集群模式下,由于Consumer要均摊消息,这就涉及到消费端的负载均衡了。分配消息时,都会将MessageQueue和消费者ID进行排序后,再用不同的分配算法进行分配。
在这里插入图片描述
消费端内置的分配策略有六种,分别对应AllocateMessageQueueStrategy类下的六种实现类,可以在consumer中直接通过set来指定,六种策略如下:

  1. AllocateMachineRoomNearby: 将同机房的Consumer和Broker优先分配在一起。
  2. AllocateMessageQueueAveragely:平均分配。将所有MessageQueue平均分给每一个消费者,默认的分配策略
  3. AllocateMessageQueueAveragelyByCircle: 轮询分配。轮流的给一个消费者分配一个MessageQueue。
  4. AllocateMessageQueueByConfig: 不分配,直接指定一个messageQueue列表。类似于广播模式,直接指定所有队列。
  5. AllocateMessageQueueByMachineRoom:按逻辑机房的概念进行分配。又是对BrokerName和ConsumerIdc有定制化的配置。
  6. AllocateMessageQueueConsistentHash。源码中有测试代码AllocateMessageQueueConsitentHashTest。这个一致性哈希策略只需要指定一个虚拟节点数,是用的一个哈希环的算法,虚拟节点是为了让Hash数据在换上分布更为均匀。
    在这里插入图片描述

默认情况下使用的是最简单的平均分配策略,平均分配时的分配情况是这样的:

在这里插入图片描述
        

4.2 消费者拉取、消费消息

        拉取消息逻辑在pullMessageService.start()内部的run方法中,拉取消息时会进行流控、更新时间戳等操作,然后消费消息,消费时对普通消息和顺序消息有不同的消费逻辑
在这里插入图片描述
消费普通消息时:默认一次拉取32条,不足32条的直接拉完
在这里插入图片描述
消费顺序消息时:需要保证同一个队列下的消息只能串行消费,为了防止并发消费,就需要用到锁
在这里插入图片描述

        

⑤:主题(Topic)

         表示一类消息的集合,每个主题包含若干条消息,每条消息只能属于一个主题,是RocketMQ进行消息订阅的基本单位。同一个Topic下的数据,会分片保存到不同的Broker上,而每一个分片单位,就叫做MessageQueue。MessageQueue是生产者发送消息与消费者消费消息的最小单位。也就是说生产者和消费者从MessageQueue上获取消息。

注意:每一个项目推荐使用一个Topic,每一个Topic下的不同消息类型使用Tag来区分,Tag不足以区分时,再为消息加上Key属性进行区分!。

        

⑥:消息(Message)

         消息系统所传输信息的物理载体,生产和消费数据的最小单位,每条消息必须属于一个主题Topic。RocketMQ中每个消息拥有唯一的Message ID,且可以携带具有业务标识的Key。系统提供了通过Message ID和Key查询消息的功能。

         并且Message上有一个为消息设置的标志:Tag标签。用于同一主题下区分不同类型的消息。来自同一业务单元的消息,可以根据不同业务目的在同一主题下设置不同标签。标签能够有效地保持代码的清晰度和连贯性,并优化RocketMQ提供的查询系统。消费者可以根据Tag实现对不同子主题的不同消费逻辑,实现更好的扩展性。

        

3. 各组件的联系

RocketMQ主要由 Producer、Broker、Consumer 三部分组成,各组件关系如下

  1. 其中Producer 负责生产消息,Consumer 负责消费消息,Broker 负责存储消息。
  2. Broker 在实际部署过程中对应一台服务器,每个 Broker 可以存储多个Topic的消息,每个Topic的消息也可以分片存储于不同的 Broker。
  3. Message Queue 用于存储消息的物理地址,每个Topic中的消息地址存储于多个 Message Queue 中。


4. RcoketMQ普通集群与Dledger高可用集群

普通集群:

         这种集群模式下会给每个节点分配一个固定的角色,master负责响应客户端的请求,并存储消息。slave则只负责对master的消息进行同步保存,并响应部分客户端的读请求。消息同步方式分为同步同步和异步同步。这种集群模式下各个节点的角色无法进行切换,也就是说,master节点挂了,这一组Broker就不可用了。(点击查看RocketMQ单机和普通集群搭建教程
在这里插入图片描述

Dledger高可用集群:

         Dledger是RocketMQ自4.5版本引入的实现高可用集群的一项技术。这个模式下的集群会随机选出一个节点作为master,而当master节点挂了后,会从slave中自动选出一个节点升级成为master。Dledger技术做的事情:

  1. 接管Broker的CommitLog消息存储,普通集群是由broker自己去写CommitLog消息存储,现在是由Dledger去写CommitLog消息存盘
  2. 从集群中选举出master节点,Dledger最主要的功能
  3. 完成master节点往slave节点的消息同步

Dledger是怎样进行leader选举的呢?

         在探究这个问题之前,我们先要明确一件事情,Dledger使RocketMQ集群的每个节点都有三个状态

  1. Leader 主节点:主要用于接收客户端请求
  2. Follower 从节点 :主要用于Leader节点的数据备份,当有客户端请求到Follower时,Follower节点会把请求转到Leader节点
  3. Candidate 候选者节点:只有Candidate状态的节点才会参与主节点的选举。

         正常的集群架构下,应该是一个Leader主节点,多个Follower从节点。主从节点之间通过心跳机制确认状态。当需要进行选举时,都会切换成Candidate 候选者节点参与选举!Dledger是使用Raft算法来进行节点选举的,在Raft协议中,会将时间分为一些任意时间长度的时间片段,叫做term。term会使用一个全局唯一,连续递增的编号作为标识,也就是起到了一个逻辑时钟的作用。
在这里插入图片描述

         在每一个term时间片里,都会进行新的选举,每一个Candidate都会努力争取成为leader。获得票数最多的节点就会被选举为Leader。也就是说,在这个集群中, leader节点是随着term时间片不断变化的。

         选举开始后,每个follower会增加自己当前的term,并将自己转为candidate。然后向其他节点发起投票请求,请求时会带上自己的编号和termID,根据termID的大小来确定是否成为Leader(termID最大的成为leader),默认会投自己一票。之后candidate状态可能会发生以下三种变化:

  1. 赢得选举,成为leader: 如果它在一个term内收到了大多数的选票,将会在接下的剩余term时间内称为leader,然后就可以通过发送心跳确立自己的地位。(每一个server在一个term内只能投一张选票,并且按照先到先得的原则投出)
  2. 其他节点成为leader: 在等待投票时,可能会收到其他server发出心跳信号,说明其他leader已经产生了。这时通过比较自己的term编号和RPC过来的term编号,如果比对方大,说明leader的term过期了,就会拒绝该RPC,并继续保持候选人身份;
    如果对方编号不比自己小,则承认对方的地位,转为follower。
  3. 选票被瓜分,选举失败: 如果没有candidate获取大多数选票,,比如各节点的termID都一样,无法遵循半数选举机制则,没有leader产生。 candidate们等待超时后发起另一轮选举. 为了防止下一次选票还被瓜分,必须采取一些额外的措施, raft采用随机election timeout(随机休眠时间)的机制防止选票被持续瓜分。通过将timeout随机设为一段区间上的某个值, 因此很大概率会有某个candidate率先超时然后赢得大部分选票。

在这里插入图片描述

所以以三个节点的集群为例,选举过程会是这样的:

  1. 集群启动时,三个节点都是follower,发起投票后,三个节点都会给自己投票。这样一轮投票下来,三个节点的term都是1,是一样的,这样是选举不出Leader的。
  2. 当一轮投票选举不出Leader后,三个节点会进入随机休眠,例如A休眠1秒,B休眠3秒,C休眠2秒。
  3. 一秒后,A节点醒来,会把自己的term加一票,投为2。然后2秒时,C节点醒来,发现A的term已经是2,比自己的1大,就会承认A是Leader,把自己的term也更新为2。实际上这个时候,A已经获得了集群中的多数票,2票,A就会被选举成Leader。这样,一般经过很短的几轮选举,就会选举出一个Leader来。
  4. 到3秒时,B节点会醒来,他也同样会承认A的term最大,他是Leader,自己的term也会更新为2。这样集群中的所有Candidate就都确定成了leader和follower.
  5. 然后在一个任期内,A会不断发心跳给另外两个节点。当A挂了后,另外的节点没有收到A的心跳,就会都转化成Candidate状态,重新发起选举。
  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值