java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > RocketMQ原理

深入讲解RocketMQ原理

作者:爱吃牛肉的大老虎

这篇文章主要介绍了详解SpringBoot整合RocketMQ,RocketMQ作为一款纯java、分布式、队列模型的开源消息中间件,支持事务消息、顺序消息、批量消息、定时消息、消息回溯等,需要的朋友可以参考下

RocketMQ

1.1 为什么要选RocketMQ

在这里插入图片描述

总结一下: 选择中间件的可以从这些维度来考虑:可靠性,性能,功能,可运维行,可拓展性,社区活跃度。目前常用的几个中间件,ActiveMQ作为“老古董”,市面上用的已经不多,其它几种:

1.2 RocketMQ优缺点

RocketMQ优点:

RocketMQ缺点:

1.3 消息模型

1.3.1 消息队列模型

消息队列有两种模型:队列模型发布/订阅模型

1.队列模型

这是最初的一种消息队列模型,对应着消息队列发-存-收的模型。生产者往某个队列里面发送消息,一个队列可以存储多个生产者的消息,一个队列也可以有多个消费者,但是消费者之间是竞争关系,也就是说每条消息只能被一个消费者消费。

在这里插入图片描述

2.发布/订阅模型

如果需要将一份消息数据分发给多个消费者,并且每个消费者都要求收到全量的消息。很显然,队列模型无法满足这个需求。解决的方式就是发布/订阅模型。 在发布 - 订阅模型中,消息的发送方称为发布者(Publisher),消息的接收方称为订阅者(Subscriber),服务端存放消息的容器称为主题(Topic)。发布者将消息发送到主题中,订阅者在接收消息之前需要先订阅主题。“订阅”在这里既是一个动作,同时还可以认为是主题在消费时的一个逻辑副本,每份订阅中,订阅者都可以接收到主题的所有消息。

在这里插入图片描述

它和 队列模式的异同:生产者就是发布者,队列就是主题,消费者就是订阅者,无本质区别。唯一的不同点在于:一份消息数据是否可以被多次消费

1.3.2 RocketMQ消息模型

RocketMQ使用的消息模型是标准的发布-订阅模型,在RocketMQ的术语表中,生产者、消费者和主题,与发布-订阅模型中的概念是完全一样的。

1.3.3 RocketMQ中成员

RocketMQ本身的消息是由下面几部分组成:

在这里插入图片描述

Message

Message(消息)就是要传输的信息 一条消息必须有一个主题(Topic),主题可以看做是你的信件要邮寄的地址。 一条消息也可以拥有一个可选的标签(Tag)和额处的键值对,它们可以用于设置一个业务Key 并在 Broker 上查找此消息以便在开发期间查找问题。

Topic

Topic(主题)可以看做消息的归类,它是消息的第一级类型。比如一个电商系统可以分为:交易消息物流消息等,一条消息必须有一个 Topic

Topic 与生产者和消费者的关系非常松散,一个 Topic 可以有0个1个多个生产者向其发送消息,一个生产者也可以同时向不同的 Topic 发送消息。 一个 Topic 也可以被 0个1个多个消费者订阅。

Tag

Tag(标签)可以看作子主题,它是消息的第二级类型,用于为用户提供额外的灵活性。使用标签,同一业务模块不同目的的消息就可以用相同 Topic 而不同的 Tag 来标识。比如交易消息又可以分为:交易创建消息、交易完成消息等,一条消息可以没有 Tag

标签有助于保持你的代码干净和连贯,并且还可以为 RocketMQ 提供的查询系统提供帮助。

Group

Message Queue

Message Queue(消息队列),一个 Topic 下可以设置多个消息队列,Topic 包括多个 Message Queue ,如果一个 Consumer 需要获取 Topic下所有的消息,就要遍历所有的 Message Queue

RocketMQ还有一些其它的Queue——例如ConsumerQueue

Offset

Topic的消费过程中,由于消息需要被不同的组进行多次消费,所以消费完的消息并不会立即被删除,这就需要RocketMQ为每个消费组在每个队列上维护一个消费位置(Consumer Offset),这个位置之前的消息都被消费过,之后的消息都没有被消费过,每成功消费一条消息,消费位置就加一

也可以这么说,Queue 是一个长度无限的数组,Offset 就是下标。

总结图示

RocketMQ的消息模型中,这些就是比较关键的概念了 画张图总结一下

在这里插入图片描述

1.4 消息的消费模式

消息消费模式有两种:Clustering(集群消费)和Broadcasting(广播消费)

在这里插入图片描述

