消息队列RocketMQ
一、基本概念
1.1 消息模型(Message Model)
RocketMQ
主要由Producer
、Broker
、Consumer
三部分组成,其中Producer
负责生产消息,Consumer
负责消费消息,Broker
负责存储消息。Broker
在实际部署过程中对应一台服务器,每个Broker
可以存储多个Topic
的消息,每个Topic
的消息也可以分片存储于不同的Broker
。Message Queue
用于存储消息的物理地址,每个Topic
中的消息地址存储于多个Message Queue
中(默认4个)。ConsumerGroup
由多个Consumer
实例构成。
1.2 消息生产者(Producer)
负责生产消息,一般由业务系统负责生产消息。一个消息生产者会把业务应用系统里产生的消息发送到broker
服务器。RocketMQ
提供多种发送方式,同步发送、异步发送、顺序发送、单向发送。同步和异步方式均需要Broker
返回确认信息,单向发送不需要。同一类Producer
的集合称为生产者组(Producer Group),这类Producer
发送同一类消息且发送逻辑一致。如果发送的是事务消息且原始生产者在发送之后崩溃,则Broker
服务器会联系同一生产者组的其他生产者实例以提交或回溯消费。
1.3 消息消费者(Consumer)
负责消费消息,一般是后台系统负责异步消费。一个消息消费者会从Broker
服务器拉取消息、并将其提供给应用程序。从用户应用的角度而言提供了两种消费形式:拉取式消费、推动式消费。
- 拉取式消费(Pull Consumer):
Consumer
消费的一种类型,应用通常主动调用Consumer
的拉消息方法从Broker
服务器拉消息、主动权由应用控制。一旦获取了批量消息,应用就会启动消费过程。 - 推动式消费(Push Consumer):
Consumer
消费的一种类型,该模式下Broker
收到数据后会主动推送给消费端,该消费模式一般实时性较高。
同一类Consumer
的集合称为消费者组(Consumer Group),这类Consumer
通常消费同一类消息且消费逻辑一致。消费者组使得在消息消费方面,实现负载均衡和容错的目标变得非常容易。要注意的是,消费者组的消费者实例必须订阅完全相同的Topic
。RocketMQ
支持两种消息模式:集群消费(Clustering
)和广播消费(Broadcasting
)。
- 集群消费(Clustering):集群消费模式下,相同
Consumer Group
的每个Consumer
实例平均分摊消息。 - 广播消费(Broadcasting):广播消费模式下,相同
Consumer Group
的每个Consumer
实例都接收全量的消息。
1.4 代理服务器(Broker Server)
消息中转角色,负责存储消息、转发消息。代理服务器在RocketMQ
系统中负责接收从生产者发送来的消息并存储、同时为消费者的拉取请求作准备。代理服务器也存储消息相关的元数据,包括消费者组、消费进度偏移和主题和队列消息等。
BrokerServer
:Broker
主要负责消息的存储、投递和查询以及服务高可用保证,为了实现这些功能,Broker
包含了以下几个重要子模块。
Remoting Module
:整个Broker
的实体,负责处理来自clients
端的请求。Client Manager
:负责管理客户端(Producer/Consumer
)和维护Consumer
的Topic
订阅信息Store Service
:提供方便简单的API
接口处理消息存储到物理硬盘和查询功能。HA Service
:高可用服务,提供Master Broker
和Slave Broker
之间的数据同步功能。Index Service
:根据特定的Message key
对投递到Broker
的消息进行索引服务,以提供消息的快速查询。
1.5 名字服务(Name Server)
名称服务充当路由消息的提供者。生产者或消费者能够通过名字服务查找各主题相应的Broker IP
列表。多个Namesrv
实例组成集群,但相互独立,没有信息交换。
1.6 消息主题与标签
- 主题(Topic): 表示一类消息的集合,每个主题包含若干条消息,每条消息只能属于一个主题,是
RocketMQ
进行消息订阅的基本单位。 - 标签(Tag): 为消息设置的标志,用于同一主题下区分不同类型的消息。来自同一业务单元的消息,可以根据不同业务目的在同一主题下设置不同标签。标签能够有效地保持代码的清晰度和连贯性,并优化
RocketMQ
提供的查询系统。消费者可以根据Tag
实现对不同子主题的不同消费逻辑,实现更好的扩展性。 - 消息(Message): 消息系统所传输信息的物理载体,生产和消费数据的最小单位,每条消息必须属于一个主题。
RocketMQ
中每个消息拥有唯一的Message ID
,且可以携带具有业务标识的Key
。系统提供了通过Message ID
和Key
查询消息的功能。
二、特性
2.1 订阅与发布
消息的发布是指某个生产者向某个topic
发送消息;消息的订阅是指某个消费者关注了某个topic
中带有某些tag
的消息,进而从该topic
消费数据。
2.2 消息顺序
消息有序指的是一类消息消费时,能按照发送的顺序来消费。例如:一个订单产生了三条消息分别是订单创建、订单付款、订单完成。消费时要按照这个顺序消费才能有意义,但是同时订单之间是可以并行消费的。RocketMQ
可以严格的保证消息有序。
顺序消息分为全局顺序消息与分区顺序消息,全局顺序是指某个Topic
下的所有消息都要保证顺序;部分顺序消息只要保证每一组消息被顺序消费即可。
- 全局顺序
对于指定的一个Topic
,所有消息按照严格的先入先出(FIFO
)的顺序进行发布和消费。
适用场景:性能要求不高,所有的消息严格按照FIFO
原则进行消息发布和消费的场景 - 分区顺序
对于指定的一个Topic
,所有消息根据sharding key
进行区块分区。同一个分区内的消息按照严格的FIFO
顺序进行发布和消费。Sharding key
是顺序消息中用来区分不同分区的关键字段,和普通消息的Key
是完全不同的概念。
适用场景:性能要求高,以sharding key
作为分区字段,在同一个区块中严格的按照FIFO
原则进行消息发布和消费的场景。
- 普通顺序消息(Normal Ordered Message): 普通顺序消费模式下,消费者通过同一个消息队列(
Topic
分区,称作Message Queue
)收到的消息是有顺序的,不同消息队列收到的消息则可能是无顺序的。 - 严格顺序消息(Strictly Ordered Message): 严格顺序消息模式下,消费者收到的所有消息均是有顺序的。
2.3 消息过滤
RocketMQ
的消费者可以根据Tag
进行消息过滤,也支持自定义属性过滤。消息过滤目前是在Broker
端实现的,优点是减少了对于Consumer
无用消息的网络传输,缺点是增加了Broker
的负担、而且实现相对复杂。
2.4 消息可靠性
RocketMQ
支持消息的高可靠,影响消息可靠性的几种情况:
Broker
非正常关闭Broker
异常Crash
OS Crash
- 机器掉电,但是能立即恢复供电情况
- 机器无法开机(可能是
cpu
、主板、内存等关键设备损坏) - 磁盘设备损坏
前四种情况都属于硬件资源可立即恢复情况,RocketMQ
在这四种情况下能保证消息不丢,或者丢失少量数据(依赖刷盘方式是同步还是异步)。
5、6属于单点故障,且无法恢复,一旦发生,在此单点上的消息全部丢失。RocketMQ
在这两种情况下,通过异步复制,可保证99%的消息不丢,但是仍然会有极少量的消息可能丢失。通过同步双写技术可以完全避免单点,同步双写势必会影响性能,适合对消息可靠性要求极高的场合,例如与Money
相关的应用。注:RocketMQ
从3.0
版本开始支持同步双写。
2.5 至少一次
至少一次(At least Once
)指每个消息必须投递一次。Consumer
先Pull
消息到本地,消费完成后,才向服务器返回ack
,如果没有消费一定不会ack
消息,所以RocketMQ
可以很好的支持此特性。
2.6 回溯消费
回溯消费是指Consumer
已经消费成功的消息,由于业务上需求需要重新消费,要支持此功能,Broker
在向Consumer
投递成功消息后,消息仍然需要保留。并且重新消费一般是按照时间维度,例如由于Consumer
系统故障,恢复后需要重新消费1小时前的数据,那么Broker
要提供一种机制,可以按照时间维度来回退消费进度。RocketMQ
支持按照时间回溯消费,时间维度精确到毫秒。
2.7 事务消息
事务消息(Transactional Message
)是指应用本地事务和发送消息操作可以被定义到全局事务中,要么同时成功,要么同时失败。RocketMQ
的事务消息提供类似X/Open XA
的分布事务功能,通过事务消息能达到分布式事务的最终一致。
2.8 定时消息
定时消息(延迟队列)是指消息发送到broker
后,不会立即被消费,等待特定时间投递给真正的topic
。
broker
有配置项messageDelayLevel
,默认值为“1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
”,18
个level
。可以配置自定义messageDelayLevel
。注意,messageDelayLevel
是broker
的属性,不属于某个topic
。发消息时,设置delayLevel
等级即可:msg.setDelayLevel(level)
。level
有以下三种情况:
level == 0
,消息为非延迟消息1 <= level <= maxLevel
,消息延迟特定时间,例如level ==1
,延迟1s
level > maxLevel
,则level == maxLevel
,例如level ==20
,延迟2h
定时消息会暂存在名为SCHEDULE_TOPIC_XXXX
的topic
中,并根据delayTimeLevel
存入特定的queue
,queueId = delayTimeLevel – 1
,即一个queue
只存相同延迟的消息,保证具有相同发送延迟的消息能够顺序消费。broker
会调度地消费SCHEDULE_TOPIC_XXXX
,将消息写入真实的topic
。
需要注意的是,定时消息会在第一次写入和调度写入真实topic
时都会计数,因此发送数量、tps
都会变高。
2.9 重投与重试
重投:生产者在发送消息时,同步消息失败会重投,异步消息有重试,oneway
没有任何保证。消息重投保证消息尽可能发送成功、不丢失,但可能会造成消息重复,消息重复在RocketMQ
中是无法避免的问题。消息重复在一般情况下不会发生,当出现消息量大、网络抖动,消息重复就会是大概率事件。另外,生产者主动重发、consumer
负载变化也会导致重复消息。如下方法可以设置消息重试策略:
retryTimesWhenSendFailed
:同步发送失败重投次数,默认为2
,因此生产者会最多尝试发送retryTimesWhenSendFailed + 1
次。不会选择上次失败的broker
,尝试向其他broker
发送,最大程度保证消息不丢。超过重投次数,抛出异常,由客户端保证消息不丢。当出现RemotingException
、MQClientException
和部分MQBrokerException
时会重投。retryTimesWhenSendAsyncFailed
:异步发送失败重试次数,异步重试不会选择其他broker
,仅在同一个broker
上做重试,不保证消息不丢。retryAnotherBrokerWhenNotStoreOK
:消息刷盘(主或备)超时或slave
不可用(返回状态非SEND_OK),是否尝试发送到其他broker
,默认false
。十分重要消息可以开启。
重试:Consumer
消费消息失败后,要提供一种重试机制,令消息再消费一次。Consumer
消费消息失败通常可以认为有以下几种情况:
- 由于消息本身的原因,例如反序列化失败,消息数据本身无法处理(例如话费充值,当前消息的手机号被注销,无法充值)等。这种错误通常需要跳过这条消息,再消费其它消息,而这条失败的消息即使立刻重试消费,
99%
也不成功,所以最好提供一种定时重试机制,即过10
秒后再重试。 - 由于依赖的下游应用服务不可用,例如
db
连接不可用,外系统网络不可达等。遇到这种错误,即使跳过当前失败的消息,消费其他消息同样也会报错。这种情况建议应用sleep 30s
,再消费下一条消息,这样可以减轻Broker
重试消息的压力。
RocketMQ
会为每个消费组都设置一个Topic
名称为“%RETRY%+consumerGroup
”的重试队列(这里需要注意的是,这个Topic
的重试队列是针对消费组,而不是针对每个Topic
设置的),用于暂时保存因为各种异常而导致Consumer
端无法消费的消息。考虑到异常恢复起来需要一些时间,会为重试队列设置多个重试级别,每个重试级别都有与之对应的重新投递延时,重试次数越多投递延时就越大。RocketMQ
对于重试消息的处理是先保存至Topic
名称为“SCHEDULE_TOPIC_XXXX
”的延迟队列中,后台定时任务按照对应的时间进行Delay
后重新保存至“%RETRY%+consumerGroup
”的重试队列中。
死信队列:死信队列用于处理无法被正常消费的消息。当一条消息初次消费失败,消息队列会自动进行消息重试;达到最大重试次数后,若消费依然失败,则表明消费者在正常情况下无法正确地消费该消息,此时,消息队列不会立刻将消息丢弃,而是将其发送到该消费者对应的特殊队列中。
RocketMQ
将这种正常情况下无法被消费的消息称为死信消息(Dead-Letter Message
),将存储死信消息的特殊队列称为死信队列(Dead-Letter Queue
)。在RocketMQ
中,可以通过使用console
控制台对死信队列中的消息进行重发来使得消费者实例再次进行消费。
2.10 流量控制
生产者流控,因为broker
处理能力达到瓶颈;消费者流控,因为消费能力达到瓶颈。
生产者流控:
commitLog
文件被锁时间超过osPageCacheBusyTimeOutMills
时,参数默认为1000ms,返回流控。- 如果开启
transientStorePoolEnable == true
,且broker
为异步刷盘的主机,且transientStorePool
中资源不足,拒绝当前send
请求,返回流控。 broker
每隔10ms检查send
请求队列头部请求的等待时间,如果超过waitTimeMillsInSendQueue
,默认200ms,拒绝当前send
请求,返回流控。broker
通过拒绝send
请求方式实现流量控制。
注意,生产者流控,不会尝试消息重投。
消费者流控:
- 消费者本地缓存消息数超过
pullThresholdForQueue
时,默认1000
。 - 消费者本地缓存消息大小超过
pullThresholdSizeForQueue
时,默认100MB
。 - 消费者本地缓存消息跨度超过
consumeConcurrentlyMaxSpan
时,默认2000
。
消费者流控的结果是降低拉取频率。
三、消息存储
消息存储是RocketMQ
中最为复杂和最为重要的一部分,本节将分别从RocketMQ
的消息存储整体架构、PageCache
与Mmap内存映射以及RocketMQ
中两种不同的刷盘方式三方面来分别展开叙述。RocketMQ
底层优化:顺序写、异步刷、零拷贝
3.1 消息存储整体架构
消息存储架构图中主要有下面三个跟消息存储相关的文件构成。
- CommitLog:消息主体以及元数据的存储主体,存储
Producer
端写入的消息主体内容,消息内容不是定长的。单个文件大小默认1G
,文件名长度为20
位,左边补零,剩余为起始偏移量,比如00000000000000000000
代表了第一个文件,起始偏移量为0
,文件大小为1G
=1073741824
;当第一个文件写满了,第二个文件为00000000001073741824
,起始偏移量为1073741824
,以此类推。消息主要是顺序写入日志文件,当文件满了,写入下一个文件; - 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
; - 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
(索引文件)数据。
3.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.3 消息刷盘
- 同步刷盘:如上图所示,只有在消息真正持久化至磁盘后
RocketMQ
的Broker
端才会真正返回给Producer
端一个成功的ACK
响应。同步刷盘对MQ
消息可靠性来说是一种不错的保障,但是性能上会有较大影响,一般适用于金融业务应用该模式较多。 - 异步刷盘:能够充分利用
OS
的PageCache
的优势,只要消息写入PageCache
即可将成功的ACK
返回给Producer
端。消息刷盘采用后台异步线程提交的方式进行,降低了读写延迟,提高了MQ
的性能和吞吐量。
四、通信机制
RocketMQ
消息队列集群主要包括NameServer
、Broker
(Master
/Slave
)、Producer
、Consumer
4个角色,基本通讯流程如下:
Broker
启动后需要完成一次将自己注册至NameServer
的操作;随后每隔30s
时间定时向NameServer
上报Topic
路由信息。- 消息生产者
Producer
作为客户端发送消息时候,需要根据消息的Topic
从本地缓存的TopicPublishInfoTable
获取路由信息。如果没有则更新路由信息会从NameServer
上重新拉取,同时Producer
会默认每隔30s
向NameServer
拉取一次路由信息。 - 消息生产者
Producer
根据2
)中获取的路由信息选择一个队列(MessageQueue
)进行消息发送;Broker
作为消息的接收者接收消息并落盘存储。 - 消息消费者
Consumer
根据2
)中获取的路由信息,并再完成客户端的负载均衡后,选择其中的某一个或者某几个消息队列来拉取消息并进行消费。
从上面1~3中可以看出在消息生产者,Broker
和NameServer
之间都会发生通信(这里只说了MQ
的部分通信),因此如何设计一个良好的网络通信模块在MQ
中至关重要,它将决定RocketMQ
集群整体的消息传输能力与最终的性能。
rocketmq-remoting
模块是RocketMQ
消息队列中负责网络通信的模块,它几乎被其他所有需要网络通信的模块(诸如rocketmq-client
、rocketmq-broker
、rocketmq-namesrv
)所依赖和引用。为了实现客户端与服务器之间高效的数据请求与接收,RocketMQ
消息队列自定义了通信协议并在Netty
的基础之上扩展了通信模块。
4.1 Remoting通信类结构
- RemotingService是最上层的接口,定义了三个方法
void start();
void shutdown();
void registerRPCHook(RPCHook rpcHook);
- RemotingServer:定义了服务端的接口,继承了上层接口
RemotingService
- RemotingClient:定义了客户端的接口,继承了上层
RemotingService
RemotingServer
与RemotingClient
定义的方法是类似的,主要包含了同步、异步、oneway
方式的通信和注册处理器processor
,其余的就是针对服务端和客户端特定的接口方法,比如服务端根据requestCode
获取处理器的getProcessorPair()
方法,客户端获取NameServer
地址列表getNameServerAddressList()
方法。
- NettyRemotingAbstract:
Netty
通信抽象类,定义并封装了服务端与客户端公共方法。这个也是RocketMQ
网络通信的核心类。 - NettyRemotingServer:服务端的实现类,实现了
RemotingServer
接口,继承NettyRemotingAbstract
抽象类。 - NettyRemotingClient:客户端的实现类,实现类
RemotingClient
接口,继承NettyRemotingAbstract
抽象类。
4.2 协议设计与编解码
在Client
和Server
之间完成一次消息发送时,需要对发送的消息进行一个协议约定,因此就有必要自定义RocketMQ
的消息协议。同时,为了高效地在网络中传输消息和对收到的消息读取,就需要对消息进行编解码。在RocketMQ
中,RemotingCommand
这个类在消息传输过程中对所有数据内容的封装,不但包含了所有的数据结构,还包含了编码解码操作。
Header字段 | 类型 | Request说明 | Response说明 |
---|---|---|---|
code |
int |
请求操作码,应答方根据不同的请求码进行 不同的业务处理 |
应答响应码。0 表示成功,非0 则表示各种错误 |
language |
LanguageCode |
请求方实现的语言 | 应答方实现的语言 |
version |
int |
请求方程序的版本 | 应答方程序的版本 |
opaque |
int |
相当于requestId ,在同一个连接上的不同请求标识码,与响应消息中的相对应 |
应答不做修改直接返回 |
flag |
int |
区分是普通RPC 还是onewayRPC 的标志 |
区分是普通RPC 还是onewayRPC 的标志 |
remark |
String |
传输自定义文本信息 | 传输自定义文本信息 |
extFields |
HashMap |
请求自定义扩展信息 | 响应自定义扩展信息 |
可见传输内容主要可以分为以下4部分:
- 消息长度:总长度,四个字节存储,占用一个
int
类型; - 序列化类型&消息头长度:同样占用一个
int
类型,第一个字节表示序列化类型,后面三个字节表示消息头长度; - 消息头数据:经过序列化后的消息头数据;
- 消息主体数据:消息主体的二进制字节数据内容;
4.3 消息的通信方式和流程
在RocketMQ
消息队列中支持通信的方式主要有同步(sync
)、异步(async
)、单向(oneway
)三种。其中“单向”通信模式相对简单,一般用在发送心跳包场景下,无需关注其Response
。这里,主要介绍RocketMQ
的异步通信流程。
4.4 Reactor多线程设计
RocketMQ
的RPC
通信采用Netty
组件作为底层通信库,同样也遵循了Reactor
多线程模型,同时又在这之上做了一些扩展和优化。
上面的框图中可以大致了解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”)。从入口到业务逻辑的几个步骤中线程池一直再增加,这跟每一步逻辑复杂性相关,越复杂,需要的并发通道越宽。
线程数 | 线程名 | 线程具体说明 |
---|---|---|
1 | NettyBoss_%d | Reactor主线程 |
N | NettyServerEPOLLSelector_%d_%d | Reactor线程池 |
M1 | NettyServerCodecThread_%d | Worker线程池 |
M2 | RemotingExecutorThread_%d | 业务processor处理线程池 |
五、负载均衡
RocketMQ
中的负载均衡都在Client
端完成,具体来说的话,主要可以分为Producer
端发送消息时候的负载均衡和Consumer
端订阅消息的负载均衡。
5.1 Producer的负载均衡
Producer
端在发送消息的时候,会先根据Topic
找到指定的TopicPublishInfo
,在获取了TopicPublishInfo
路由信息后,RocketMQ
的客户端在默认方式下selectOneMessageQueue()
方法会从TopicPublishInfo
中的messageQueueList
中选择一个队列(MessageQueue
)进行发送消息。具体的容错策略均在MQFaultStrategy
这个类中定义。这里有一个sendLatencyFaultEnable
开关变量,如果开启,在随机递增取模的基础上,再过滤掉not available
的Broker
代理。所谓的"latencyFaultTolerance
",是指对之前失败的,按一定的时间做退避。例如,如果上次请求的latency
超过550Lms
,就退避3000Lms
;超过1000L
,就退避60000L
;如果关闭,采用随机递增取模的方式选择一个队列(MessageQueue
)来发送消息,latencyFaultTolerance
机制是实现消息发送高可用的核心关键所在。
5.2 Consumer的负载均衡
在RocketMQ
中,Consumer
端的两种消费模式(Push/Pull
)都是基于拉模式来获取消息的,而在Push
模式只是对pull
模式的一种封装,其本质实现为消息拉取线程在从服务器拉取到一批消息后,然后提交到消息消费线程池后,又“马不停蹄”的继续向服务器再次尝试拉取消息。如果未拉取到消息,则延迟一下又继续拉取。在两种基于拉模式的消费方式(Push/Pull
)中,均需要Consumer
端在知道从Broker
端的哪一个消息队列—队列中去获取消息。因此,有必要在Consumer
端来做负载均衡,即Broker
端中多个MessageQueue
分配给同一个ConsumerGroup
中的哪些Consumer
消费。
5.2.1 Consumer端的心跳包发送
在Consumer
启动后,它就会通过定时任务不断地向RocketMQ
集群中的所有Broker
实例发送心跳包(其中包含了,消息消费分组名称、订阅关系集合、消息通信模式和客户端id
的值等信息)。Broker
端在收到Consumer
的心跳消息后,会将它维护在ConsumerManager
的本地缓存变量—consumerTable
,同时并将封装后的客户端网络通道信息保存在本地缓存变量—channelInfoTable
中,为之后做Consumer
端的负载均衡提供可以依据的元数据信息。
5.2.2 Consumer端实现负载均衡的核心类 — RebalanceImpl
在Consumer
实例的启动流程中的启动MQClientInstance
实例部分,会完成负载均衡服务线程—RebalanceService
的启动(每隔20s
执行一次)。通过查看源码可以发现,RebalanceService
线程的run()
方法最终调用的是RebalanceImpl
类的rebalanceByTopic()
方法,该方法是实现Consumer
端负载均衡的核心。这里,rebalanceByTopic()
方法会根据消费者通信类型为“广播模式”还是“集群模式”做不同的逻辑处理。这里主要来看下集群模式下的主要处理流程:
- 从
rebalanceImpl
实例的本地缓存变量—topicSubscribeInfoTable
中,获取该Topic
主题下的消息消费队列集合(mqSet
); - 根据
topic
和consumerGroup
为参数调用mQClientFactory.findConsumerIdList()
方法向Broker
端发送获取该消费组下消费者Id
列表的RPC
通信请求(Broker
端基于前面Consumer
端上报的心跳包数据而构建的consumerTable
做出响应返回,业务请求码:GET_CONSUMER_LIST_BY_GROUP
); - 先对
Topic
下的消息消费队列、消费者Id
排序,然后用消息队列分配策略算法(默认为:消息队列的平均分配算法),计算出待拉取的消息队列。这里的平均分配算法,类似于分页的算法,将所有MessageQueue
排好序类似于记录,将所有消费端Consumer
排好序类似页数,并求出每一页需要包含的平均size
和每个页面记录的范围range
,最后遍历整个range
而计算出当前Consumer
端应该分配到的记录(这里即为:MessageQueue
)。
- 然后,调用
updateProcessQueueTableInRebalance()
方法,具体的做法是,先将分配到的消息队列集合(mqSet
)与processQueueTable
做一个过滤比对。
- 上图中
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
类里面的该方法为空,这样子也就回答了上一篇中最后的那道思考题了。
消息消费队列在同一消费组不同消费者之间的负载均衡,其核心设计理念是在一个消息消费队列在同一时间只允许被同一消费组内的一个消费者消费,一个消息消费者能同时消费多个消息队列。
六、消息查询
RocketMQ
支持按照下面两种维度进行消息查询
- 按照
Message Id
查询消息 - 按照
Message Key
查询消息
6.1 按照MessageId查询消息
RocketMQ
中的MessageId
的长度总共有16
字节,其中包含了消息存储主机地址(IP
地址和端口),消息Commit Log offset
。“按照MessageId
查询消息”在RocketMQ
中具体做法是:Client
端从MessageId
中解析出Broker
的地址(IP
地址和端口)和Commit Log
的偏移地址后封装成一个RPC
请求后通过Remoting
通信层发送(业务请求码:VIEW_MESSAGE_BY_ID
)。Broker
端走的是QueryMessageProcessor
,读取消息的过程用其中的commitLog offset
和size
去commitLog
中找到真正的记录并解析成一个完整的消息返回。
6.2 按照Message Key查询消息
“按照Message Key
查询消息”,主要是基于RocketMQ
的IndexFile
索引文件来实现的。RocketMQ
的索引文件逻辑结构,类似JDK
中HashMap
的实现。索引文件的具体结构如下:
IndexFile
索引文件为用户提供通过“按照Message Key
查询消息”的消息索引查询服务,IndexFile
文件的存储位置是:$HOME\store\index\${fileName}
,文件名fileName
是以创建时的时间戳命名的,文件大小是固定的,等于40+500W\*4+2000W\*20= 420000040
个字节大小。如果消息的properties
中设置了UNIQ_KEY
这个属性,就用 topic + “#” + UNIQ_KEY
的value
作为key
来做写入操作。如果消息设置了KEYS
属性(多个KEY
以空格分隔),也会用topic + “#” + KEY
来做索引。
其中的索引数据包含了Key Hash/CommitLog Offset/Timestamp/NextIndex offset
这四个字段,一共20Byte
。NextIndex offset
即前面读出来的slotValue
,如果有hash
冲突,就可以用这个字段将所有冲突的索引用链表的方式串起来了。Timestamp
记录的是消息storeTimestamp
之间的差,并不是一个绝对的时间。整个Index File
的结构如图,40Byte
的Header
用于保存一些总的统计信息,4\*500W
的Slot Table
并不保存真正的索引数据,而是保存每个槽位对应的单向链表的头。20\*2000W
是真正的索引数据,即一个Index File
可以保存2000W
个索引。
“按照Message Key
查询消息”的方式,RocketMQ
的具体做法是,主要通过Broker
端的QueryMessageProcessor
业务处理器来查询,读取消息的过程就是用topic
和key
找到IndexFile
索引文件中的一条记录,根据其中的commitLog offset
从CommitLog
文件中读取消息的实体内容。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek “源神”启动!「GitHub 热点速览」
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
· NetPad:一个.NET开源、跨平台的C#编辑器