RabbitMQ08-消息存储和镜像队列

1、存储机制

  • 持久化消息和非持久化消息都可以被写入到磁盘。
    • 持久化消息一到达队列就会被写入磁盘。持久化消息会尽可能地保存在内存中,这样可以提高一定的性能,但当内存吃紧时会从内存中清除。
    • 非持久化消息一般只会保存在内存中,但在内存吃紧时也会被写入到磁盘中,以节省内存空间。
  • 将持久化消息和非持久化消息存储到磁盘中,是由RabbitMQ的"持久层"完成。
  • 持久层是一个逻辑上的概念,包含两个部分:队列索引(rabbit_queue_index)和消息存储(rabbit_msg_store)。
    • rabbit_queue_index:负责维护存储到磁盘上的消息在内存中的元数据,包括消息的存储地点、是否己被交付给消费者、是否己被消费者ack等。每个队列都有一个与之对应的rabbit_queue_index
    • rabbit_msg_store:负责将消息以键值对的形式存储到磁盘上,它被所有队列共享在每个节点中有且只有一个
  • 从技术层面上看,rabbit_msg_store可以分为msg_store_persistent和msg_store_transient。
    • msg_store_persistent:负责持久化消息的持久化,重启后消息不会丢失。
    • msg_store_transient:负责非持久化消息的持久化,重启后消息会丢失。
]# ls -l /apps/rabbitmq/var/lib/rabbitmq/mnesia/rabbit@hh14/msg_stores/vhosts/628WB79CIFDYO9LJI6DKMI09L/
drwxr-xr-x 2 root root   19 7月  11 09:28 msg_store_persistent
drwxr-xr-x 2 root root   19 7月  11 09:28 msg_store_transient
-rw-r--r-- 1 root root 5464 7月  11 09:28 recovery.dets
  • RabbitMQ会将较小的消息交予rabbit_queue_index处理(存储到内存中),将较大的消息交予rabbit_msg_store处理(存储到磁盘上)
    • 当消息大小(消息体、属性及headers的整体大小)小于queue_index_embed_msgs_below参数设置的值时,会将消息交予rabbit_queue_index处理。queue_index_embed_msgs_below的默认大小为4096,单位为B。
  • rabbit_queue_index在内存中至少有一个段文件。每个段文件中包含16384条消息的记录。因此,如果增加queue_index_embed_msgs_below一定要小心,小幅的增加就可能会导致内存爆炸式的增长。
  • rabbit_msg_store处理的所有消息都会以追加的方式写入到文件中,当一个文件的大小超过指定的限制(msg_store_file_size_limit)后,就会关闭这个文件再创建一个新的文件以供新的消息写入。文件名(文件后缀是".rdq")从0开始进行累加,因此文件名最小的文件也是最老的文件。
    • 在进行消息的存储时,RabbitMQ会在ETS(Erlang Term Storage)表中记录消息在文件中的位置映射(Index)和文件的相关信息(FileSummary)。
    • 在读取消息的时候,先根据消息的ID(msg_id)找到对应的存储文件,如果文件存在并且未被锁住,则直接打开文件,从指定位置读取消息的内容。如果文件不存在或者被锁住了,则发送请求由rabbit_msg_store进行处理。
  • 删除消息只是从ETS表删除指定消息的相关信息,同时更新消息对应的存储文件的相关信息。执行消息删除操作时,并不立即对在文件中的消息进行删除,也就是说消息依然在文件中,仅仅是标记为垃圾数据而己。
    • 当一个文件中都是垃圾数据时可以将这个文件删除。
    • 当检测到前后两个文件中的有效数据可以合并在一个文件中,并且所有的垃圾数据的大小和所有文件(至少有3个文件存在的情况下)的数据大小的比值超过设置的阈值GARBAGE FRACTION(默认值为0.5)时才会触发垃圾回收将两个文件合并。
  • 执行合井的两个文件一定是逻辑上相邻的两个文件。执行合并时首先锁定这两个文件,并先对前面文件中的有效数据进行整理,再将后面文件的有效数据写入到前面的文件,同时更新消息在ETS表中的记录,最后删除后面的文件。

