一、概要

随着使用的队列和虚拟主题的增加,ActiveMQ IO 模块达到了瓶颈。官方表述他们想尽力通过节流、断路器或降级来解决这个问题,但效果不佳。所以我们开始关注当时流行的消息解决方案Kafka。不幸的是,Kafka 无法满足要求,尤其是在低延迟和高可靠性方面(更多参看);在这种情况下,RocketMQ应运而生,旨在成为一种新的消息传递引擎来处理更广泛的用例,从传统的发布/订阅场景到大容量实时零损失容忍交易系统。如今,已有 100 多家公司在其业务中使用了 RocketMQ 的开源版本。

Apache RocketMQ 是一个分布式消息和流媒体平台,具有低延迟、高性能和可靠性、万亿级容量和灵活的可扩展性。

spring rocketmq 配置详解 rocketmq apache_kafka


官方网站

rocketmq4.9.1-linux源码包

当前作为一款MQ软件,它还是种跨进程的通信机制,用于进程间传递消息。同样有异步解耦的特点,主要目的是减少请求响应时间和解耦,主要的使用场景就是将比较耗时而且不需要即时(同步)返回结果的操作作为消息放入消息队列,同时,由于使用了消息队列MQ,只要保证消息格式不变,消息的发送方和接收方并不需要彼此联系,也不需要受对方的影响。另一主要场景就是被用于流量削峰,比如在秒杀、团购等活动时,由于用户访问量非常大,会导致服务无法承受这么大的流量,甚至导致系统崩溃,为此,可在服务调用之间加入MQ,使得巨大的流量按照顺序依次调用服务,精准控制服务的最大请求数,保证服务的可用性。

二、架构

1、【官方架构图】:

spring rocketmq 配置详解 rocketmq apache_rabbitmq_02


如上图所示,Apache RocketMQ 由四部分组成:名称服务器、代理、生产者和消费者。它们中的每一个都可以水平扩展从而可避免单一的故障点。

Nameserver(cluster):名称服务器提供轻量级服务发现路由。每个 Name Server 记录完整的路由信息,提供相应的读写服务,并支持快速存储扩展。NameServer 提供了很多功能,主要包括两个:

1)Broker 管理:NameServer 接受来自 Broker 集群的注册,并提供心跳机制来检查 Broker 是否处于活动状态。

2)路由管理:每个 NameServer 将保存有关代理集群的完整路由信息客户端查询队列信息。

    NameServer本身是无状态的,并且多个NameServer之间并没有通信,可以横向扩展多台,Broker会和每一台NameServer建立长连接;

Broker(Cluster):Brokers 通过提供轻量级的 TOPIC(主题)QUEUE(队列) 机制来处理消息存储。它们支持Push(推)和Pull模型,包含容错机制(2副本或3副本),并提供强大的峰值填充和按原始时间顺序累积数千亿条消息的能力。此外,Brokers 提供灾难恢复、丰富的指标统计和警报机制,这些都是传统消息传递系统所缺乏的。

    Broker是RocketMQ中的核心组件,用于消息的接收、存储、递和消息查询、HA 保证等,而NameServer是用来管理Broker的。Broker Server还有几个重要的子模块:

spring rocketmq 配置详解 rocketmq apache_apache_03


a)远程模块(Remoting Module):代理的入口,处理来自客户端的请求。

b)客户端管理器(Client Manager):管理客户端(生产者/消费者)并维护消费者的主题订阅。

c)存储服务(Store Service):提供简单的 API 来存储或查询物理磁盘中的消息。

d)HA Service:提供主代理和从代理之间的数据同步功能。

e)索引服务(Index Service):通过指定的key为消息建立索引,并提供快速的消息查询。

    Broker是RocketMQ的核心,提供了消息的接收,存储,拉取等功能,一般都需要保证Broker的高可用,所以会配置Broker Slave,当Master挂掉之后,Consumer然后可以消费Slave;Broker分为Master和Slave,一个Master可以对应多个Slave,Master与Slave的对应关系通过指定相同的BrokerName,不同的BrokerId来定义,BrokerId为0表示Master,非0表示Slave;

Producer (Cluster):生产者支持分布式部署。分布式生产者通过多种负载均衡方式向 Broker 集群发送消息。发送进程支持快速故障切换和低延迟。

    Producer是消息队列的生产者,需要与NameServer建立连接,从NameServer获取Topic路由信息,并向提供Topic服务的Broker Master建立连接;Producer也是无状态的。

Consumer (Cluster):消费者也支持推拉模型中的分布式部署。它还支持集群消费和消息广播。提供实时消息订阅机制,可以满足大部分消费者需求。

    消息队列的消费者,同样与NameServer建立连接,从NameServer获取Topic路由信息,并向提供Topic服务的Broker Master,Slave建立连接;

