一、路由管理
NameServer是RocketMQ的路由中心,负责为Producer和Consumer提供Topic路由信息。NameServer集群各节点是无状态的(相互没有联系),每个节点的路由信息依靠与Broker的心跳来更新,实现路由信息的最终一致性,如下图所示:

路由注册
Broker启动后会定时向NameServer集群各节点发送心跳包,NameServer节点收到心跳包后会更新路由信息,以及Broker的心跳时间戳等信息;
路由删除
NameServer节点会定时扫描缓存中各Broker的心跳时间信息,如果发现超过120s没有收到心跳,则该Broker的路由信息会被移除,同时关闭对应的Sokcet连接;
路由发现
RocketMQ的路由发现是非实时的,当Topic的路由信息发生变更时,NameServer并不会主动推送给Producer和Consumer,而是由Producer和Consumer定时拉取路由信息来更新;(Producer会有容错机制来保障消息发送的高可用性)
二、消息发送
消息发送方式
RocketMQ支持3种消息发送方式:同步(sync)、异步(async)和单向(oneway)。
- 同步:发送消息时,同步等待发送结果;
- 异步:发送消息时,注册回调函数,无需等待发送结果;
- 单向:只管发送,不关心发送结果;
消息队列负载均衡
RocketMQ默认采用RR策略进行负载均衡,大致过程如下:
- 首先,对消息队列进行排序,优先按照BrokerName排序,BrokerName相同时按照消息队列的序号排序;
- 其次,计数器对消息队列取模,每发送一条消息计数器加1;
问题: RR策略存在消息发送高可用的问题,当某个Broker宕机后,向Broker上面所有的队列发送的消息都会失败;
消息发送高可用
RocketMQ实现消息发送高可用的策略为 : 重试与 Broker 故障规避 。 Broker故障规避是指在一次消息发送过程中如果向某个Broker的消息发送失败,则在某一时间段内,Producer不会再向Broker上的消息队列发送消息,提高发送消息的成功率 。
三、消息存储架构
主要存储文件
- CommitLog: 消息存储文件,当Broker接收到消息后(所有Topic的)会顺序写入CommitLog文件(Kafka每个Topic都有独立的数据文件,当Topic数量变多消息持久化逐渐变成一种随机写磁盘的行为)。每个CommitLog文件大小固定为1G,以第一条记录的偏移量作为文件名。(顺序写、随机读)
- ConsumeQueue: 消息队列文件,相当于消息的索引,提升消息消费性能。每个Topic有多个队列/分区,每个分区对应一个ConsumeQueue文件。消息队列文件固定包含30万个记录,每条记录固定大小为20字节,单个文件大小约5.72M,适合加载到内存。(顺序写、顺序读)
- IndexFile: 索引文件,相当于HashMap,用于根据消息的key快速查找消息的场景。单个IndexFile文件有50万个哈希槽位,2000万个哈希记录;

刷盘策略
- 同步刷盘:消息追加到内存映射文件的内存后,立即将数据从内存刷写到磁盘,然后返回ACK;
- 异步刷盘:消息追加到内存映射文件的内存后,立即返回ACK,然后由后台异步线程将数据从内存刷写到磁盘;
文件恢复机制
问题: 由于索引信息是异步的,如果Broker异常宕机,可能导致数据文件与索引文件数据不一致,如下图所示:

解决方案:
- 首先,判断Broker是否异常退出;
Broker启动时会生成abort文件,正常退出时会通过JVM钩子函数销毁abort文件,因此可以在Broker启动时判断abort文件是否存在来判断Broker是否异常退出; - 然后,根据Broker是否异常退出采取不同的方案;
如果是正常退出,说明消息数据都已经刷入磁盘,索引数据也都被更新,数据文件与索引文件是一致的;否则,PageCache中还没刷入磁盘的数据会丢失,数据文件与索引文件也可能不一致,核心思路是取三个文件中刷盘偏移量最小的FlushedPosition,然后CommitLog文件从自此处开始重新构建索引信息;
四、消息消费
负载均衡(消费端)
负载均衡算法: RocketMQ提供了5中负载均衡算法,最常用的是平均分配法。举例来说,如果现在有6 个消息消费队列和2个消费者,消息队列分配如下图所示。消息队列的分配遵循一个原则:一个消息队列只能分配给一个消费者,一个消费者可以分配多个队列,当消费者数量大于消息队列数量时,会有消费者空闲下来。