1.1、队列的结构

  • 队列由rabbit_amqqueue_process和backing_queue两部分组成
    • rabbit_amqqueue_proces负责消息的处理,即接收生产者发布的消息、向消费者交付消息、处理消息的确认(包括生产端的confirm和消费端的ack))等。
    • backing_queue是消息存储的具体形式和引擎,并向rabbit_amqqueue_process提供相关的接口以供调用。

1.1.1、队列的状态

  • 如果消息投递的目的队列是空的,并且有消费者订阅了这个队列,那么消息会直接发送给消费者,不会存储到队列中。当消息无法直接投递给消费者时,才需要将消息存入队列中。消息存入队列后,它会随着系统的负载在队列中不断地流动,即消息的状态在不断地发生变化。
  • 消息在队列可能会处于以下4种状态:
    • alpha:消息索引和消息(包括消息体、属性和headers)都存储在内存中。
    • beta:消息索引存储在内存中,消息存储在磁盘中。
    • gamma:消息索引存储在内存和磁盘中,消息存储在磁盘中。(只有持久化消息才会有该状态)
    • delta:消息索引和消息都存储在磁盘中。
  • 消息有4种状态主要作用是可以满足不同内存和CPU的需求。
    • alpha状态虽然最消耗内存,但很少消耗CPU。
    • delta状态基本不消耗内存,但是需要消耗更多的CPU和磁盘IO操作。
    • delta状态需要执行两次IO操作才能读取到消息,一次是读消息索引(从rabbit_queue_index中),一次是读消息(从rabbit_msg_store中)。
    • beta和gamma状态都只需要一次IO操作就可以读取到消息(从rabbit_msg_store中)。
  • 持久化消息的消息索引和消息都必须先存储在磁盘上,才会处于4种状态之一。
  • RabbitMQ会定期根据消息传送的速度计算出一个当前内存中能够保存的最大消息数量(target_ram_count) 。当内存中的消息数量大于该值时,就会引起消息的状态转换。会将部分消息从alpha状态转换到beta、gamma或者delta状态。

1.1.2、队列的架构

  • 对于普通的没有设置优先级和镜像的队列来说,backing_queue的默认实现是rabbit_variable_queue,其内部通过5个子队列Q1、Q2、Delta、Q3和Q4来体现消息的各个状态。
  • 如图9-2所示,整个队列的架构包括rabbit_amqqueue_process和backing_queue的各个子队列。
    • Q1:消息索引和消息都在内存中。(只包含alpha状态的消息)
    • Q2:消息索引在内存中,消息在磁盘中。(包含beta和gamma状态的消息)
    • Delta:消息索引和消息都在磁盘中。(只包含delta状态的消息)
    • Q3:消息索引在内存中,消息在磁盘中。(包含beta和gamma状态的消息)
    • Q4:消息索引和消息都在内存中。(只包含alpha状态的消息)