【架构参考】:

spring rocketmq 配置详解 rocketmq apache_rabbitmq_04

2、消息中间件对照

【RocketMQ 、 ActiveMQ vs、Kafka对比】:下入是rocketmq官网的分析,可能偏产品,但还是可以作为我们选品的参考

spring rocketmq 配置详解 rocketmq apache_客户端_05

3、工作原理:

RocketMQ 客户端(生产者/消费者)会从 NameServer 查询队列路由信息,但是客户端如何找到 NameServer 地址呢?有四种方法(详细请单击参看)可以将 NameServer 地址列表提供给客户端:

1>程序化方式,如producer.setNamesrvAddr(“ip:port”)。

a. 对于broke来说,我们可在其配置文件中指定:namesrvAddr=name-server-ip1:port;name-server-ip2:port来告知Nameserver。
b. admin shell命令里指定:sh mqadmin command-name -n name-server-ip1:port;name-server-ip2:port -X OTHER-OPTION

2>Java 选项,使用 Rocketmq.namesrv.addr

3>环境变量,使用 NAMESRV_ADDR。

4>HTTP Endpoint。

如果不使用前面提到的方法指定名称服务器地址列表,Apache RocketMQ 将每两分钟访问以下 HTTP 端点获取和更新名称服务器地址列表,初始延迟为 10 秒。默认情况下,endpoint是:
http://jmenv.tbsite.net:8080/rocketmq/nsaddr,按现场实际可修改此endpoint。

如果在生产中运行 Apache RocketMQ,建议使用上述方法,因为它提供了最大的灵活性——后期可以根据名称服务器的系统负载动态添加或删除名称服务器节点,而无需重新启动代理和客户端。

    Nameserver旨在协调分布式系统的每个组件,并通过管理主题路由信息来履行大部分职责。管理组件,粗略地说,即上文提到的代理和路由两部分;因此,在启动代理和客户端之前,我们需要通过向它们提供名称服务器地址列表来告诉它们如何到达名称服务器,方式即上述4种。

注:优先级为:编程方式 > Java 选项 > 环境变量 > HTTP 端点。

spring rocketmq 配置详解 rocketmq apache_客户端_06


spring rocketmq 配置详解 rocketmq apache_kafka_07


其中:

NameServer: NameServer是一个非常简单的Topic路由注册中心,其角色类似Dubbo中的zookeeper,支持Broker的动态注册与发现。NameServer是一个几乎无状态节点,可集群部署,节点之间无任何信息同步。主要包括两个功能:

    Broker管理,NameServer接受Broker集群的注册信息并且保存下来作为路由信息的基本数据。然后提供心跳检测机制,检查Broker是否还存活;

    路由信息管理,每个NameServer将保存关于Broker集群的整个路由信息和用于客户端查询的队列信息。然后Producer和Conumser通过NameServer就可以知道整个Broker集群的路由信息,从而进行消息的投递和消费。NameServer通常也是集群的方式部署,各实例间相互不进行信息通讯。Broker是向每一台NameServer注册自己的路由信息,所以每一个NameServer实例上面都保存一份完整的路由信息。当某个NameServer因某种原因下线了,Broker仍然可以向其它NameServer同步其路由信息,Producer,Consumer仍然可以动态感知Broker的路由的信息。

spring rocketmq 配置详解 rocketmq apache_客户端_08

Broker: Broker分为Master与Slave,一个Master可以对应多个Slave,但是一个Slave只能对应一个Master,Master与Slave 的对应关系通过指定相同的BrokerName,不同的BrokerId 来定义,BrokerId为0表示Master,非0表示Slave。Master也可以部署多个。每个Broker与NameServer集群中的所有节点建立长连接,定时注册Topic信息到所有NameServer。 注意:当前RocketMQ版本在部署架构上支持一Master多Slave,但只有BrokerId=1的从服务器才会参与消息的读负载。

Producer:它与NameServer集群中的其中一个节点(随机选择)建立长连接,定期从NameServer获取Topic路由信息,并向提供Topic 服务的Master建立长连接,且定时向Broker Master发送心跳。Producer完全无状态,可集群部署。

Consumer:它与NameServer集群中的其中一个节点(随机选择)建立长连接,定期从NameServer获取Topic路由信息,并向提供Topic服务的Broker Master、Slave建立长连接,且定时向Master、Slave发送心跳。Consumer既可以从Master订阅消息,也可以从Slave订阅消息,消费者在向Master拉取消息时,Master服务器会根据拉取偏移量与最大偏移量的距离(判断是否读老消息,产生读I/O),以及从服务器是否可读等因素建议下一次是从Master还是Slave拉取。