负载均衡过程:
- 每个消费者有个负载均衡的线程,定时从NameServer获取路由信息;
- 对队列和消费者进行排序;
- 按照设定的负载均衡算法生成新的集合;
- 对比新老集合,停止消费不在新集合中的队列,同时保存消费进度,开始消费新分配的队列;
延迟消息
处于性能考虑,RocketMQ只支持特定级别的延迟消息,延迟消息的Topic名称为SCHEDULE TOPIC XXXX,每个延迟级别对应一个消息队列,如下图所示。核心思想是先将消息加入延迟队列,当时间到达后再写入消息队列进行计时,像普通消息一样消息,具体过程如下:
- 当Broker接收到延迟消息后,首先更改消息Topic名称为SCHEDULE TOPIC XXXX,写入CommitLog文件;
- 然后根据延迟级别追加索引信息到对应的消息队列;
- 每个消息队列有定时任务,当消息到达时间后将消息名称还原,重新写入CommitLog文件,并更新对应的消息队列;
- 消费者拉取消息并消费;

普通消息/并发消费
并发消费也称为乱序消费,相比于顺序消费,并发消费没有加锁/解锁的过程,所以消费速度要快很多,因此是RocketMQ默认的消费方式,具体过程如下:
- 消费端的PullMessageService线程不断从Broker拉取消息(无锁);
- 拉取到的消息存储到ProcessQueue,并创建消费任务(包含消息,不用从ProcessQueue拉取)提交到线程池;
- 消费线程消费完消息后,删除ProcessQueue中对应的消息,并以最小偏移量更新消费进度;

顺序消息
RocketMQ的顺序消息分为2种情况:局部有序和全局有序。
- 局部有序:单个队列内的消息保持有序性,相同Topic的不同队列间是无序的;(实际常用,将需要保持顺序性消息发送到相同队列)
- 全局有序:相同Topic的不同队列间消息保持有序性(通过将队列树设为1实现);
顺序发送原理: 将需要保持顺序的消息同步发送到相同队列即可,如下图所示。
顺序消费原理:
- 消费端从Broker队列拉取消息时,需要先加锁,只有加锁成功才能拉取消息;(防止负载均衡时被其它队列消费)
- 拉取到的消息存储到本地ProcessQueue,按照偏移量从小到大排序,创建消费任务(不包含消息,需从ProcessQueue拉取)提交到线程池;
- 消费线程获取ProcessQueue的锁,消费偏移量最小的消息,然后释放锁;
- 消费成功后更新消费进度到Broker;
将需要保持顺序的消息同步发送到相同队列即可,如下图所示。

说明: 当发生分区变更(扩容、缩容)、Broker宕机、或者主从切换时都会导致发送端将消息路由到其它分区,导致消息短暂的乱序。如果想要保持”严格有序“,则Topic单个分区,同时容忍短暂的不可用(故障恢复)。
事务消息
事务消息的核心原理:两阶段提交 + 超时回调。类似于延迟消息,发送方在阶段一发送Prepare消息后,会更改消息Topic为RMQ_SYS_TRANS_HALF_TOPIC(暂时对消费方不可见)。如果发送方在阶段二Commit,则恢复消息重新存入Commit以及对应的消息队列(对消费者可见);如果Rollback,则删除对应的Prepare消息。RMQ_SYS_TRANS_HALF_TOPIC的消息队列会有定时任务,如果Prepare消息超时没有提交或回滚,则回调发送方进行确认,具体过程如下图所示:


五、架构设计
5.1 高性能
- 对于整个集群,支持水平扩展,可通过增加节点提升集群的存储量和吞吐量;
- 对于Broker,通过磁盘顺序写、索引文件加速读、PageCache读写、异步刷盘、mmap零拷贝,以及文件预热等方式提升单节点性能;文件预热:提前进行commitlog文件的创建和加载,避免消息写入时导致的延迟;
- 对于Producer和Consumer,通过将topic对应的消息队列分散到多个Broker上,支持多个并发的发送和消费消息;
- 底层通信模型基于Netty,IO模型使用NIO;
5.2 高可用
Broker的主备结构实现,Master支持读写,Slave支持读。当主节点挂掉后,对于Consumer,从主切换到从继续读取;对于Producer,通过故障规避机制发送到其他Broker上;
主从同步:主从之间建立连接,从服务器上报未拉取消息偏移量,主服务器推送消息到从服务器;可以分为同步和异步两种;
参考:
934

被折叠的 条评论
为什么被折叠?