默认情况下就是集群消费,这种模式下一个消费者组共同消费一个主题的多个队列,一个队列只会被一个消费者消费,如果某个消费者挂掉,分组内其它消费者会接替挂掉的消费者继续消费。 而广播消费消息会发给消费者组中的每一个消费者进行消费。

1.5 RoctetMQ基本架构

先看图,RocketMQ的基本架构

在这里插入图片描述

RocketMQ 一共有四个部分组成:NameServerBrokerProducer 生产者Consumer 消费者,它们对应了:发现,为了保证高可用,一般每一部分都是集群部署的

类比一下我们生活的邮政系统—— 邮政系统要正常运行,离不开下面这四个角色, 一是发信者,二 是收信者, 三是负责暂存传输的邮局, 四是负责协调各个地方邮局的管理机构。对应到 RocketMQ 中,这四个角色就是 ProducerConsumerBrokerNameServer

在这里插入图片描述

1.5.1 NameServer

NameServer 是一个无状态的服务器,角色类似于 Kafka使用的 Zookeeper,但比 Zookeeper 更轻量。

特点:

每个 NameServer 结点之间是相互独立,彼此没有任何信息交互。 Nameserver 被设计成几乎是无状态的,通过部署多个结点来标识自己是一个伪集群,Producer 在发送消息前从 NameServer中获取 Topic 的路由信息也就是发往哪个 BrokerConsumer 也会定时从 NameServer 获取 Topic 的路由信息,Broker 在启动时会向 NameServer 注册,并定时进行心跳连接,且定时同步维护的 TopicNameServer 功能主要有两个:

1.5.2 Broker

消息存储和中转角色,负责存储和转发消息

Broker 内部维护着一个个 Consumer Queue,用来存储消息的索引,真正存储消息的地方是 CommitLog(日志文件)

单个 Broker 与所有的 Nameserver 保持着长连接和心跳,并会定时将 Topic 信息同步到 NameServer,和 NameServer 的通信底层是通过 Netty 实现的。

1.5.3 Producer

消息生产者,业务端负责发送消息,由用户自行实现和分布式部署。

Producer由用户进行分布式部署,消息由Producer通过多种负载均衡模式发送到Broker集群,发送低延时,支持快速失败。 RocketMQ 提供了三种方式发送消息:同步异步单向

1.5.4 Consumer

消息消费者,负责消费消息,一般是后台系统负责异步消费。

Consumer也由用户部署,支持PUSHPULL两种消费模式,支持集群消费和广播消费,提供实时的消息订阅机制。

2 原理

2.1 RocketMQ整体工作流程

简单来说,RocketMQ是一个分布式消息队列,也就是消息队列+分布式系统

作为消息队列,它是发-存-收的一个模型,对应的就是Producer、Broker、Cosumer;作为分布式系统,它要有服务端、客户端、注册中心,对应的就是Broker、Producer/Consumer、NameServer

所以我们看一下它主要的工作流程:RocketMQNameServer注册中心集群、Producer生产者集群、Consumer消费者集群和若干BrokerRocketMQ进程)组成:

在这里插入图片描述

2.2 为什么RocketMQ不使用Zookeeper作为注册中心

Kafka我们都知道采用Zookeeper作为注册中心——当然也开始逐渐去ZookeeperRocketMQ不使用Zookeeper其实主要可能从这几方面来考虑:

2.3 Broker保存数据(CommitLog,ConsumeQueue,Indexfile)

RocketMQ主要的存储文件包括CommitLog文件、ConsumeQueue文件、Indexfile文件

在这里插入图片描述

在这里插入图片描述

CommitLog文件保存于${Rocket_Home}/store/commitlog目录中,从图中我们可以明显看出来文件名的偏移量,每个文件默认1G,写满后自动生成一个新的文件。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

总结一下:RocketMQ采用的是混合型的存储结构,即为Broker单个实例下所有的队列共用一个日志数据文件(即为CommitLog)来存储。

RocketMQ的混合型存储结构(多个Topic的消息实体内容都存储于一个CommitLog中)针对ProducerConsumer分别采用了数据索引部分相分离的存储结构,Producer发送消息至Broker端,然后Broker端使用同步或者异步的方式对消息刷盘持久化,保存至CommitLog中。