Topic和Message Queue的作用:

Topic(主题),用来区分不同类型的消息,发送和接收消息前都需要先创建Topic,针对Topic来发送和接收消息,为了提高性能和吞吐量,引入了Message Queue,一个Topic可以设置一个或多个Message Queue,有点类似kafka的分区(Partition),这样消息就可以并行往各个Message Queue发送消息,消费者也可以并行的从多个Message Queue读取消息;

整个流程如下:

1)启动NameServer,NameServer起来后监听端口,等待Broker、Producer、Consumer连上来完成注册,相当于一个路由控制中心。
2)Broker启动,跟所有的NameServer保持长连接,定时发送心跳包。心跳包中包含当前Broker信息(IP+端口等)以及存储所有Topic信息。注册成功后,NameServer集群中就有Topic跟Broker的映射关系。
3)收发消息前,先创建Topic,创建Topic时需要指定该Topic要存储在哪些Broker上,也可以在发送消息时自动创建Topic。
4)Producer发送消息,启动时先跟NameServer集群中的其中一台建立长连接,并从NameServer中获取当前发送的Topic存在哪些Broker上,轮询从队列列表中选择一个队列,然后与队列所在的Broker建立长连接从而向Broker发消息。
5)Consumer跟Producer类似,跟其中一台NameServer建立长连接,获取当前订阅Topic存在哪些Broker上,然后直接跟Broker建立连接通道,开始消费消息。

总结:消息生产者生产消息前,会先向NameServer中获取Broker,这就要求Broker需先注册到NameServer中,producer获取到Broker以后,就可以生产消息并交由Broker存储,Broker内部调用模块MessageQueue存储消息,而MessageQueue又会被逻辑划分为一个一个的topic,之后,消息消费者从NameServer中获取Broker,获取到Broker后,取出消息进行消费。

spring rocketmq 配置详解 rocketmq apache_客户端_09


详情参看

4、消息存储

消息存储是RocketMQ中最为复杂和最为重要的一部分。我们从RocketMQ的消息存储整体架构、PageCache与Mmap内存映射以及RocketMQ中两种不同的刷盘方式 3个方面来理解。

1)消息存储整体架构

消息存储架构图中主要有下面三个跟消息存储相关的文件构成。

(1) CommitLog消息主体以及元数据的存储主体,存储Producer端写入的消息主体内容,消息内容不是定长的。单个文件大小默认1G ,文件名长度为20位,左边补零,剩余为起始偏移量,比如00000000000000000000代表了第一个文件,起始偏移量为0,文件大小为1G=1073741824;当第一个文件写满了,第二个文件为00000000001073741824,起始偏移量为1073741824,以此类推。消息主要是顺序写入日志文件,当文件满了,写入下一个文件;

(2) ConsumeQueue:消息消费队列,引入的目的主要是提高消息消费的性能,由于RocketMQ是基于主题topic订阅模式,消息消费是针对主题进行的,如果要遍历commitlog文件中根据topic检索消息是非常低效的。Consumer即可根据ConsumeQueue来查找待消费的消息。其中,ConsumeQueue(逻辑消费队列)作为消费消息的索引,保存了指定Topic下的队列消息在CommitLog中的起始物理偏移量offset,消息大小size和消息Tag的HashCode值。consumequeue文件可以看成是基于topic的commitlog索引文件,故consumequeue文件夹的组织方式如下:topic/queue/file三层组织结构,具体存储路径为:$HOME/store/consumequeue/{topic}/{queueId}/{fileName}。同样consumequeue文件采取定长设计,每一个条目共20个字节,分别为8字节的commitlog物理偏移量、4字节的消息长度、8字节tag hashcode,单个文件由30W个条目组成,可以像数组一样随机访问每一个条目,每个ConsumeQueue文件大小约5.72M

(3) IndexFile:IndexFile(索引文件)提供了一种可以通过key或时间区间来查询消息的方法。Index文件的存储位置是:$HOME \store\index${fileName},文件名fileName是以创建时的时间戳命名的,固定的单个IndexFile文件大小约为400M,一个IndexFile可以保存 2000W个索引,IndexFile的底层存储设计为在文件系统中实现HashMap结构,故Rocketmq的索引文件其底层实现为hash索引