1.1.3、消息的状态转换

  • 一般情况下,消息会在Q1 -> Q2 -> Delta -> Q3 -> Q4中依次流动,但并不是每一条消息都会经历所有的状态,这个取决于当前系统的负载状况。
    • 消息从Q1到Q4基本上经历了从内存到磁盘,再从磁盘回到内存中
    • 如此可以在队列负载很高的情况下,能够通过将一部分消息转储到磁盘中来节省内存空间,而在负载降低的时候,又逐渐将消息加载到内存中,然后被消费者获取,使得整个队列具有很好的弹性。
  • 消费者消费消息也会引起消息的状态转换
    • 消费者会先从Q4中获取消息,如果获取成功则返回。如果Q4为空,则尝试从Q3中获取消息。
    • 从Q3中获取消息:
      • 先判断Q3是否为空,如果为空则返回队列为空,即此时队列中无消息。
      • 如果Q3不为空,则从Q3中获取消息。此时会判断Q3和Delta中的长度:
        • 如果Q3和Delta都为空,则可以认为Q2、Delta、Q3、Q4全部为空,然后将Q1中的消息直接转移至Q4,下次会直接从Q4中获取消息。
        • 如果Q3为空,Delta不为空,则将Delta的消息转移至Q3中,下次可以直接从Q3中获取消息。
  • 在将消息从Delta转移到Q3的过程中,是按照索引分段读取的,首先读取某一段,然后判断读取的消息的个数与Delta中消息的个数是否相等:
    • 如果相等,则可以判定Delta中己无消息,则直接将Q2和刚读取到的消息一并放入到Q3中。
    • 如果不相等,仅将此次读取到的消息转移到Q3。
  • 为什么Q3为空则可以认为整个队列为空?
    • 如果Q3为空,Delta不为空,那么在Q3取出最后一条消息的时候,Delta上的消息就会被转移到Q3,这样与Q3为空矛盾;
    • 如果Delta为空且Q2不为空,则在Q3取出最后一条消息时会将Q2的消息并入到Q3中,这样也与Q3为空矛盾;
    • 在Q3取出最后一条消息之后,如果Q2、Delta、Q3都为空,且Q1不为空时,则将Q1中的消息转移到Q4,这与Q4为空矛盾。
  • 如果消息被消费的速度不小于接收新消息的速度,对于不需要保证可靠不丢失的消息来说,极有可能只会处于alpha状态。对于durable属性设置为true的消息,它一定会进入gamma状态,并且在开启publisher confmn机制时,只有到了gamma状态时才会确认该消息己被接收,若消息消费速度足够快、内存也充足,这些消息也不会继续走到下一个状态。
  • 在系统负载较高时,己接收到的消息若不能很快被消费掉,这些消息就会进入到很深的队列中去,这样会增加每个消息被处理的平均开销。因为要花更多的时间和资源处理"堆积"的消息,如此用来处理新流入的消息的能力就会降低,使得后流入的消息又被积压到很深的队列中继续增大处理每个消息的平均开销,继而情况变得越来越恶化,使得系统的处理能力大大降低。
  • 降低消息"堆积"一般有3种方法:
    • (1)增加prefetch_count的值,即一次发送多条消息给消费者,加快消息被消费的速度。
    • (2)采用multiple ack,降低处理ack带来的开销。
    • (3)流量控制。

1.2、惰性队列

  • RabbitMQ从3.6.0版本开始引入了惰性队列( Lazy Queue)的概念。
  • 惰性队列会尽可能地将消息存储到磁盘中,在消费者消费到相应的消息时才会被加载到内存中。
  • 惰性队列的主要作用就是能够支持更长的队列,即支持更多的消息存储。当消费者由于各种各样的原因(比如消费者下线、岩机或者由于维护而关闭等)致使长时间内不能消费消息而造成堆积时,惰性队列就很有必要了。
  • 队列有两种模式:default和lazy。默认为default模式,在3.6.0之前的版本无须做任何变更。
    • default模式,当生产者将消息发送到RabbitMQ的时候,队列中的消息会尽可能地存储在内存之中,这样可以快速地将消息发送给消费者。即使是持久化消息,在被写入磁盘的同时也会在内存保留一份。当RabbitMQ需要释放内存的时候,会将内存中的消息转存到磁盘中,这个操作会耗费较长的时间,也会阻塞队列的操作,进而无法接收新的消息。
    • lazy模式即为惰性队列的模式,可以通过调用channel.queueDeclare方法的时候在参数中设置,也可以通过Policy的方式设置,如果一个队列同时使用这两种方式设置,那么只有Policy方式设置的会生效。
    • 如果要通过声明的方式改变己有队列的模式,那么只能先删除队列,然后再重新声明一个新的。
  • 惰性队列会将接收到的消息直接存储到磁盘中,而不管是持久化的还是非持久化的,这样可以减少了内存的消耗,但是会增加IO操作。
    • 如果消息是持久化的,那么IO操作不可避免,惰性队列和持久化消息可谓是"最佳拍档"。
    • 如果消息是非持久化的,内存的使用率会一直很稳定,但是重启之后消息一样会丢失。
  • 惰性队列和普通队列相比,只有很小的内存开销。

