RabbitMQ 普通队列与镜像队列 底层原理

RabbitMQ 底层实现原理




普通MQ的结构

    MQ内部大致又可以分为两部分:amqueue和backing queue,  

    amqqueue负责实现amqp协议规定的mq的基本逻辑,

    backing queue则实现消息的存储,它会尽量为durable=true的消息做持久化的存储,而在内存不足时将一部分消息放入DISK换取更多的内存空间。

Backing queue内部又细分为5各小Q



Alpha:该消息的位置信息和消息本身都在RAM中,这类消息排列在Q1Q4

Beta:消息的位置保存在RAM中,消息本身保存在DISK中,这类消息排列在Q2Q3中。

Gamma: 消息的位置保存RAMDISK中,消息本身保存在DISK中,这类消息排列在Q2Q3中。

Delta:消息的位置和消息本身都保存在DISK中,这类消息排列在delta中。

从Q1->Q2->delta这一个过程是将消息逐步从RAM移动到DISK的过程,而delta->Q3->Q4是从DISK逐步移动到RAM的过程。


引起消息流转有两个因素

1.消费者获取消息

2.内存不足,引起消息换出到磁盘上

 RabbitMQ 在系统运行时会根据消息传输的速度计算一个当前内存中能够保存的最大消息数量( Target_RAM_Count ),当内存中的消息数量大于该值时,就会引起消息的流动。

Q1,Q2, Delta Q3 Q4 几个队列状态流转


Q1 Q4 对应的是 Alpha状态,消息的索引和内容都保存在RAM

Q2 Q3 对应的是Beta 状态,索引在RAM ,消息体在磁盘

Delta 对应的是 Delta状态


当消费者获取消息时

    首先,从Q4获取消息,如果Q4获取成功,则返回。如果没获取成功从Q3获取,

    如果Q3中获取消息为空,那么直接返回 队列为空 ???(为啥,如果Q3为空,可以)

    如果Q3中消息不为空,则取出Q3消息,判断此时 Q3和delta的队列长度,

            如果Q3,delta都为空,则 认为Q2 ,Delta Q4  Q1都为空 ??(如果Q3为空,会从delta取出消息,先消费Q3的,不够从delta上去取Dela上没有会去Q2上去取,Q3为空,说明delta和Q2都被取光了,都为空; 假设Q1不为空,从Q4消费时,没有的话,Q1会流向Q4,再次从Q4取消息), 此时,将Q1中的消息转移到Q4,下次从Q4取消息;

            如果Q3为空,delta不为空,则将Delta消息转移到Q3,RabbitMQ按照分段索引的方式读取,每次读一段,直达读到的消息为空为止,

                    如果读取的消息个数和Delta中消息个数相等,那么认为delta中已经没有消息了。那么将Q2的消息转入到Q3

                    如果不相等,则将次数读取的消息转移到Q3

            如果Q3不为空,那么下次直接从Q3取出消息。


当内存不足,引起消息换出到磁盘

            消息换出的条件是内存中保存的消息数量 + 等待 ACK 的消息的数量 >Target_RAM_Count 

           当条件出发时,判断 等待ack消息的速度,大于进入队列的消息速度,会先处理ack的消息,  Q1向 Q2或者Q3移动, Q4向Q3移动, Q2和Q3向Delta移动

       

            




镜像MQ的结构





mirror queue基本上就是一个特殊的backing queue, 它内部包裹了一个普通的backing queue做本地的消息持久化处理,在此基础上增加了将消息和ack复制到所有镜像的功能。所有对rabbit_mirror_queue_master的操作,会通过组播GMGuarenteed Multicast)的方式同步到各slave节点。

新节点加入

允许新的slave节点中途加入到集群中,新加入的slave节点并不同步master节点的所有在该slave加入之前存在的消息,只对新来的消息保持同步,随着旧的消息被消费,经过一段时间后,slave节点就会与master节点完全同步。

节点失效

master节点失效后,所有slave中消息队列最长者会成为新的master,因为这样的节点最有可能与原来的master节点完全同步。

节点重启

当一个节点无论master还是slave失效后重启,都会丢弃本地记录在disk中的所有消息,作为一个全新的slave节点加入到集群中去。

GM

GM模块实现的一种可靠的组播通讯协议,该协议能够保证组播消息的原子性,即保证组中活着的节点要么都收到消息要么都收不到。它的实现大致如下:

一个组的所有成员组成一个ring,例如 A->B->C->D->A。假如Amaster节点,A要发组播消息,A首先会将消息发送到A的后继节点BB接收到消息后在传递给C然后是D,最后D再发给A。在此过程中若有节点失效,发送节点就会往失效的节点的后继节点发消息,若后继节点也失效就往后继的后继发消息。当A收到传回来的消息时,A就可以确认所有“活着的”节点都已收到该消息,但此时BCD并不能确认所有节点都收到了该消息,所以不能往上提交该消息。这时,ABAck,当B收到ack后就能确认所有的节点都收到该消息,B将该ack继续传递给CD最终又传回A,至此整个发送过程就完成了。若最终A没有收到ack,则说明此次发送失败。



使用RabbitMQ HA存在的问题及可能的解决方法

(1) master节点失效时,slave节点可能并未与mater节点完全同步造成消息丢失。

首先定位到rabbit_mirror_queue_master中消息的处理的入口函数

publish(Msg = #basic_message { id = MsgId }, MsgProps, ChPid,

        State = #state { gm                  = GM,

                         seen_status         = SS,

                         backing_queue       = BQ,

                         backing_queue_state = BQS }) ->

    false = dict:is_key(MsgId, SS), %% ASSERTION

    ok = gm:broadcast(GM, {publish, false, ChPid, MsgProps, Msg}),

    BQS1 = BQ:publish(Msg, MsgProps, ChPid, BQS),

    ensure_monitoring(ChPid, State #state { backing_queue_state = BQS1 }).

第一句Gm:broadcast做的是将新消息组播到所有slave节点,第二句BQ:publish的功能是做本地持久化的处理,这里BQ是普通的BQ。对于durable=true的消息,只有当消息持久化成功后即进入gamma阶段才会向sender发确认。所以新消息的处理过程大致可以归纳为:

a. 向所有slave节点发送该消息

b. 持久化

c. 发送acksender

根据gm.erl中对broadcast的描述“This is a cast and the function call will return immediately. Thereis no guarantee that the message will reach any member of the group.”

该函数是异步的,所以就有可能出现 slave并未接收到该消息,此时又向sender发了确认,接着master失效,这条消息就丢失了。

解决这个问题的可以采用同步组播方式向所有slave节点发消息,即确认所有的slave都已收到了该消息才向sender确认。目前gm.erl代码中已经提供了同步发消息的函数gm:confirmed_broadcast/2。从前面对gm算法的描述可以看出,这样做的坏处是会增加处理延时,slave越多,时延越大。

(2) mater节点失效时,ack消息可能并未同步到所有节点,造成消息的重复发送。

为保证消息不丢,接收者在收到消息后,需要向MQackMQ收到ack后才会删除该消息。但由于ack的发送是异步的,将该ack消息组播到每个slave节点也是异步的,所以在mater节点失效时,ack消息可能并未同步到所有节点,造成消息的重复发送。

为解决这一问题,初步有两种思路:第一种,去重。第二种,更改接收者回ack的逻辑,改为同步的发送,保证所有的节点都以收到该ack,才算ack成功。









猜你喜欢

转载自blog.csdn.net/wangming520liwei/article/details/79893763