在上面的RocketMQ的消息存储整体架构图中可以看出,RocketMQ采用的是混合型的存储结构,即为Broker单个实例所有的队列共用一个日志数据文件(即为CommitLog)来存储。RocketMQ的混合型存储结构(多个Topic的消息实体内容都存储于一个CommitLog中)针对Producer和Consumer分别采用了数据和索引部分相分离的存储结构,Producer发送消息至Broker端,然后Broker端使用同步或者异步的方式对消息刷盘持久化,保存至CommitLog中。只要消息被刷盘持久化至磁盘文件CommitLog中,那么Producer发送的消息就不会丢失。正因为如此,Consumer也就肯定有机会去消费这条消息。当无法拉取到消息后,可以等下一次消息拉取,同时服务端也支持长轮询模式,如果一个消息拉取请求未拉取到消息,Broker允许等待30s的时间,只要这段时间内有新消息到达,将直接返回给消费端。这里,RocketMQ的具体做法是,使用Broker端的后台服务线程—ReputMessageService不停地分发请求并异步构建ConsumeQueue(逻辑消费队列)和IndexFile(索引文件)数据。

spring rocketmq 配置详解 rocketmq apache_apache_10


commitlog 是整个消息队列存储的核心文件,而consumerquque是逻辑消息队列,主要存储commitlog offset,消息长度,tag的hashcode,用于在消息消费时快速定位消息(索引到消息)在commit log文件位置,便于读取消息。IndexFile俗称索引文件,主要存储消息key的hashcode以及commitlog offset,用于通过key快速定位到消息在commit log文件位置,便于读取消息。

拉消息----读index----根据消息的key快速定位commitlog文件中消息位置----拉取消息—完成消费。

2) 页缓存与内存映射

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

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

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

3)消息刷盘

spring rocketmq 配置详解 rocketmq apache_kafka_11


(1) 同步刷盘:如上图所示,只有在消息真正持久化至磁盘后,RocketMQ的Broker端才会真正返回给Producer端一个成功的ACK响应。同步刷盘对MQ消息可靠性来说是一种不错的保障,但是性能上会有较大影响,一般适用于金融业务应用该模式较多。

(2) 异步刷盘:能够充分利用OS的PageCache的优势,只要消息写入PageCache即可将成功的ACK返回给Producer端。消息刷盘采用后台异步线程提交的方式进行,降低了读写延迟,提高了MQ的性能和吞吐量。

5、通信机制

RocketMQ消息队列集群主要包括NameServer、Broker(Master/Slave)、Producer、Consumer4个角色,基本通讯流程如下:

(1) Broker启动后需要完成一次将自己注册至NameServer的操作;随后每隔30s时间定时向NameServer上报Topic路由信息。

(2) 消息生产者Producer作为客户端发送消息时候,需要根据消息的Topic从本地缓存的TopicPublishInfoTable获取路由信息。如果没有则更新路由信息会从NameServer上重新拉取,同时Producer会默认每隔30s向NameServer拉取一次路由信息。

(3) 消息生产者Producer根据2)中获取的路由信息选择一个队列(MessageQueue)进行消息发送;Broker作为消息的接收者接收消息并落盘存储。

(4) 消息消费者Consumer根据2)中获取的路由信息,并再完成客户端的负载均衡后,选择其中的某一个或者某几个消息队列来拉取消息并进行消费。

从上面1)~3)中可以看出在消息生产者, Broker和NameServer之间都会发生通信(这里只说了MQ的部分通信),因此如何设计一个良好的网络通信模块在MQ中至关重要,它将决定RocketMQ集群整体的消息传输能力与最终的性能。

rocketmq-remoting 模块是 RocketMQ消息队列中负责网络通信的模块,它几乎被其他所有需要网络通信的模块(诸如rocketmq-client、rocketmq-broker、rocketmq-namesrv)所依赖和引用。为了实现客户端与服务器之间高效的数据请求与接收,RocketMQ消息队列自定义了通信协议并在Netty的基础之上扩展了通信模块。

1) Remoting通信类结构

spring rocketmq 配置详解 rocketmq apache_客户端_12


2)Reactor多线程设计

RocketMQ的RPC通信采用Netty组件作为底层通信库,同样也遵循了Reactor多线程模型,同时又在这之上做了一些扩展和优化。

spring rocketmq 配置详解 rocketmq apache_rabbitmq_13


上面的框图中可以大致了解RocketMQ中NettyRemotingServer的Reactor 多线程模型。一个 Reactor 主线程(eventLoopGroupBoss,即为上面的1)负责监听 TCP网络连接请求,建立好连接,创建SocketChannel,并注册到selector上。RocketMQ的源码中会自动根据OS的类型选择NIO和Epoll,也可以通过参数配置),然后监听真正的网络数据。拿到网络数据后,再丢给Worker线程池(eventLoopGroupSelector,即为上面的“N”,源码中默认设置为3),在真正执行业务逻辑之前需要进行SSL验证、编解码、空闲检查、网络连接管理,这些工作交给defaultEventExecutorGroup(即为上面的“M1”,源码中默认设置为8)去做。而处理业务操作放在业务线程池中执行,根据 RomotingCommand 的业务请求码code去processorTable这个本地缓存变量中找到对应的 processor,然后封装成task任务后,提交给对应的业务processor处理线程池来执行(sendMessageExecutor,以发送消息为例,即为上面的 “M2”)。从入口到业务逻辑的几个步骤中线程池一直再增加,这跟每一步逻辑复杂性相关,越复杂,需要的并发通道越宽。