2、内存和磁盘告警

  • 内存已用空间或磁盘剩余空间到达配置的阈值时:
    • RabbitMQ都会暂时阻塞客户端的连接(Connection),并停止接收从生产者发来的消息,以此避免服务崩溃。
    • 客户端与服务端的心跳检测也会失效。
  • 被阻塞的Connection的状态要么是blocking,要么是blocked。
    • blocking对应于并不试图发送消息的Connection,比如消费者关联的Connection,这种状态下的Connection可以继续运行。
    • blocked对应于一直有消息发送的Connection,这种状态下的Connection会被停止发送消息。
    • 注意,在一个集群中,如果一个Broker节点的内存或者磁盘受限,会引起整个集群中所有的Connection被阻塞。
  • 理想的情况是当发生阻塞时可以在阻止生产者的同时而又不影响消费者的运行。但是在AMQP协议中,一个信道(Channel)上可以同时承载生产者和消费者,同一个Connection中也可以同时承载若干个生产者的信道和消费者的信道,这样就会使阻塞逻辑错乱,虽然大多数情况下并不会发生任何问题,但还是建议生产和消费的逻辑分别使用独立的Connection,而不发生任何交集。

2.1、内存告警

  • RabbitMQ服务器会在启动或者执行rabbitmqctl set_vm_memory_high_watermark命令时计算系统内存的大小。vm_memory_high_watermark的默认值为0.4,即内存阈值为0.4,表示当RabbitMQ使用的内存超过40%时,就会产生内存告警并阻塞所有生产者的连接。一旦告警被解除(有消息被消费或者从内存转储到磁盘等情况的发生),一切都会恢复正常。
  • 默认情况下RabbitMQ的内存的阈值为40%,但这并不意味着此时RabbitMQ不能使用超过40%的内存,这仅仅只是限制了RabbitMQ的生产者。在最坏的情况下,Erlang的垃圾回收机制会导致两倍的内存消耗,也就是80%的使用占比。
  • 内存阈值可以通过rabbitmq.conf配置文件来配置。
//使用百分比的形式设置内存阈值(取值应该在0.4-0.66之间,不建议超过0.7。)
vm_memory_high_watermark.relative = 0.4
//使用绝对值的形式设置内存阈值,默认单位为B(也可以是KB、MB、GB)
vm_memory_high_watermark.absolute = 2GB
  • 内存阈值可以通过rabbitmqctl set_vm_memory_high_watermark命令设置,但在服务器重启之后对阈值的设置就会失效
//fraction是大于等于0,小于等于1的浮点数
rabbitmqctl set_vm_memory_high_watermark <fraction>
//value默认单位为B(也可以是KB、MB、GB)
rabbitmqctl set_vm_memory_high_watermark absolute <value>
  • 默认情况下,在内存到达内存阈值的50%时会进行换页动作。也就是说,在默认的内存阈值为0.4的情况下,当内存的使用率超过0.4*0.5=0.2时会进行换页动作。
    • 可以在配置文件中使用vm_memory_high_watermark_paging_ratio参数进行修改,换页比率默认是0.5。
    • 可以将vm_memory_high_watermark_paging_ratio设置为大于1的浮点数,这种配置相当于禁用了换页功能。
//在内存到达内存阈值的50%时会进行换页动作
vm_memory_high_watermark_paging_ratio = 0.5

2.2、磁盘告警

  • 当磁盘可用空间低于给定的阈值时,RabbitMQ会阻塞生产者,这样可以避免消息持续换页而耗尽磁盘空间导致服务崩溃。
  • 默认情况下,磁阈值为50MB,这意味着当磁盘可用空间低于50MB时会阻塞生产者并停止内存中消息的换页动作。
  • 一个相对谨慎的做法是将磁盘的阈值设置为与操作系统的内存大小一样。
  • RabbitMQ会定期检测磁盘剩余空间,检测的频率与上一次执行检测到的磁盘剩余空间大小有关。正常情况下,每10秒执行一次检测,随着磁盘剩余空间与磁盘阈值的接近,检测频率会有所增加。当要到达磁盘阈值时,检测频率为每秒10次,这样有可能会增加系统的负载。
  • 磁盘阈值可以通过rabbitmq.conf配置文件来配置。
//参考机器内存的大小为磁盘阈值设置一个相对的比值(建议值是1.0-2.0)
disk_free_limit.relative = 1.0
//使用绝对值的形式设置磁盘阈值,默认单位为B(也可以是KB、MB、GB)
disk_free_limit.absolute = 2GB
  • 磁盘阈值可以通过rabbitmqctl set_disk_free_limit命令设置,但在服务器重启之后对阈值的设置就会失效