只要消息被刷盘持久化至磁盘文件CommitLog中,那么Producer发送的消息就不会丢失。正因为如此,Consumer也就肯定有机会去消费这条消息。当无法拉取到消息后,可以等下一次消息拉取,同时服务端也支持长轮询模式,如果一个消息拉取请求未拉取到消息,Broker允许等待30s的时间,只要这段时间内有新消息到达,将直接返回给消费端。 这里,RocketMQ的具体做法是,使用Broker端的后台服务线程—ReputMessageService不停地分发请求并异步构建ConsumeQueue(逻辑消费队列)和IndexFile(索引文件)数据。

在这里插入图片描述

2.4 RocketMQ怎么对文件进行读写

RocketMQ对文件的读写巧妙地利用了操作系统的一些高效文件读写方式——PageCache顺序读写零拷贝

2.4.1 PageCache、顺序读取

RocketMQ中,ConsumeQueue逻辑消费队列存储的数据较少,并且是顺序读取,在page cache机制的预读取作用下,Consume Queue文件的读性能几乎接近读内存,即使在有消息堆积情况下也不会影响性能。而对于CommitLog消息存储的日志数据文件来说,读取消息内容时候会产生较多的随机访问读取,严重影响性能。如果选择合适的系统IO调度算法,比如设置调度算法为Deadline(此时块存储采用SSD的话),随机读的性能也会有所提升。

页缓存(PageCache)是OS对文件的缓存,用于加速对文件的读写。一般来说,程序对文件进行顺序读写的速度几乎接近于内存的读写速度,主要原因就是由于OS使用PageCache机制对读写访问操作进行了性能优化,将一部分的内存用作PageCache。对于数据的写入,OS会先写入至Cache内,随后通过异步的方式由pdflush内核线程将Cache内的数据刷盘至物理磁盘上。对于数据的读取,如果一次读取文件时出现未命中PageCache的情况,OS从物理磁盘上访问读取文件的同时,会顺序对其他相邻块的数据文件进行预读取

2.4.2 零拷贝

RocketMQ主要通过MappedByteBuffer对文件进行读写操作。其中,利用了NIO中的FileChannel模型将磁盘上的物理文件直接映射到用户态的内存地址中(这种Mmap的方式减少了传统IO,将磁盘文件数据在操作系统内核地址空间的缓冲区,和用户应用程序地址空间的缓冲区之间来回进行拷贝的性能开销),将对文件的操作转化为直接对内存地址进行操作,从而极大地提高了文件的读写效率(正因为需要使用内存映射机制,故RocketMQ的文件存储都使用定长结构来存储,方便一次将整个文件映射至内存)。

什么是零拷贝 在操作系统中,使用传统的方式,数据需要经历几次拷贝,还要经历用户态/内核态切换

在这里插入图片描述

所以,可以通过零拷贝的方式,减少用户态与内核态的上下文切换和内存拷贝的次数,用来提升I/O的性能。零拷贝比较常见的实现方式是mmap,这种机制在Java中是通过MappedByteBuffer实现的。

在这里插入图片描述

2.5 消息刷盘怎么实现

RocketMQ提供了两种刷盘策略:同步刷盘异步刷盘

Broker在消息的存取时直接操作的是内存(内存映射文件),这可以提供系统的吞吐量,但是无法避免机器掉电时数据丢失,所以需要持久化到磁盘中

刷盘的最终实现都是使用NIO中的 MappedByteBuffer.force() 将映射区的数据写入到磁盘,如果是同步刷盘的话,在Broker把消息写到CommitLog映射区后,就会等待写入完成 异步而言,只是唤醒对应的线程,不保证执行的时机,流程如图所示。

在这里插入图片描述

2.6 RocketMQ的负载均衡

RocketMQ中的负载均衡都在Client端完成,具体来说的话,主要可以分为Producer端发送消息时候的负载均衡和Consumer端订阅消息的负载均衡。

2.6.1 Producer的负载均衡

Producer端在发送消息的时候,会先根据Topic找到指定的TopicPublishInfo,在获取了TopicPublishInfo路由信息后,RocketMQ的客户端在默认方式下selectOneMessageQueue()方法会从TopicPublishInfo中的messageQueueList中选择一个队列(MessageQueue)进行发送消息。具这里有一个sendLatencyFaultEnable开关变量,如果开启,在随机递增取模的基础上,再过滤掉not availableBroker代理。

Producer负载均衡:索引递增随机取模
public MessageQueue selectOneMessageQueue(){
	//索引递增
	int index = this.sendWhichQueue.incrementAndGet();
	//利用索引取随机数,取余数
	int pos = Math.abs(index) % this.messageQueueList.size();
	if(pos<0){
		pos=0;	
	}
	return this.messageQueueList.get(pos);
}