spring rocketmq 配置详解 rocketmq apache_客户端_14

6、消息过滤

RocketMQ分布式消息队列的消息过滤方式有别于其它MQ中间件,是在Consumer端订阅消息时再做消息过滤的。RocketMQ这么做是在于其Producer端写入消息和Consumer端订阅消息采用分离存储的机制来实现的,Consumer端订阅消息是需要通过ConsumeQueue这个消息消费的逻辑队列拿到一个索引,然后再从CommitLog里面读取真正的消息实体内容,所以说到底也是还绕不开其存储结构。其ConsumeQueue的存储结构如下,可以看到其中有8个字节存储的Message Tag的哈希值,基于Tag的消息过滤正式基于这个字段值的。

spring rocketmq 配置详解 rocketmq apache_rabbitmq_15


主要支持如下2种的过滤方式:

(1) Tag过滤方式:Consumer端在订阅消息时除了指定Topic还可以指定TAG,如果一个消息有多个TAG,可以用||分隔。其中,Consumer端会将这个订阅请求构建成一个 SubscriptionData,发送一个Pull消息的请求给Broker端。Broker端从RocketMQ的文件存储层—Store读取数据之前,会用这些数据先构建一个MessageFilter,然后传给Store。Store从 ConsumeQueue读取到一条记录后,会用它记录的消息tag hash值去做过滤,由于在服务端只是根据hashcode进行判断,无法精确对tag原始字符串进行过滤,故在消息消费端拉取到消息后,还需要对消息的原始tag字符串进行比对,如果不同,则丢弃该消息,不进行消息消费。

(2) SQL92的过滤方式:这种方式的大致做法和上面的Tag过滤方式一样,只是在Store层的具体过滤过程不太一样,真正的 SQL expression 的构建和执行由rocketmq-filter模块负责的。每次过滤都去执行SQL表达式会影响效率,所以RocketMQ使用了BloomFilter避免了每次都去执行。SQL92的表达式上下文为消息的属性。

7、负载均衡

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

1) Producer的负载均衡

Producer端在发送消息的时候,会先根据Topic找到指定的TopicPublishInfo,在获取了TopicPublishInfo路由信息后,RocketMQ的客户端在默认方式下selectOneMessageQueue()方法会从TopicPublishInfo中的messageQueueList中选择一个队列(MessageQueue)进行发送消息。具体的容错策略均在MQFaultStrategy这个类中定义。这里有一个sendLatencyFaultEnable开关变量,如果开启,在随机递增取模的基础上,再过滤掉not available的Broker代理。所谓的"latencyFaultTolerance",是指对之前失败的,按一定的时间做退避。例如,如果上次请求的latency超过550Lms,就退避3000Lms;超过1000L,就退避60000L;如果关闭,采用随机递增取模的方式选择一个队列(MessageQueue)来发送消息,latencyFaultTolerance机制是实现消息发送高可用的核心关键所在。

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端实现负载均衡的核心类—RebalanceImpl

在Consumer实例的启动流程中的启动MQClientInstance实例部分,会完成负载均衡服务线程—RebalanceService的启动(每隔20s执行一次)。通过查看源码可以发现,RebalanceService线程的run()方法最终调用的是RebalanceImpl类的rebalanceByTopic()方法,该方法是实现Consumer端负载均衡的核心。这里,rebalanceByTopic()方法会根据消费者通信类型为“广播模式”还是“集群模式”做不同的逻辑处理。这里主要来看下集群模式下的主要处理流程:

(1) 从rebalanceImpl实例的本地缓存变量—topicSubscribeInfoTable中,获取该Topic主题下的消息消费队列集合(mqSet);

(2) 根据topic和consumerGroup为参数调用mQClientFactory.findConsumerIdList()方法向Broker端发送获取该消费组下消费者Id列表的RPC通信请求(Broker端基于前面Consumer端上报的心跳包数据而构建的consumerTable做出响应返回,业务请求码:GET_CONSUMER_LIST_BY_GROUP);

(3) 先对Topic下的消息消费队列、消费者Id排序,然后用消息队列分配策略算法(默认为:消息队列的平均分配算法),计算出待拉取的消息队列。这里的平均分配算法,类似于分页的算法,将所有MessageQueue排好序类似于记录,将所有消费端Consumer排好序类似页数,并求出每一页需要包含的平均size和每个页面记录的范围range,最后遍历整个range而计算出当前Consumer端应该分配到的记录(这里即为:MessageQueue)。