//disk_limit默认单位为B(也可以是KB、MB、GB)
rabbitmqctl set_disk_free_limit <disk_limit>
//fraction是机器内存与磁盘阈值的比值
rabbitmqctl set_disk_free_limit mem_relative <fraction>

3、流控

  • RabbitMQ可以对内存和磁盘使用量设置阈值,当达到阈值后,生产者将被阻塞,直到对应项恢复正常。除了这两个阈值,从2.8.0版本开始,RabbitMQ还引入了流控(Flow Control)机制来确保稳定性。
  • 流控机制是用来避免消息的发送速率过快而导致服务器难以支撑的情形。
    • 内存和磁盘告警相当于全局的流控(Global Flow Control),一旦触发会阻塞集群中所有的Connection。
    • 流控是针对单个Connection的,可以称之为Per-Connection Flow Control或者Intemal Flow Control。

3.1、流控的原理

  • Erlang进程之间并不共享内存(binary类型的除外),而是通过消息传递来通信,每个进程都有自己的进程邮箱(mailbox)。
    • 默认情况下,Erlang并没有对进程邮箱的大小进行限制,所以当有大量消息持续发往某个进程时,会导致该进程邮箱过大,最终内存溢出并崩溃。
    • 如果生产者持续高速发送消息,而消费者消费速度较低,而且RabbitMQ没有流控,很快就会使内部进程邮箱的大小达到内存阀值。
  • RabbitMQ使用了一种基于信用证算法(credit-based algorithm)的流控机制来限制发送消息的速率以解决前面所提出的问题。它通过监控各个进程的进程邮箱,当某个进程负载过高而来不及处理消息时,这个进程的进程邮箱就会开始堆积消息。当该进程中的消息堆积到一定量时,就会阻塞而不接收上游的新消息。从而慢慢地,上游进程的进程邮箱也会开始堆积消息。当上游进程中的消息堆积到一定量时也会阻塞而停止接收上游的消息,最后就会使负责网络数据包接收的进程阻塞而暂停接收新的数据。
  • 如图9-4所示,进程A接收消息并转发至进程B,进程B接收消息并转发至进程C。每个进程中都有一对关于收发消息的credit值。
    • 以进程B为例,{{credit_from, C}, value}表示能发送多少条消息给C,每发送一条消息该值减1,当为0时,进程B不再往进程C发送消息也不再接收进程A的消息。
    • {{credit_to, A}, value}表示再接收多少条消息就向进程A发送增加credit值的通知,进程A接收到该通知后就增加{{credit_from, B}, value}所对应的值,这样进程A就能持续发送消息。
  • 当上游发送速率高于下游接收速率时,credit值就会被逐渐耗光,这时进程就会被阻塞,阻塞的情况会一直传递到最上游。当上游进程收到来自下游进程的增加credit值的通知时,若此时上游进程处于阻塞状态则解除阻塞,开始接收更上游进程的消息,一个一个传导最终能够解除最上游的阻塞状态。由此可知,基于信用证的流控机制最终将消息发送进程的发送速率限制在消息处理进程的处理能力范围之内。

  • 一个连接(Connection)触发流控时会处于"flow"的状态,也就意味着这个Connection的状态每秒在blocked和unblocked之间来回切换数次,这样可以将消息发送的速率控制在服务器能够支撑的范围之内。
  • 处于flow状态的Connection和处于running状态的Connection并没有什么不同,这个状态只是告诉系统管理员相应的发送速率受限了。而对于客户端而言,它看到的只是服务器的带宽要比正常情况下要小一些。
  • 流控机制不只是作用于Connection,也可以作用于信道(Channel)和队列。从Connection到Channel,再到队列,最后是消息持久化存储形成一个完整的流控链,对于处于整个流控链中的任意进程,只要该进程阻塞,上游的进程必定全部被阻塞。也就是说,如果某个进程达到性能瓶颈,必然会导致上游所有的进程被阻塞。所以我们可以利用流控机制的这个特点找出瓶颈之所在
  • 处理消息的几个关键进程及其对应的顺序关系如图9-6 所示。
    • rabbit_reader:Connection的处理进程,负责接收、解析AMQP协议数据包等。
    • rabbit_channel:Channel的处理进程,负责处理AMQP协议的各种方法、进行路由解析等。
    • rabbit_amqqueue_process:队列的处理进程,负责实现队列的所有逻辑。
    • rabbit_msg_store:负责实现消息的持久化。

  • 当某个Connection处于flow状态,但这个Connection中没有一个Channel处于flow状态时,这就意味这个Connection中有一个或者多个Channel出现了性能瓶颈。某些Channel进程的运作(比如处理路由逻辑)会使得服务器CPU 的负载过高从而导致了此种情形。尤其是在发送大量较小的非持久化消息时,此种情形最易显现。
  • 当某个Connection处于flow状态,并且这个Connection中也有若干个Channel处于flow状态,但没有任何一个对应的队列处于flow状态时,这就意味着有一个或者多个队列出现了性能瓶颈。这可能是由于将消息存入队列的过程中引起服务器CPU负载过高,或者是将队列中的消息存入磁盘的过程中引起服务器IO负载过高而引起的此种情形。尤其是在发送大量较小的持久化消息时,此种情形最易显现。
  • 当某个Connection处于flow状态,同时这个Connection中也有若干个Channel处于flow状态,并且也有若干个对应的队列处于flow状态时,这就意味着在消息持久化时出现了性能瓶颈。在将队列中的消息存入磁盘的过程中引起服务器IO负载过高而引起的此种情形。尤其是在发送大量较大的持久化消息时,此种情形最易显现。