所谓的latencyFaultTolerance,是指对之前失败的,按一定的时间做退避。例如,如果上次请求的latency超过550Lms,就退避3000Lms;超过1000L,就退避60000L;如果关闭,采用随机递增取模的方式选择一个队列(MessageQueue)来发送消息,latencyFaultTolerance机制是实现消息发送高可用的核心关键所在。

2.6.2 Consumer的负载均衡

RocketMQ中,Consumer端的两种消费模式(Push/Pull)都是基于拉模式来获取消息的,而在Push模式只是对pull模式的一种封装,其本质实现为消息拉取线程在从服务器拉取到一批消息后,然后提交到消息消费线程池后,又“马不停蹄”的继续向服务器再次尝试拉取消息。如果未拉取到消息,则延迟一下又继续拉取。在两种基于拉模式的消费方式(Push/Pull)中,均需要Consumer端知道从Broker端的哪一个消息队列中去获取消息。因此,有必要在Consumer端来做负载均衡,即Broker端中多个MessageQueue分配给同一个ConsumerGroup中的哪些Consumer消费。

  1. Consumer端的心跳包发送 在Consumer启动后,它就会通过定时任务不断地向RocketMQ集群中的所有Broker实例发送心跳包(其中包含了,消息消费分组名称、订阅关系集合、消息通信模式和客户端id的值等信息)。Broker端在收到Consumer的心跳消息后,会将它维护在ConsumerManager的本地缓存变量—consumerTable,同时并将封装后的客户端网络通道信息保存在本地缓存变量—channelInfoTable中,为之后做Consumer端的负载均衡提供可以依据的元数据信息。
  2. Consumer端实现负载均衡的核心类—RebalanceImplConsumer实例的启动流程中的启动MQClientInstance实例部分,会完成负载均衡服务线程—RebalanceService的启动(每隔20s执行一次)。 通过查看源码可以发现,RebalanceService线程的run()方法最终调用的是RebalanceImpl类的rebalanceByTopic()方法,这个方法是实现Consumer端负载均衡的核心。 rebalanceByTopic()方法会根据消费者通信类型为广播模式还是集群模式做不同的逻辑处理

2.7 RocketMQ消息长轮询

所谓的长轮询,就是Consumer拉取消息,如果对应的Queue如果没有数据,Broker不会立即返回,而是把 PullReuqest hold起来,等待 queue消息后,或者长轮询阻塞时间到了,再重新处理该 queue 上的所有 PullRequest

在这里插入图片描述

PullMessageProcessor#processRequest

 //如果没有拉到数据
case ResponseCode.PULL_NOT_FOUND:
// broker 和 consumer 都允许 suspend,默认开启
if (brokerAllowSuspend && hasSuspendFlag) {
    long pollingTimeMills = suspendTimeoutMillisLong;
    if (!this.brokerController.getBrokerConfig().isLongPollingEnable()) {
          pollingTimeMills = this.brokerController.getBrokerConfig().getShortPollingTimeMills();
     }
    String topic = requestHeader.getTopic();
    long offset = requestHeader.getQueueOffset();
    int queueId = requestHeader.getQueueId();
    //封装一个PullRequest
    PullRequest pullRequest = new PullRequest(request, channel, pollingTimeMills,
    this.brokerController.getMessageStore().now(), offset, subscriptionData, messageFilter);
   //把PullRequest挂起来
   this.brokerController.getPullRequestHoldService().suspendPullRequest(topic, queueId, pullRequest);
     response = null;
     break;
}

挂起的请求,有一个服务线程会不停地检查,看queue中是否有数据,或者超时。

PullRequestHoldService#run()

@Override
public void run() {
   log.info("{} service started", this.getServiceName());
   while (!this.isStopped()) {
       try {
          if (this.brokerController.getBrokerConfig().isLongPollingEnable()) {
                 this.waitForRunning(5 * 1000);
          } else {
                    this.waitForRunning(this.brokerController.getBrokerConfig().getShortPollingTimeMills());
                }
         long beginLockTimestamp = this.systemClock.now();
         //检查hold住的请求
         this.checkHoldRequest();
         long costTime = this.systemClock.now() - beginLockTimestamp;
         if (costTime > 5 * 1000) {
              log.info("[NOTIFYME] check hold request cost {} ms.", costTime);
         }
        } catch (Throwable e) {
            log.warn(this.getServiceName() + " service has exception. ", e);
            }
        }
	log.info("{} service end", this.getServiceName());
    }

到此这篇关于深入讲解RocketMQ原理的文章就介绍到这了,更多相关RocketMQ原理内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文