spring rocketmq 配置详解 rocketmq apache_服务器_16


(4) 然后,调用updateProcessQueueTableInRebalance()方法,具体的做法是,先将分配到的消息队列集合(mqSet)与processQueueTable做一个过滤比对。

spring rocketmq 配置详解 rocketmq apache_apache_17


上图中processQueueTable标注的红色部分,表示与分配到的消息队列集合mqSet互不包含。将这些队列设置Dropped属性为true,然后查看这些队列是否可以移除出processQueueTable缓存变量,这里具体执行removeUnnecessaryMessageQueue()方法,即每隔1s 查看是否可以获取当前消费处理队列的锁,拿到的话返回true。如果等待1s后,仍然拿不到当前消费处理队列的锁则返回false。如果返回true,则从processQueueTable缓存变量中移除对应的Entry;

上图中processQueueTable的绿色部分,表示与分配到的消息队列集合mqSet的交集。判断该ProcessQueue是否已经过期了,在Pull模式的不用管,如果是Push模式的,设置Dropped属性为true,并且调用removeUnnecessaryMessageQueue()方法,像上面一样尝试移除Entry;

最后,为过滤后的消息队列集合(mqSet)中的每个MessageQueue创建一个ProcessQueue对象并存入RebalanceImpl的processQueueTable队列中(其中调用RebalanceImpl实例的computePullFromWhere(MessageQueue mq)方法获取该MessageQueue对象的下一个进度消费值offset,随后填充至接下来要创建的pullRequest对象属性中),并创建拉取请求对象—pullRequest添加到拉取列表—pullRequestList中,最后执行dispatchPullRequest()方法,将Pull消息的请求对象PullRequest依次放入PullMessageService服务线程的阻塞队列pullRequestQueue中,待该服务线程取出后向Broker端发起Pull消息的请求。其中,可以重点对比下,RebalancePushImpl和RebalancePullImpl两个实现类的dispatchPullRequest()方法不同,RebalancePullImpl类里面的该方法为空,这样子也就回答了上一篇中最后的那道思考题了。

消息消费队列在同一消费组不同消费者之间的负载均衡,其核心设计理念是在一个消息消费队列在同一时间只允许被同一消费组内的一个消费者消费,一个消息消费者能同时消费多个消息队列。

三、部署

软件下载后,执行以下命令:
unzip rocketmq-all-4.9.1-source-release.zip
 cd rocketmq-all-4.9.1/
 mvn -Prelease-all -DskipTests clean install -U
 cd distribution/target/rocketmq-4.9.1/rocketmq-4.9.1

1)启动Nameserver

nohup sh bin/mqnamesrv & //默认端口为10911;
 tail -f ~/logs/rocketmqlogs/namesrv.log //启动成功后会看如下输出,默认端口为9876;
 ……The Name Server boot success…

2)启动Broker:

export NAMESRV_ADDR=localhost:9876
nohup sh bin/mqbroker -n localhost:9876 & //-n,指定NameServer的地址用于连接,默认通信端口为9876
 tail -f ~/logs/rocketmqlogs/broker.log //启动成功后会看如下输出
 ……The broker[%s, 172.30.30.233:10911] boot success…

语法:nohup mqbroker -n xx.xx.xx.xx:9876 autoCreateTopicEnable=true -c /usr/local/rocketmq/conf/broker.conf &

3)Send & Receive Messages

在发送/接收消息之前,我们需要告诉客户端nameserver的位置(我们可以配置环境变量NAMESRV_ADDR)。 RocketMQ 提供了多种方式来实现这一点:
export NAMESRV_ADDR=localhost:9876

sh bin/tools.sh org.apache.rocketmq.example.quickstart.Producer //启动消息生产者
 SendResult [sendStatus=SEND_OK, msgId= …
 sh bin/tools.sh org.apache.rocketmq.example.quickstart.Consumer //启动消费者
 ConsumeMessageThread_%d Receive New Messages: [MessageExt…

4)关停服务

sh bin/mqshutdown broker //输出类似如下
 The mqbroker(36695) is running…
 Send shutdown request to mqbroker(36695) OK
 sh bin/mqshutdown namesrv //输出类似如下
 The mqnamesrv(36664) is running…
 Send shutdown request to mqnamesrv(36664) OK

5)测试验证:

我们可借助RocketMQ的一个Demo来发送消息,首先需要修改脚本的内容:

vim tools.sh //

发送信息:sh tools.sh org.apache.rocketmq.example.quickstart.Producer

接受信息:sh tools.sh org.apache.rocketmq.example.quickstart.Consumer

四、管理命令

1、命令行管理工具