3.2、打破队列的瓶颈

  • 图9-6中描绘了一条消息从接收到存储的一个必需的流控连。一般情况下,向一个队列里推送消息时,往往会在rabbit_amqqueue_process中(即队列进程中)产生性能瓶颈。在向一个队列中快速发送消息的时候,Connection和Channel都会处于flow状态,而队列处于running状态,分析可以得出在队列进程中产生性能瓶颈。
  • 如何提升队列的性能?一般可以有两种解决方案:
    • (1)开启Erlang语言的HiPE功能,这样保守估计可以提高30%~40%的性能,不过在较旧版本的Erlang中,这个功能不太稳定,建议使用较新版本的Erlang,版本至少是18.x。
    • (2)寻求打破rabbit_amqqueue_process的性能瓶颈(开发代码实现)。这里的打破是指以多个rabbit_amqqueue_process替换单个rabbit_amqqueue_process,这样可以充分利用上rabbit_reader或者rabbit_channel进程中被流控的性能,如图9-7所示。

4、镜像队列

4.1、镜像队列的基础

  • 避免单点故障导致的服务不可用。
    • 如果RabbitMQ集群中只有一个Broker节点,那么该节点的失效将导致整体服务的临时性不可用,并且也可能会导致消息的丢失。就算将所有消息都设置为持久化,仍然无法避免由于缓存导致的问题:因为消息在发送之后和被写入磁盘之间会在机器的内存中停留一段时间。通过publisher confirm机制能够确保消息写入磁盘。
    • 如果RabbitMQ集群是由多个Broker节点组成的,对于单点故障是有弹性的,但是只有交换器和绑定关系在其他每个Broker节点都有备份,而队列和其上的存储的消息在其他每个Broker节点都没有备份,这是因为队列进程及其内容仅在一个节点之上,所以一个节点的失效表现为其对应的队列不可用。
  • 镜像队列(Mirror Queue):可以将队列镜像到集群中的其他Broker节点之上,如果集群中的一个节点失效了,队列能自动地切换到另一个有该队列镜像的节点上以保证服务的可用性。
  • 镜像队列架构如图9-11所示,每一个配置镜像的队列(以下简称镜像队列)都包含一个主节点(master)和若干个从节点(slave)。

  • slave会准确地按照master执行命令的顺序进行动作,故slave与master上维护的状态应该是相同的。
    • 如果master由于某种原因失效,那么"资历最老"的slave会被提升为新的master。根据slave加入的时间排序,时间最长的slave即为"资历最老"。
    • 发送到镜像队列的所有消息会被同时发往master和所有的slave上,如果此时master挂掉了,消息还会在slave上,这样slave提升为master的时候消息也不会丢失。
    • 除发送消息(Basic.Publish)外的所有动作都只会发送给master,然后再由master将命令执行广播给各个slave。
  • 如果消费者与slave建立连接并进行订阅消费,其实质上都是从master上获取消息,只不过看似是从slave上消费而己。比如消费者与slave建立了TCP连接之后执行一个Basic.Get的操作,那么首先是由slave将Basic.Get请求发往master,再由master准备好数据返回给slave,最后由slave投递给消费者。
  • 大多的读写压力都落到了master上,那么这样是否负载会做不到有效的均衡?
    • 注意这里的master和slave是针对队列而言的,而队列已经均匀地分散在集群的各个Broker节点上用来达到负载均衡的目的。
  • 如图9-12所示,集群中的每个Broker节点都包含1个队列的master和2个队列的slave。Q1的负载大多都集中在broker1上,Q2的负载大多都集中在broker2上,Q3的负载大多都集中在broker3上,只要确保队列的master节点均匀散落在集群中的各个Broker节点即可确保很大程度上的负载均衡(每个队列的流量会有不同,因此均匀散落各个队列的master也无法确保绝对的负载均衡)。像MySQ1一样读写分离,RabbitMQ从编程逻辑上来说完全可以实现,但是这样得不到更好的收益,即读写分离并不能进一步优化负载,却会增加编码实现的复杂度,增加出错的可能,显得得不偿失。

  • RabbitMQ的镜像队列支持publisher confirm和事务两种机制。
    • 在事务机制中,只有当前事务在全部镜像中执行之后,客户端才会收到Tx.Commit-Ok的消息。
    • 在publisher confirm机制中,生产者进行当前消息确认的前提是该消息被全部进行所接收了。

