RabbitMQ--基础--02--原理
RabbitMQ–基础–02–原理
1、架构
1.1、整体架构
- 整体架构上是一个生产者与消费者模型
- 主要功能:负责接收、存储、转发消息
1.2、核心架构
1.2.1、Broker(服务节点)
- 是rabbitmq的server节点
- 功能:接受客户端的连接
1.2.1.1、消费数据的流程
- 生产者将消息存入 RabbitMQ 中的 Broker
- 消费者从 Broker 中消费数据
1.2.2、Virtual Host(虚拟服务器,简称为vhost)
- 一个Broker中可以有多个vhost
- 每一个vhost本质上是一个独立的小型RabbitMQ服务器,拥有自己独立的队列、交换器及绑定关系等,并且它拥有自己独立的权限
- vhost 就像是虚拟机与物理服务器一样,它们在各个实例间提供逻辑上的分离,为不同程序安全保密地运行数据,它既能将同一个RabbitMQ 中的众多客户区分开,又可以避免队列和交换器等命名冲突
- vhost之间是绝对隔离的,无法将vhost1中的交换器与vhost2中的队列进行绑定,这样既保证了安全性,又可以确保可移植性。如果在使用RabbitMQ达到一定规模的时候,建议用户对业务功能、场景进行归类区分,并为之分配独立的vhost。
- 类比:
- RabbitMq 相当于 MySQL,
- vhost 相当于 MySQL 中的一个数据库
1.2.3、Connection(连接)
- 用于应用程序生产者/消费者与 Broker 的 网络TCP/IP连接
- Connection可以有多个
1.2.4、Channel(网络信道)
- 多路复用连接中的一条独立的双向数据流通道
- 几乎所有的操作都在 Channel 中进行
- Channel 是进行消息读写的通道,每个通道 Channel 代表一个会话任务
- 信道是建立在真实的TCP连接内地虚拟链接,AMQP命令都是通过信道发出去的,不管是发布消息、订阅队列还是接收消息,这些动作都是通过信道完成的
- 作用:为了复用一条TCP连接
1.2.5、Exchange(交换机)
- 接收并转发消息
- 不具备储存消息的能力
- 生产者将消息发送到交换机 Exchange,由交换机 Exchange 将消息路由到一个或者多个队列中。
- 如果路由不到
- 或许会返回给生产者
- 或许会直接丢弃。
- 如果路由不到
- RabbitMQ 中的交换机有四种类型
- Exchange一定是要有的,如果没写的话,则是会使用默认的交换机。
1.2.6、RoutingKey(路由键)
- 交换机可以用它来确定如何路由一个特定消息。
- 生产者将消息发给交换机的时候,一般会指定一个 RoutingKey,用来指定这个消息的路由规则
- RoutingKey 需要与交换机类型(Exchange),绑定建(BindingKey)联合使用才能最终生效。
- 在交换机类型(Exchange),绑定建(BindingKey)固定的情况下,生产者发送消息给交换机时,通过指定路由键(RoutingKey)来决定消息流向哪里。
1.2.7、Binding(绑定)
- 将交换机(Exchange)与队列 Queue关联起来。
- 在绑定的时候,一般会指定一个绑定键 BindingKey,这样,RabbitMQ 就知道如何正确地将消息路由到队列了。
- 生产者将消息发送给交换机(Exchange)时,需要一个RoutingKey(路由键),当 BindingKey 和 RoutingKey 相匹配时,消息会被路由到对应的队列中。
- 在绑定多个队列到同一个交换机的时候,这些绑定允许使用相同的 BindingKey。
- BindingKey并不是在所有的情况下都生效,它依赖于交换机类型。
- 如:fanout类型的交换机就会无视BindingKey,而是将消息路由到所有绑定到该交换机的队列中。
1.2.8、Queue(队列)
- 保存消息,并转发给消费者
- 消息一直在队列里面,等待消费者连接到这个队列并将它取走
1.2.9、Message(消息)
- 应用的消息数据
- 由消息头和消息体组成
- 消息头(Properties):由一系列的可选属性组成,这些属性包括如下参数
- routing-key (路由键)
- priority (优先级)
- delivery-mode (消息可能需要持久性存储[消息的路由模式])
- 消息体(body:是不透明的
1.2.10、Producer(Publisher,消息生产者)
- 消息生产者,也是一个向交换器发布消息的客户端应用程序
- 将消息发送到RabbitMQ的服务节点上
1.2.11、Consumer(消息消费者)
- 消息消费者
- 表示从一个消息队列中取得消息的客户端应用程序
2、架构相关
2.1、消息存储
2.1.1、RabbitMQ对于queue中的message的保存方式
- disc(硬盘)
- ram(内存)
2.1.2、disc(硬盘)
采用disc,则需要对exchange,queue,delivery mode 设置成durable模式。
好处:当RabbitMQ失效了,message仍然可以在重启之后恢复。
2.1.3、ram(内存)
- 处理message的效率比:
- ram:disc=3:1
- 如果有其它HA手段保障的情况下,选用ram方式是可以提高消息队列的工作效率的。
- 使用ram方式,RabbitMQ能够承载的访问量则取决于可用的内存大小。
2.1.4、消息文件
- 所有队列中的消息都以append的方式写到一个文件中,当这个文件的大小超过指定的限制大小后,关闭这个文件再创建一个新的文件供消息的写入。
- 文件名(*.rdq)从0开始然后依次累加。
- 在进行消息的存储时,rabbitmq会在ets表中记录消息在文件中的映射,以及文件的相关信息。消息读取时,根据消息ID找到该消息所存储的文件,在文件中的偏移量,然后打开文件进行读取。
2.1.5、rabbitmq在启动时会创建以下两个进程
- msg_store_persistent进程:用于持久消息的存储
- msg_store_transient进程:用于内存不够时,将存储在内存中的非持久化数据转存到磁盘中
- 队列消息的写入和删除
- 由这两个进程负责处理
- 消息的读取
- 可能是队列本身直接打开文件进行读取
- 可能是发送请求由msg_store_persisteng/msg_store_transient进程进行处理。
2.1.6、Ets数据结构
-record(msg_location,{ msg_id, //消息ID
ref_count, //引用计数
file, //消息存储的文件名
offset, //消息在文件中的偏移量
total_size //消息的大小
}).
2.1.7、日志文件数据结构
-record(file_summary,{ file, //文件名
valid_total_size, //文件有效数据大小
left, //位于该文件左边的文件
right, //位于该文件右边的文件
file_size, //文件总的大小
locked, //上锁标记 垃圾回收时防止对文件进行操作
readers //当前读文件的队列数
})
2.2、GC过程
2.2.1、消息的删除
- 从flying_ets表删除指定消息的相关信息
- 更新 消息对应存储的文件 的相关信息
- 更新 文件有效数据 大小。
2.2.2、垃圾回收时机
- 当垃圾数据超过一定比例后,rabbitmq触发垃圾回收。
- 默认比例:40%
2.2.3、垃圾回收流程
上面X表示无效数据。
步骤1、找到符合要求的两个文件
根据 #file_summary{} 中left,right找逻辑上相邻的两个文件,并且两个文件的有效数据可在一个文件中存储
步骤2、整理两个文件
- 先锁定这两个文件
- 再对 左边文件 的 有效数据 进行整理
- 再将 右边文件 的 有效数据 写入到左边文件
步骤3、更新消息
- 更新消息的相关信息
- 消息存储的文件名
- 消息在文件中的偏移量
- 更新文件的相关信息
- 文件的有效数据
- 左边文件
- 右边文件
步骤4、删除文件
将右边的文件删除。
2.3、性能考虑
2.3.1、操作引用计数(flying_ets)
- 在进行消息的写入和删除操作前,会在flying_ets表里通过+1、-1的方式进行计数
- 然后投递请求给msg_store_persistent、msg_store_transient进程进行处理,进程在真正写操作或者删除之前会再次判断flying_ets中对应消息的计数,决定是否需要进行相应操作。
- 好处:
- 对于频繁写入和删除的操作,减少实际的写入和删除次数。
2.3.2、尽可能的并发读
- 在读取消息的时候,都先根据消息ID找到对应存储的文件。
- 如果存储文件存在 且 没被锁住
- 则直接打开文件,从指定位置读取消息的内容。
- 如果存储文件不存在 或者 被锁住
- 则发送请求,由msg_store_persistent/msg_store_transient进程进行处理。
- 如果存储文件存在 且 没被锁住
2.3.3、利用flying_ets表进行缓存
- 对于当前正在写的文件,所有消息在写入前都会在cur_file_cache_ets表中存一份
- 消息读取时会优先从cur_file_cache_ets表查找。
- 文件关闭时,会将cur_file_cache_ets表中引用计数为0的消息进行清除。
2.3.4、利用file_handle_cache的写缓存
- rabbitmq中对文件的操作转到了file_handle_cache模块(缓存模块),写模式打开文件时
- 进行文件的写操作,是先写入到这个缓存中,当缓存超过缓存空间大小或者显式刷新,才将缓存中的内容刷入磁盘中。
- file_handle_cache默认有1M的缓存空间
2.4、流控
2.4.1、阈值
- RabbitMQ可以对 内存和磁盘使用量 设置阈值,当达到阈值后,生产者将被阻塞(block),直到对应项恢复正常。
- 阈值有2个
2.4.2、没有流控 引发的 问题
Erlang进程之间并不共享内存(binaries类型除外),而是通过消息传递来通信,每个进程都有自己的进程邮箱。
Erlang默认没有对进程邮箱大小设限制,所以当有大量消息持续发往某个进程时,会导致该进程邮箱过大,最终内存溢出并崩溃。
在RabbitMQ中,如果生产者持续高速发送,而消费者消费速度较低时,如果没有流控,很快就会使内部进程邮箱大小达到内存阈值,阻塞生产者(得益于block机制,并不会崩溃)。然后RabbitMQ会进行page操作,将内存中的数据持久化到磁盘中。
为了解决该问题,RabbitMQ使用了一种基于信用证的流控机制。用流控(Flow Control)机制来确保稳定性。
2.4.3、基于信用证的流控机制
- 消息处理进程有一个信用组{InitialCredit,MoreCreditAfter},默认值为{200, 50}。
- 消息发送者(进程A) 向 接收者(进程B)发消息,每发一条消息,Credit数量减1,直到为0,A被block住
- 对于接收者(进程B),每接收MoreCreditAfter条消息,会向A发送一条消息,给予A MoreCreditAfter个Credit,当A的Credit>0时,A可以继续向B发送消息。
2.4.3.1、RabbitMQ生产消息传输路径
- 从上可以看出,基于信用证的流控 最终将 消息发送进程 的 发送速度 限制在 消息处理进程 的 处理速度内。R
- abbitMQ中与流控有关的进程构成了一个有向无环图。
3、MQ队列详解–普通队列
3.1、普通队列的结构
3.2、队列由两部分组成
- AMQQueue:负责AMQP协议相关的消息处理,具体如下
- 接收生产者发布的消息
- 向消费者投递消息
- 处理消息confirm、acknowledge等等
- BackingQueue:
- 提供了相关的接口供AMQQueue调用
- 完成消息的存储以及可能的持久化工作等。
3.3、多层队列结构的设计
- 上图的BackingQueue由5个子队列组成:Q1, Q2,Delta,Q3,Q4。
- RabbitMQ中的消息一旦进入队列,不是固定不变的,它会随着系统的负载在队列中不断流动,消息不断发生变化。
- 在BackingQueue中,消息的生命周期分为4个状态
- Alpha:消息的内容和消息索引都在RAM中。Q1和Q4的状态。
- Beta:消息的内容保存在DISK上,消息索引保存在RAM中。Q2和Q3的状态。
- Gamma:消息内容保存在DISK上,消息索引在DISK和RAM都有。Q2和Q3的状态。
- Delta:消息内容和索引都在DISK上。Delta的状态。
上述就是RabbitMQ的多层队列结构的设计,我们可以看出从Q1到Q4,基本经历RAM->DISK->RAM这样的过程。
3.4、多层队列结构设计的好处
- 当队列负载很高的情况下,能够通过将一部分消息由磁盘保存来节省内存空间
- 当负载降低的时候,这部分消息又渐渐回到内存,被消费者获取,使得整个队列具有很好的弹性。
3.5、消息队列的工作流程
3.5.1、引起消息流动的主要2个因素
- 消费者获取消息
- 由于内存不足,引起消息从内存 转移到 磁盘。
3.5.2、消息流动
- RabbitMQ 根据 消息传输的速度,计算一个当前内存中能够保存的 最大消息数量(Target_RAM_Count)
- 当内存中的消息数量 > Target_RAM_Count,就会引起消息流动。
- 进入队列的消息,一般会按照Q1->Q2->Delta->Q3->Q4的顺序进行流动,但是并不是每条消息都一定会经历所有的状态,这个取决于当前系统的负载状况。
3.5.3、消费者获取消息 引起的消息流动过程
步骤1、从Q4中获取消息
- 当消费者获取消息时,首先会从Q4队列中获取消息
- 如果Q4非空,则返回,到这里流程就结束。
- 如果Q4为空,从Q3获取消息,执行步骤2
步骤2、从Q3中获取消息
- 如果Q3为空,即此时队列中无消息(后续会论证)。
- 如果Q3非空,则取出Q3的消息
- 判断此时Q3和Delta队列的长度
- 如果都为空
- 则可认为Q2、Delta、Q3、Q4全部为空(后续会论证),此时将Q1中消息直接转移到Q4中,下次直接从Q4中获取消息
- 如果Q3为空,Delta不为空
- 则将Delta转移到Q3中
- 如果Q3不为空
- 则直接下次从Q3中获取消息
- 如果都为空
- 判断此时Q3和Delta队列的长度
- Delta转移到Q3的过程
- RabbitMQ是按照索引分段读取的,首先读取某一段,直到读到的消息非空为止,然后判断读取的消息个数与Delta中的消息个数是否相等
- 如果相等,则断定此时Delta中已无消息,则直接将Q2和刚读到的消息一并放入Q3中
- 如果不相等,则仅将此次读取到的消息转移到Q3。
- RabbitMQ是按照索引分段读取的,首先读取某一段,直到读到的消息非空为止,然后判断读取的消息个数与Delta中的消息个数是否相等
3.5.4、消息换出的条件
- 消息换出的条件是内存中保存的消息数量+等待ACK的消息的数量>Target_RAM_Count。
- 当条件触发时,系统首先会判断如果当前进入等待ACK的消息的速度大于进入队列的消息的速度时,会先处理等待ACK的消息。
3.5.5、为什么Q3队列为空即可以认定整个队列为空?
- 试想如果Q3为空,Delta不空,则在Q3取出最后一条消息时,则会将Delta的消息转移到Q3,与Q3空矛盾。
- 结论:Q3为空,Delta一定为空
- 如果Q2不空,则在Q3取出最后一条消息时,因为Delta为空,则会将Q2的消息转移到Q3,与Q3为空矛盾
- 结论:Q3为空,Q2一定为空
- 如果Q1为空,则在Q3取出最后一条消息时,因为Delta和Q2均为空,则将Q1的消息转移到Q4中,与Q4为空矛盾。
- Q3为空,Q1一定为空
总结
- Q3和Delta为空,Q2就为空
- Q3为空,整个队列为空
3.5.6、消息的生命周期变化的场景
3.5.6.1、负载正常情况
消息被消费的速度 大于等于 接收新消息的速度,对于不需要保证可靠不丢的消息极可能只会有Alpha状态。
对于durable=true的消息,它一定会进入gamma状态,若开启publish confirm机制,只有到了这个阶段才会确认该消息已经被接受,若消息消费速度足够快,内存也充足,这些消息也不会继续走到下一状态。
3.5.6.2、负载较高情况
消息被消费的速度 小于 接收新消息的速度,这些消息就会进入到很深的队列中去,增加处理每个消息的平均开销。
因为要花更多的时间和资源处理"积压"的消息,所以用于处理新来的消息的能力就会降低,使得后来的消息又被积压进入很深的队列,继续加大处理每个消息的平均开销,这样情况就会越来越恶化,使得系统的处理能力大大降低。
根据官网资料,应对这一问题,有三个措施:
1. 进行流量控制。
2. 增加prefetch的值,即一次发送多个消息给接收者,加快消息被消费掉的速度。
3. 采用multipleack,降低处理ack带来的开销。
4、MQ队列详解–镜像队列
4.1、镜像队列的结构
- 镜像队列就是一个特殊的BackingQueue
- 内部包裹了一个普通的BackingQueue做本地消息持久化处理,在此基础上增加了将消息、ack复制到所有镜像的功能。
- 所有对mirror_queue_master的操作,会通过组播GM的方式同步到各slave节点。
4.1.1、GM
负责消息的广播
4.1.2、mirror_queue_slave
- 负责回调处理
- mirror_queue_slave中包含了普通的BackingQueue,进行消息的存储
4.1.3、coordinator
负责master上的回调处理
4.1.4、mirror_queue_master
- mirror_queue_master中包含了BackingQueue,进行消息的存储
- BackingQueue由AMQQueue进行调用。
4.1.4、master节点
- 完成 消息的发布(除了Basic.Publish之外)
- 完成 消息的消费
- master节点对消息进行处理的同时将消息的处理动作通过GM广播给所有的slave节点,slave节点的GM收到消息后,通过回调交由mirror_queue_slave进行实际的处理。
4.1.4、master节点
对于Basic.Publish,消息同时发送到master和所有slave上,如果此时master宕掉了,消息还发送slave上,这样当slave提升为master的时候消息也不会丢失。
4.2、组播(GM,Guarenteed Multicast)
GM模块实现的一种可靠的组播通讯协议,该协议能够保证组播消息的原子性,即保证组中活着的节点要么都收到消息要么都收不到。
4.2.1、组播通讯协议的实现
- 将所有的节点形成一个循环链表,每个节点都会监控位于自己左右两边的节点
- 当有节点新增时,相邻的节点保证当前广播的消息会复制到新的节点上;
- 当有节点失效时,相邻的节点会接管保证本次广播的消息会复制到所有的节点。
- 在master节点和slave节点上的这些gm形成一个group,group(gm_group)的信息会记录在mnesia中。
- 不同的镜像队列形成不同的group。
- 从master节点发出gm后,消息顺着链表依次传送到所有的节点,由于所有节点组成一个循环链表,master节点对应的gm最终会收到自己发送的消息,这个时候master节点就知道消息已经复制到所有的slave节点了。
4.3、节点的失效
- 如果某个slave失效了,系统除了做些记录外几乎啥都不做
- master依旧是master,客户端不需要采取任何行动,或者被通知slave失效。
- 如果master失效了,那么slave中的一个必须被选中为master。
- 被选中作为新master的slave通常是最老的那个,因为最老的slave与前任master之间的同步状态应该是最好的。
- 注意:如果存在没有任何一个slave与master完全同步的情况,那么前任master中未被同步的消息将会丢失。
4.4、消息的同步
4.4.1、ha-sync-mode=manual(默认)
- 当新节点加入已存在的镜像队列,镜像队列中的消息不会主动同步到新节点
- 只有显式调用同步命令,镜像队列中的消息才回主动同步到新节点
- 当调用同步命令后,队列开始阻塞,无法对其进行操作,直到同步完毕。
4.4.1、ha-sync-mode=automatic(默认)
- 当新节点加入已存在的镜像队列,镜像队列中的消息会自动同步到新节点
- 由于同步过程的限制,所以不建议在生产的active队列(有生产消费消息)中操作。
4.4.3、命令
# 查看那些slaves已经完成同步
rabbitmqctl list_queues
# 手动的方式同步一个queue
rabbitmqctl sync_queue name
# 取消某个queue的同步功能
rabbitmqctl cancel_sync_queue name
5、 集群原理
5.1、基本概念
5.1.1、镜像队列(Mirrored Queue)
- 默认情况,集群的队列只存在单一节点上
- 可以把队列配置为同时存在在多个节点上,也就是说队列可以被镜像到多个节点上。
- 发布(publish)到镜像队列上的消息(message)会被复制(replicated)到所有的节点上。
- 一个镜像队列包含一个主(master)和多个从(slave)。
5.1.2、非同步的Slave(unsynchronised slave)
在rabbitmq中同步(synchronised)是用来描述master和slave之间的数据状态是否一致的。
- 如果slave包含master中的所有message,则这个slave是synchronised。
- 如果slave没有包含master中所有的message,则这个slave是unsynchronised。
5.1.3、在什么情况下会出现 unsynchronised slave?
5.1.3.1、场景1
当一个新slave加入到一个镜像队列时,这时这个新slave是空的,而master中这时可能包含之前接收到的消息。假设这时master包含了N条消息,当第N+1条消息被添加到这个镜像队列中,这个新slave会从这个第N+1条消息开始接收。
- 此时这个slave就是unsynchronised slave。
- 随着前N条消息从镜像队列中被消费掉(consumed), 这个slave变成了synchronised。
5.1.3.2、场景2
- slave 重新加入(rejoin)到镜像队列时,也会出现非同步的情况。
- 一个slave要重新加入镜像队列之前,slave可能已经接收了一些消息,要重新加入镜像队列,就要清空自己之前已经接收的所有消息,好像自己是第一次加入队列一样。
- slave需要重新加入镜像队列原因,举例如下
- 网络分区(networkpartition)
5.2、服务可用性(Availablity)与数据可靠性(Reliability)
5.2.1、选主方式
由RabbitMQ的slave加入和重新加入队列的方式,我们得出一个结论,越早加入队列的slave,越有更大的机会是同步状态的,所以RabbitMQ通过下面方式选主
- 当master因为某种原因失效时,最早加入镜像队列的slave被提升成master。
5.2.2、RabbitMQ的可用性(Availablity)和数据可靠性(Reliability)
RabbitMQ通过参数配置的方式,在可用性和数据可靠性做出了一定的权衡。
5.2.2.1、参数:ha-sync-mode
- 这个参数是可用性和可靠性的一个平衡
- 值:
- munual(默认值)
- automatic
5.2.2.1.1、ha-sync-mode=munual
- 是默认值
- 不保证数据可靠性,在某些情况会出现丢消息的可能,但是保证了队列的可用性。
- 当一个slave加入和重新加入队列时的时候,我们可以通过命令手工(munual)同步。
# 手工(munual)同步命令
rabbitmqctl sync_queue name
5.2.2.1.2、ha-sync-mode=automatic
当一个新slave加入时,slave会自动同步master中的所有消息,在所有消息被同步完成之前,所有的操作都会被阻塞(blocking),会出现队列的暂时不可用。
5.2.2.2、参数:ha-promote-on-shutdown
- 用来控制选主的行为
- 这个参数是可用性和可靠性的一个平衡
- 值
- when-synced
- always
ha-promote-on-shutdown=when-synced
- 是默认值
- 在可控的master关闭时(比如停止RabbitMQ服务或者关闭操作系统),RabbitMQ会拒绝把一个非同步的slave提升成新的master。
- 只有在非可控的master关闭时(比如server crash, 断网),才会故障恢复到一个非同步的slave。
- 可靠性更好,可用性降低了,因为如果所有的slave都是非同步状态,那就没有符合条件的slave可以被提升成master,这时队列就处在不可用状态。
ha-promote-on-shutdown=always
在所有情况下,master关闭时,RabbitMQ可以把一个非同步的slave提升成新的master。
5.3、集群原理
5.3.1、单节点
- 节点的失效将导致整个服务临时性的不可用
- 可能会导致message的丢失,以下场景就会发生
- 在非持久化message存储于非持久化queue中的时候
5.3.1.1、防止消息丢失设置
- 将所有的publish的message都设置为持久化
- 将queue设置为持久化
- 以上的配置,仍然会丢失消息,只是概率很低
5.3.1.2、为什么 以上的配置,仍然会丢失消息
- 如果 生产者 发送消息之后,且被同步到磁盘之前,MQ服务挂了,这个时候消息就会丢失了。
- 可以通过publisher的confirm机制能够确保客户端知道哪些message已经存入磁盘
5.3.2、普通队列
- 如果RabbitMQ集群是由多个broker节点构成的,那么从服务的整体可用性上来讲,该集群对于单点失效是有弹性的
- 注意:
- 尽管exchange和binding能够在单点失效问题上幸免于难,但是queue和其上持有的message却不行,这是因为queue及其内容仅仅存储于单个节点之上,所以一个节点的失效表现为其对应的queue不可用。
5.3.3、镜像队列
RabbitMQ的镜像队列机制是将queue镜像到cluster中其他的节点之上。
在通常的用法中,针对每一个镜像队列都包含一个master和多个slave,分别位于于不同的节点。slave会准确地按照master执行命令的顺序进行命令执行,故slave与master上维护的状态应该是相同的。
所有动作都只会向master发送,然后由master将命令执行的结果广播给slave们,故看似从镜像队列中的 消费操作 实际上是在master上执行的。
在该实现下,如果镜像队列中的一个master失效了,集群自动选出一个slave(最老的slave)提升为master,此后message可以继续发送到队列上。
5.3.3.1、镜像队列 支持 publisher confirm和事务 两种机制
- 在事务机制中,只有当前事务在全部镜像queue中执行之后,客户端才会收到Tx.Commit Ok的消息。
- 在publisher confirm机制中,只有message被全部镜像所接受了,才 向publisher 进行 当前message的确认。
6、集群补充要点
6.1、镜像队列不能作为负载均衡使用
因为每个操作在所有节点都要做一遍。
6.2、ha-mode参数和durable declare参数 对exclusive队列都不生效
因为exclusive队列是连接独占的,当连接断开,队列自动删除。所以实际上这两个参数对exclusive队列没有意义。
6.3、当所有slave都处在(与master)未同步状态时,并且ha-promote-on-shutdown=when-synced
6.3.1、如果master因为主动的原因停掉
比如是通过rabbitmqctl stop命令停止或者优雅关闭OS,那么slave不会接管master,也就是说此时镜像队列不可用
6.3.2、如果master因为被动原因停掉
- 比如 VM 或者OS crash了,那么slave会接管master。
- 这个配置项隐含的价值取向是优先保证消息可靠不丢失,放弃可用性。
6.4、当所有slave都处在(与master)未同步状态时,并且ha-promote-on-shutdown=when-alway
不论master因为何种原因停止,slave都会接管master,优先保证可用性。
6.5、镜像队列节点 启动和停止 顺序
- 镜像队列中最后一个停止的节点会是master
- 启动顺序必须是master先启动
- 如果slave先启动,它会有30s的等待时间,等待master的启动,然后加入cluster中 1. 如果30s内master没有启动,slave会自动停止
- 当所有节点因故(断电等)同时离线时,每个节点都认为自己不是最后一个停止的节点。
- 要恢复镜像队列,可以尝试在30s之内启动所有节点。
6.6、客户端Basic.Publish操作
客户端Basic.Publish操作,消息会同步到所有节点(master和所有slave),如果此时master宕掉了,消息还发送slave上,这样当slave提升为master的时候消息也不会丢失
6.7、客户端其他操作
- 通过master中转,再由master将操作作用于slave。
- 举例:
- 一个Basic.Get操作,假如客户端与slave建立了TCP连接,首先是slave将Basic.Get请求发送至master,由master备好数据,返回至slave,投递给消费者。
6.8、slave宕掉的影响
除了与slave相连的客户端连接全部断开之外,没有其他影响。
6.9、master宕掉的影响
6.9.1、与master相连的客户端连接全部断开
6.9.2、选举最老的slave节点为master
如果此时所有slave处于未同步状态,则未同步部分消息丢失
6.9.3、新的master节点 重新执行 所有unack消息
- 因为 新的master节点 无法区分 这些unack消息的以下情况
- unack消息是否已经到达客户端
- ack消息是否丢失在老的master的链路
- ack消息是否丢失在master组播ack消息到所有slave的链路
- 所以处于消息可靠性的考虑,重新执行 所有unack的消息。此时客户端可能有重复消息
6.9.4、x-cancal-on-ha-failover参数 情况
- 如果客户端连着slave,并且Basic.Consume消费时指定了x-cancel-on-ha-failover参数,那么客户端会受到一个ConsumerCancellation Notification通知,Java SDK中会回调Consumer接口的handleCancel方法,故需覆盖此方法。
- 如果客户端连着slave,如果未指定x-cancal-on-ha-failover参数,那么消费者就无法感知master宕机,会一直等待下去。
7、镜像队列的恢复
7.1、前提
- 两个节点A和B组成 镜像队列
- A是slave
- B是master
7.2、场景1:A先停,B后停
- 方法1:只要先启动B,再启动A即可。
- 方法2:先启动A,再在30s之内启动B即可恢复镜像队列。
- 如果没有在30s内恢复B,那么A自己就停掉自己
7.3、场景2:A,B同时停
该场景下可能是由掉电等原因造成,只需在30s内连续启动A和B即可恢复镜像队列。
7.4、场景3:A先停,B后停,且A无法恢复。
- 先等B起来后,在B节点上调用rabbitmqctl forget_cluster_node A 以解除A的cluster关系
- 再将新的slave节点 加入B
- 以上操作:即可重新恢复镜像队列。
7.5、场景4:A先停,B后停,且B无法恢复
7.5.1、forget_cluster_node支持-offline参数
offline参数允许rabbitmqctl在离线节点上执行forget_cluster_node命令,迫使RabbitMQ在未启动的slave节点中选择一个作为master。
7.5.2、操作
- 当在A节点执行 rabbitmqctl forget_cluster_node-offline B时,RabbitMQ会 模拟一个节点代表A,执行forget_cluster_node命令将B提出cluster,然后A就能正常启动了。
- 最后将新的slave节点加入A即可重新恢复镜像队列
7.1.5、场景5:A先停,B后停,且A和B均无法恢复,但是能得到A或B的磁盘文件
- 先将A或B的数据库文件($RabbitMQ_HOME/var/lib目录中)copy至新节点C的目录下
- 再将C的hostname改成A或者B的hostname。
- 如果拷贝过来的是A节点的磁盘文件,按场景4处理
- 如果拷贝过来的是B节点的磁盘文件,按场景3处理。
- 最后将新的slave节点加入C即可重新恢复镜像队列。
7.1.6、场景6:A先停,B后停,且A和B均无法恢复,且无法得到A和B的磁盘文件
无法处理
8、参考博客
官网文档
http://www.rabbitmq.com
RabbitMQ 队列镜像配置主机挂掉之后自动切换 另一台
http://blog.csdn.net/csethcrm/article/details/53928313
RabbitMQ不同Confirm模式下的性能对比
http://ju.outofmemory.cn/entry/177937