mqadmin是RocketMQ自带的命令行管理工具,可以创建、修改Topic,查询消息,更新配置信息等操作,具体可以通过如下命令查看:

\rocketmq-all-4.3.2-bin-release\bin>mqadmin
The most commonly used mqadmin commands are:
   updateTopic          Update or create topic
   deleteTopic          Delete topic from broker and NameServer.
   updateSubGroup       Update or create subscription group
   deleteSubGroup       Delete subscription group from broker.
   updateBrokerConfig   Update broker's config
   updateTopicPerm      Update topic perm
   topicRoute           Examine topic route info
   topicStatus          Examine topic Status info
   topicClusterList     get cluster info for topic
   brokerStatus         Fetch broker runtime status data
   queryMsgById         Query Message by Id
   queryMsgByKey        Query Message by Key
   queryMsgByUniqueKey  Query Message by Unique key
   queryMsgByOffset     Query Message by offset
   printMsg             Print Message Detail
   printMsgByQueue      Print Message Detail
   sendMsgStatus        send msg to broker.
   brokerConsumeStats   Fetch broker consume stats data
   producerConnection   Query producer's socket connection and client vers
   consumerConnection   Query consumer's socket connection, client version
ubscription
   consumerProgress     Query consumers's progress, speed
   consumerStatus       Query consumer's internal data structure
   cloneGroupOffset     clone offset from other group.
   clusterList          List all of clusters
   topicList            Fetch all topic list from name server
   updateKvConfig       Create or update KV config.
   deleteKvConfig       Delete KV config.
   wipeWritePerm        Wipe write perm of broker in all name server
   resetOffsetByTime    Reset consumer offset by timestamp(without client
t).
   updateOrderConf      Create or update or delete order conf
   cleanExpiredCQ       Clean expired ConsumeQueue on broker.
   cleanUnusedTopic     Clean unused topic on broker.
   startMonitoring      Start Monitoring
   statsAll             Topic and Consumer tps stats
   allocateMQ           Allocate MQ
   checkMsgSendRT       check message send response time
   clusterRT            List All clusters Message Send RT
   getNamesrvConfig     Get configs of name server.
   updateNamesrvConfig  Update configs of name server.
   getBrokerConfig      Get broker config by cluster or special broker!
   queryCq              Query cq command.
   sendMessage          Send a message
   consumeMessage       Consume message

See 'mqadmin help <command>' for more information on a specific command.

如果想看详细的可以如下命令:

>rocketmq-all-4.3.2-bin-release\bin>mqadmin help statsAll
usage: mqadmin statsAll [-a] [-h] [-n <arg>] [-t <arg>]
 -a,--activeTopic         print active topic only
 -h,--help                Print help
 -n,--namesrvAddr <arg>   Name server address list, eg: 192.168.0.1:9876;192.168
.0.2:9876
 -t,--topic <arg>         print select topic only
1)mqadmin之Broke操作命令

updateBrokerConfig、brokerStatus,wipeWritePerm和getBrokerConfig,其中:

updateBrokerConfig :动态更新broker的配置(重启broker后会配置失效)

brokerStatus :获取broker的运行时状态数据

wipeWritePerm:设置某broker为只读

getBrokerConfig:获取broker的配置信息

1》获取broker的配置信息

./mqadmin getBrokerConfig -h

spring rocketmq 配置详解 rocketmq apache_rabbitmq_18


查看指定集群的所有broker信息:./mqadmin getBrokerConfig -c CLUSTERNAME -n nameserver1:9876

spring rocketmq 配置详解 rocketmq apache_rabbitmq_19


查看指定集群的某broker信息:./mqadmin getBrokerConfig -c CLUSTERNAME -n nameserver1:9876 -b 192.168.2.11:10911

spring rocketmq 配置详解 rocketmq apache_客户端_20


2》动态更新broker的配置:./mqadmin updateBrokerConfig -h //命令帮助

spring rocketmq 配置详解 rocketmq apache_客户端_21

./mqadmin updateBrokerConfig -n nameserver1:9876 -c CLUSTERNAME -k fileReservedTime -v 48 //更新集群中所有broker的fileReservedTime为48

spring rocketmq 配置详解 rocketmq apache_apache_22


./mqadmin updateBrokerConfig -n nameserver1:9876 -c CLUSTERNAME -k fileReservedTime -v 48 -b 192.168.2.11:10911 //更新集群中指定broker的fileReservedTime为48

spring rocketmq 配置详解 rocketmq apache_kafka_23


3》获取broker的运行时状态数据

./mqadmin brokerStatus -h //命令帮助

spring rocketmq 配置详解 rocketmq apache_apache_24


./mqadmin brokerStatus -c CLUSTERNAME -n nameserver1:9876 //查看指定集群的所有broker运行时状态