4.2、镜像队列原理

  • 镜像队列的backing_queue不但实现了本地消息消息持久化处理(backing_queue的默认实现是rabbit_variable_queue),在此基础上增加了将消息和ack复制到所有镜像的功能:
    • master的backing_queue实现是rabbit_mirror_queue_master。
    • slave的backing_queue实现是rabbit_mirror_queue_slave。
  • 镜像队列的结构如图9-13所示:

  • 所有对rabbit_mirror_queue_master的操作都会通过组播GM(Guaranteed Multicast)的方式同步到各个slave中。
    • 除了Basic.Publish,所有的操作都是通过master来完成的,master对消息进行处理的同时会将消息的处理通过GM广播给所有的slave,slave的GM收到消息后,通过回调交由rabbit_mirror_queue_slave进行实际的处理。
    • master上的回调处理是由coordinator负责完成的。
  • GM模块实现的是一种可靠的组播通信协议,该协议能够保证组播消息的原子性,即保证组中活着的节点要么都收到消息要么都收不到,它的实现大致为:将所有的节点形成一个循环链表,每个节点都会监控位于自己左右两边的节点。
    • 当有节点新增时,相邻的节点保证当前广播的消息会复制到新的节点上。
    • 当有节点失效时,相邻的节点会接管以保证本次广播的消息会复制到所有的节点。
  • 在master和slave的GM形成一个组(gm_group),这个组的信息会记录在Mnesia中。不同的镜像队列形成不同的组
    • 操作命令从master对应的GM发出后,顺着链表传送到所有的节点。
    • 由于所有节点组成了一个循环链表,master对应的GM最终会收到自己发送的操作命令,这个时候master就知道该操作命令都同步到了所有的slave上。
  • 新节点的加入过程如图9-14所示,整个过程就像在链表中间插入一个节点。注意每当一个节点加入或者重新加入到这个镜像链路中时,之前队列保存的内容会被全部清空。

  • 当slave挂掉之后,除了与slave相连的客户端连接全部断开,没有其他影响。
  • 当master挂掉之后,会有以下连锁反应:
    • (1)与master连接的客户端连接全部断开。
    • (2)选举最老的slave作为新的master,因为最老的slave与旧的master之间的同步状态应该是最好的。如果此时所有slave处于未同步状态,则未同步的消息会丢失。
    • (3)新的master重新入队所有unack的消息,因为新的master无法区分这些unack的消息是否己经到达客户端,或者是ack信息丢失在老的master链路上,再或者是丢失在老的master组播ack消息到所有slave的链路上,所以出于消息可靠性的考虑,重新入队所有unack的消息,不过此时客户端可能会有重复消息。
    • (4)如果客户端连接着slave,并且Basic.Consume消费时指定了x-cancel-on-hafailover参数,那么断开之时客户端会收到一个Consumer Cancellation Notification的通知,消费者客户端中会回调Consumer接口的handleCancel方法。如果未指定x-cancelon-ha -failover参数,那么消费者将无法感知master岩机。

4.3、镜像队列使用

  • 镜像队列是通过Policy进行配置的:
rabbitmqctl set_policy [--vhost <vhost>] [--priority <priority>] [--apply-to <apply-to>] <name> <pattern> <definition>
    配置镜像队列时,definition包含3个部分:ha-mode、ha-params和ha-sync-mode。
        ha-mode:指明镜像队列的模式,有效值为all、exactly、nodes,默认为all。
            all表示在集群中所有的节点上进行镜像。
            exactly表示在指定个数的节点上进行镜像,节点个数由ha-params指定。
            nodes表示在指定节点上进行镜像,节点名称通过ha-params指定,节点的名称通常类似于rabbit@hostname。
        ha-params:不同的ha-mode配置中需要用到的参数。
        ha-sync-mode:队列中消息的同步方式,有效值为automatic和manual。
  • ha-mode参数对排他(exclusive)队列并不生效,因为排他队列是连接独占的,当连接断开时队列会自动删除,所以实际上这个参数对排他队列没有任何意义。
  • ha-sync-mode对新节点加入己存在的镜像队列时的影响:
    • 默认情况下ha-sync-mode取值为manual,镜像队列中的消息不会主动同步到新的slave中,除非显式调用同步命令。当调用同步命令后,队列开始阻塞,无法对其进行其他操作,直到同步完成。
    • 当ha-sync-mode设置为automatic时,新加入的slave会默认同步己知的镜像队列。
    • 由于同步过程的限制,所以不建议对生产环境中正在使用的队列进行操作
//查看哪些slaves已经完成同步
rabbitmqctl list_queues {name} slave_pids synchronised_slave_pids
//通过手动方式同步一个队列
rabbitmqctl sync_queue {name}
//取消某个队列的同步
rabbitmqctl cancel_sync_queue {name}
  • 当所有slave都出现未同步状态,并且ha-prornote-on-shutdown设置为when-synced(默认)时,如果master 因为主动原因停掉,比如通过rabbitmqctl stop命令或者优雅关闭操作系统,那么slave不会接管master,也就是此时镜像队列不可用;但是如果master 因为被动原因停掉,比如Erlang虚拟机或者操作系统崩溃,那么slave会接管master。这个配置项隐含的价值取向是保证消息可靠不丢失,同时放弃了可用性。
  • 如果ha-prornote-on-shutdown设置为always,那么不论master因为何种原因停止,slave都会接管master,优先保证可用性,不过消息可能会丢失。
  • 镜像队列中最后一个停止的节点会是master,启动顺序必须是master先启动。如果slave先启动,它会有30秒的等待时间,等待master的启动,然后加入到集群中。如果30秒内master没有启动,slave会自动停止。当所有节点因故(断电等)同时离线时,每个节点都认为自己不是最后一个停止的节点,要恢复镜像队列,可以尝试在30秒内启动所有节点。

4.4、镜像队列示例

4.4.1、创建vhost、exchange、queue和binding

1、创建vhost

2、创建exchange

3、创建queue

4、创建binding

4.4.2、给队列配置镜像

  • 或是使用rabbitmqctl命令配置policy。
rabbitmqctl set_policy --vhost test1_vhost --priority 0 --apply-to queues mirror_queue_test1 "^test.*" '{"ha-mode":"exactly","ha-params":2,"ha-sync-mode":"automatic" }'

4.4.3、查看是否创建成功

1、产看Policy

2、查看queue

  • 可以看到Node上多“+1”的标记。

  • 查看队列的详情

4.4.4、测试

1、向队列中发送一个消息

2、停止rabbit@hh12节点

  • 停止rabbit@hh12节点后,查看该队列的详情。可以看到rabbit@hh13成为了master,rabbit@hh14上被添加了该队列的镜像。

3、获取消息

posted @ 2022-07-11 22:11  麦恒  阅读(157)  评论(0编辑  收藏  举报