spring rocketmq 配置详解 rocketmq apache_rabbitmq_25


./mqadmin brokerStatus -c CLUSTERNAME -n nameserver1:9876 -b 192.168.2.11:10911 //查看指定集群的某broker的运行时状态

spring rocketmq 配置详解 rocketmq apache_客户端_26


4》设置某broker为只读

./mqadmin wipeWritePerm -h

spring rocketmq 配置详解 rocketmq apache_客户端_27


./mqadmin wipeWritePerm -n nameserver1:9876 -b broker-a //设置某broker为只读

2)查看消费进度:

[root@rocketmq-master1 bin]# mqadmin consumerProgress -n ‘192.168.1.13:9876;192.168.1.14:9876’ //查看当前所有订阅组的状态

spring rocketmq 配置详解 rocketmq apache_服务器_28

2、图形界面管理工具:

除了命令,还提供了图形界面管理工具,在RocketMQ的扩展包里面,具体地址如下:

https://github.com/apache/rocketmq-externals/tree/release-rocketmq-console-1.0.0/rocketmq-console

或者:docker run -e “JAVA_OPTS=-Drocketmq.namesrv.addr=host:9876 -Dcom.rocketmq.sendMessageWithVIPChannel=false” -p 12581:8080 -t styletang/rocketmq-console-ng

目前的稳定版本是1.0.0,可以下载下来在本地运行,它是一个SpringBoot应用,我们需要修改一下应用的配置:首先是端口号,对application.properties做简单配置:

rocketmq.config.namesrvAddr=192.168.10.20:9876

修改指定NameServer的地址,将该应用也注册到NameServer中才能够查询Broker的信息。然后使用Maven进行打包:

mvn clean package -Dmaven.test.skip=true //打包完成后会生成target目录,在该目录下有打包好的jar文件,执行它:

java -jar rocketmq-console-ng-1.0.0.jar

运作之后会启动8080端口,直接访问地址:http://localhost:8080

spring rocketmq 配置详解 rocketmq apache_kafka_29


注:若是启动控制台报错:需要修改一下Broker配置,在RocketMQ安装目录下的conf文件夹中,修改一下broker.conf:添加这样一项配置:

brokerIP1=192.168.10.13    # 你的虚拟机IP
nameSrvAddr=192.168.10.20:9876

然后重启下Broker:

sh mqshutdown broker # 关闭broker
nohup sh mqbroker -n localhost:9876 -c conf/broker.conf &  //-c conf/broker.conf 用于重新加载配置文件

五、FAQ:

1)Broker启动报内存错误:

[root@localhost bin]# ./mqbroker
 Java HotSpot™ 64-Bit Server VM warning: INFO: os::commit_memory(0x00000005c0000000, 8589934592, 0) failed; error=‘Cannot allocate memory’ (errno=12)
 ##There is insufficient memory for the Java Runtime Environment to continue.
 #Native memory allocation (mmap) failed to map 8589934592 bytes for committing reserved memory.
 #An error report file with more information is saved as:
 #/root/rocketmq-all-4.3.2-bin-release/bin/hs_err_pid3977.log

启动失败,报内存不足,主要是rocketmq默认配置的启动参数值比较大,修改runbroker.sh即可

vi runbroker.sh //可看到默认配置的可用内存为8g,虚拟机内存不够时就会报上述错误

JAVA_OPT="${JAVA_OPT} -server -Xms8g -Xmx8g -Xmn4g"

修改JAVA_OPT="${JAVA_OPT} -server -Xms128m -Xmx128m -Xmn128m",完成后,再次启动即可。

2)发送信息时,遇到No route info of this topic 错误

重新启动一下Broker,带上 autoCreateTopicEnable=true 参数启动:

nohup sh mqbroker -n localhost:9876 autoCreateTopicEnable=true -c …/conf/broker.conf

3)mq中生产10万条数据如何知道mq什么时候消费完成

方案1:打标,就是在生产消息的时候把最后一条数据打标,从而在消费的时候能知道哪一条数最后一条数据,当执行完该条数据时,就可以进行数据落地操作,但是考虑到MQ是集群部署的,也就是说可能是乱序的,此方案PASS。

方案2:监听器,监听消费端的某个必执行的方法范围时间内是否被调用,如果未被调用则可认定为已经消费完。前后考虑此方案可行。

方案3:检查生产、消费的数量,需要对生产及消费进行incr计数,当消费总数等于生产总数时,就可以认定消费已完成,最终确定此方案也OK。注意的是:生产计数要在生产完所有消息后才能进行计数。另外,需要考虑消息丢失、重复消费的问题,最后我是用异步来处理数据落地的时机的。

六、参考文献: