原文链接: http://www.rabbitmq.com/ha.html
什么是镜像队列
默认情况下,rabbitmq集群中的队列内容位于单个节点(声明队列的节点)上。这与交换器和绑定形成了对比,交换器和绑定始终可视为位于所有节点上。队列可以选择性地跨多个节点进行镜像。
每个镜像队列由一个master和一个或多个mirrors组成。主节点位于一个通常称为master的节点上。每个队列都有自己的主节点。给定队列的所有操作首先应用于队列的主节点,然后传播到镜像。这包括队列发布(enqueueing publishes)、向消费者传递消息、跟踪消费者的确认等等。
队列镜像意味着是一个集群内的节点。因此,不建议跨广域网使用它(当然,客户机仍然可以根据需要尽可能近地连接)。
发布到队列的消息将复制到所有镜像。不管消费者连接到哪个节点,都会连接到master,镜像会删除在master上已确认的消息。因此,队列镜像提高了可用性,但不会在节点之间分配负载(所有参与节点都完成所有工作)。
如果承载队列master的节点出现故障,则最旧的镜像将升级为新的master,只要它已同步。根据队列镜像参数,也可以升级未同步的镜像。
在分布式系统中,通常使用多个术语来标识主副本和辅助副本。本指南通常使用“master”来指代队列的主副本,使用“mirror”指代辅助副本。但是,你会发现很多地方都使用了"slave"。这是因为rabbitmq CLI工具在历史上一直使用术语“slave”来指代辅助设备。因此,这两个术语目前都可以互换使用,但我们希望最终摆脱遗留术语。
镜像配置方式
镜像参数是使用Policies配置的。策略按名称(使用正则表达式模式)匹配一个或多个队列,并包含添加到匹配队列的总属性集的定义(可选参数映射)。有关策略的详细信息,请参阅运行时参数和策略。
控制镜像的队列参数
如上所述,队列通过策略启用了镜像。策略可以随时更改;创建一个非镜像队列,然后在以后某个时间点对其进行镜像是有效的(反之亦然)。非镜像队列(non-mirrored queue)和没有镜像的镜像队列(a mirrored queue which doesn't have any mirrors)之间存在差异——前者缺少额外的镜像基础结构,可能会提供更高的吞吐量。
为了创建镜像队列,需要创建一个与队列匹配的策略并设置策略参数ha-mode和ha-params(可选)。下表解释了这些可选键的含义:
ha-mode | ha-params | Result |
---|---|---|
exactly | count | 集群中队列副本数量(master和mirrors之和) count值为1表示单个副本:只有队列主节点。如果运行该master的节点不可用,后续行为依赖于该队列的持久性。 count值为2表示有两个副本:1个master和1个mirror。换句话说,NumberOfQueueMirrors = NumberOfNodes - 1。当该队列master所在的节点不可用,则队列mirror将根据配置的镜像升级策略自动升级为master。 如果集群中的节点少于count个,那么队列将镜像到所有节点。如果群集中节点数多于count,并且一个包含镜像的节点关闭,那么将在另一个节点上创建一个新镜像。档使用exactly模式,并且“ha-promote-on-shutdown”的值为“always”可能很危险,因为队列可以跨集群迁移,并在关闭时变得不同步。 |
all | (none) | 在集群中所有节点上进行镜像,将新节点添加到集群时,队列将镜像到该节点。 这种设置非常保守,建议镜像到集群节点quorum的(N/2 + 1)。镜像到所有节点会给集群中所有节点带来额外的压力,包括网络I/O、磁盘I/O和磁盘空间使用。 |
nodes | node names | 在指定节点上进行镜像,节点名在ha-params中指定。节点名是以rabbitmq cluster_status列出的Erlang节点名,通常格式为“rabbit@hostname"。 如果这些节点名中的任何一个不是集群的一部分也不会构成错误。如果在声明队列时列表中没有任何节点处于联机状态,那么将在声明客户机所连接的节点上创建队列。 |
每当队列的HA策略更改时,它将尽力使其现有镜像与新策略保持一致。
镜像最佳数量
在集群中所有节点上镜像是最保守的选项。这将给所有集群节点带来额外的压力,包括网络io、磁盘io和磁盘空间使用。大多数情况下没有必要在每个节点上建立副本。
对于包含3个或更多节点的集群,建议将其复制到仲裁(大多数)节点,例如3个节点集群中的2个节点或5个节点集群中的3个节点。
由于某些数据可能本质上是暂时的或是对时间非常敏感的,因此对某些队列使用较少数量的镜像(甚至不使用任何镜像)是完全合理的。
查看队列是否为镜像队列
镜像队列将在管理UI的“Queue”页上具有策略名称和旁边的其他副本(镜像)数。
如果队列页面没有列出任何镜像,则队列没有被镜像(或只有一个非在线的镜像)
当新增一个队列日志,该事件会记录在日志中 :
2018-03-01 07:26:33.121 [info] <0.1360.0> Mirrored queue 'two.replicas' in vhost '/': Adding mirror on node hare@warp10: <37324.1148.0>
可使用rabbitmqctl list_queues name policy pid slave_pids列出队列的master和mirrors。
如果预期要镜像的队列没有镜像成功,则通常意味着它的名称与控制镜像的策略中指定的名称不匹配,或者另一个策略具有优先级(并且该策略未启用镜像)。请参阅运行时参数和策略以了解更多信息。
队列master位置
rabbitmq中的每个队列都有一个主节点。该节点称为队列主节点。所有队列操作都首先通过master,然后复制到镜像。这对于保证消息的FIFO排序是必要的。
可以使用多种策略在节点之间分布队列master。使用的策略有三种控制方式,即使用x-queue-master-locator队列声明参数、设置queue-master-locator策略key或通过在配置文件中定义queue_master_locator。以下是可能的策略以及如何设置它们:
选取承载最小绑定master数的节点:min-masters
选择客户端声明队列时连接的那个节点:client-local
随机选择节点:random
节点策略和Masters迁移
请注意,如果新策略中没有列出“节点”策略,则设置或修改该策略可能会导致现有master消失。为了防止消息丢失,rabbitmq将保留现有master,直到至少一个其他镜像已同步(即使这是很长的时间)。但是,一旦发生同步,事情将继续进行,就像节点失败一样:消费者将与主节点断开连接,需要重新连接。
例如,如果一个队列在[A B]上(A是master),并且给它一个节点策略(”nodes“ policy),告诉它在[C D]上,那么它最初将在[A C D]上结束。一旦队列在其新镜像[C D]上同步,A上的主机将关闭。
排他队列的镜像
当声明排他队列的连接关闭时,排他队列会被删除。因此,为排他队列设置镜像没有任何意义。因为当承载排他队列的节点down后,连接将被关闭,排他队列无论如何都会被删除。
因此,排他队列永远不会被镜像(即使它们与声明应该镜像的策略相匹配)。它们也永远不会持久化(即使声明是这样)。
集群中的非镜像队列
本指南重点介绍镜像队列,但是,简要说明一下与镜像队列相比,非镜像队列在集群中的行为也是很有必要的。
如果队列的主节点(运行队列master的节点)可用,则可以在任何节点上执行所有队列操作(例如声明、绑定和消费者管理、消息路由到队列)。集群节点将把操作透明地路由到主节点上。
如果队列的主节点不可用,则非镜像队列的行为取决于其持久性。在节点恢复正常之前,持久化队列将不可用。在集群中其他节点上对主节点不可用的持久化队列进行任何操作都将失败,并在服务器日志中显示如下消息:
operation queue.declare caused a channel exception not_found: home node 'rabbit@hostname' of durable queue 'queue-name' in vhost '/' is down or inaccessible
非持久化队列将被删除。
如果希望队列始终保持可用,则可以将镜像配置为即使在不同步的情况下mirror也升级为master。
示例:Below is a policy where queues whose names begin with "two." are mirrored to any two nodes in the cluster, with automatic synchronisation:
rabbitmqctl | |
---|---|
rabbitmqctl (Windows) | |
HTTP API | |
Web UI |
|
The following example declares a policy named ha-all which matches the queues whose names begin with "ha." and configures mirroring to all nodes in the cluster (see To How Many Nodes to Mirror? above):
rabbitmqctl | |
---|---|
rabbitmqctl (Windows) | |
HTTP API | |
Web UI |
|
A policy where queues whose names begin with "nodes." are mirrored to specific nodes in the cluster:
rabbitmqctl | |
---|---|
rabbitmqctl (Windows) | |
HTTP API | |
Web UI |
|
镜像队列实现和语义
如前所述,对于每个镜像队列,都有一个master和几个mirrors,每个镜像位于不同的节点上。Mirrors以与master完全相同的顺序完成master发生的操作,从而保持相同的状态。除了publish以外的所有操作仅发送到master,然后master将操作的结果广播到mirrors。因此,当客户端从mirrored queue消费实际上是从master消费。Thus clients consuming from a mirrored queue are in fact consuming from the master.
当一个mirror失败时,master依然是master,Client不需要采取任何行动也不会被告知mirror failure。Mirror发生故障可能不会被立即检测到,每个连接流控制机制的中断可能会延迟消息发布。
如果master发生故障,某一个mirror将会提升为master:
1、运行时间最长的那个mirror将成为master,因为它最有可能和原来的master完全同步。如果没有与master同步的mirror,则只存在于master上的消息将丢失。
2、镜像认为所有以前的消费者都突然断开了连接。它将requeue所有已发送到client但正在等待ack的消息。这可以包括client已发出ack的消息,例如,ack在到达承载master的节点主机之前在线路上丢失,或者在从master向mirror广播时丢失。在这两种情况下,新master别无选择,只能requeue所有未看到确认的消息。
3、当队列发生故障时,那些请求收到通知的消费者将会收到cancellation通知。
4、requeue可能会导致消费者收到重复的消息。
5、当被选中的mirror成为master的时候,这期间发给该镜像队列的消息不会丢失(除非被提升的那个节点出现了故障)。发布到queue mirror节点的消息将被路由到queue master然后复制到所有mirrors。如果master失败,则消息将继续发送到mirrors,并在mirror升为master完成后将消息添加到队列中。
6、当客户端发送了带有publish confirm的消息,即使发布消息后与发布方收到ack之间master或任何mirror出现了故障,消息最终依然会被confirm。而从publisher的角度来看,发送消息到镜像队列的方式和发送消息到非镜像队列的方式两者并无不同。
如果您使用noAck=true从镜像队列中消费(即消费端没有发送消息确认),那么消息可能会丢失。当然,这与规范没有什么不同:Broker认为消息在被发送到noAck=true消费者时就已经被确认。如果client突然断开连接,则可能永远不会收到消息。在镜像队列的情况下,如果master发生故障,那么那些正在发送到noAck=true消费者的消息可能永远不会被client接收,并且不会被新master requeue。由于消费端可能连接到存活的节点,因此consumer cancellation notication有助于确定此类事件可能在何时发生。当然,在实践中,如果您希望不丢失消息,那么建议您使用noAck=false。
发送方确认机制和事务机制
镜像队列支持发送方确认机制(publisher confirm)和事务(transaction)机制。在confirm和transaction的情况下,操作将跨越队列的所有mirrors。因此,在事务的情况下,只有当事务已应用到队列的所有mirrors时,才会将tx. commitl -ok返回给client。同样,在publisher confirm的情况下,消息只有在所有mirrors都接受后才会被确认。
Flow Control
RabbitMQ使用credit-based算法来限制消息发布的速度。发布者在从队列的所有mirrors接收到credit时允许发布。Credit在此表示发布权限。不能发放Credits的mirrors可能会导致publisher暂停。发布者将一直被阻塞,直到所有mirrors发出credit,或者直到其余节点认为该mirror已从集群断开。Erlang通过定期向所有节点发送tick来检测此类断开。可以通过配置net_ticktime来设置tick时间间隔。
Mater Failures and Consumer Cancellation
正在从镜像队列中消费的client可能希望知道他们一直消费的队列已经失败。当镜像队列失败时,将不知道哪些消息已经发到了哪个消费者,因此所有未确认的消息都将根据redelivered flag重新传递。消费者可能希望知道会发生这种情况。
如果是这样,消费者消费时可将参数x-cancel-on-ha-failover设为true。消费将在故障时被取消并且会发送一个consumer cancellation notification给消费者。然后消费端可以再发送一个basic.consume来继续消费。
java代码示例如下:
Channel channel = ...;
Consumer consumer = ...;
Map args = new HashMap();
args.put("x-cancel-on-ha-failover", true);
channel.basicConsume("my-queue", false, args, consumer);
Unsynchronised Mirrors
节点可以在任何时候加入集群。根据队列的配置,当节点加入集群时,队列可以在新节点上添加镜像。此时,新镜像将为空:它不包含队列的任何现有内容。这样的mirror将接收发布到队列的新消息,因此随着时间的推移,将准确地表示镜像队列的尾部。当从镜像队列中提取消息时,新镜像丢失消息的队列的头部的大小将会缩小,直到最终镜像的内容与master内容完全匹配为止。此时,可以认为镜像是完全同步的,但是需要注意的是,发生这种情况是因为客户端耗尽队列中已存在内容。
新添加的mirror不提供附加形式的冗余或队列内容的可用性,除非队列已显式同步。由于在显式同步发生时队列变得无响应,因此最好允许正在从中提取消息的活动队列自然同步,并且仅显式同步非活动队列。
在启用自动队列镜像时,请考虑所涉及的队列在磁盘上的预期数据集。具有大量数据集(例如,几十gb或更多)的队列必须将其复制到新添加的镜像,这可能会对集群资源(如网络带宽和磁盘I/O)造成很大的负载。例如,对于lazy queues,这是一个常见的场景。
查看mirror状态(无论是否同步):
rabbitmqctl list_queues name slave_pids synchronised_slave_pids
手动同步队列:
rabbitmqctl sync_queue name
取消正在同步的队列:
rabbitmqctl cancel_sync_queue name
这些操作也可以在管理页面实现。
Promotion of Unsynchronised Mirrors on Failure
默认情况下,如果队列的主节点故障,将失去与它的对等节点的连接,或者从集群中删除,那么最老的镜像将被提升为新的主节点。在某些情况下,这个mirror可以不同步,这将导致数据丢失。
从RabbitMQ3.7.5开始,ha-promote-on-failure将控制是否允许提升未同步的mirror。当被设为when-synced,则未同步的mirror不会接管master。
默认值是always。应当谨慎使用“when-synced”。when-synced保证了消息的可靠性但放弃了可用性。一旦master永久不可用,则该队列也将不可用,除非它被删除并重新声明。删除队列将删除其所有的内容,always优先保证可用性,不过可能会导致消息丢失。
使用when-synced策略的系统必须采用发送者确认机制,以检测队列不可用性和代理无法对消息进行enqueue。
Stopping Nodes and Synchronisation
如果停掉一个包含镜像队列master的节点,则其它节点的某个mirror将接管master(假设有一个已同步mirror)。如果继续停止节点,那么最终镜像队列不再有其它mirror:它只存在于一个节点上,该节点现在是它的master。如果镜像队列被声明为持久的,那么如果它的最后一个剩余节点被关闭,那么队列中的持久消息将在该节点重新启动后存活。通常,在重新启动其他节点时,如果它们以前是镜像队列的一部分,那么它们将重新加入镜像队列。
但是,目前没有办法让mirror知道它的队列内容是否已经和它要重新连接的master产生偏差(例如,这可能发生在网络分区期间)。因此,当一个mirror重新进入镜像队列时,它会丢弃它已经拥有的所有持久本地内容,并开始清空。此时,它的行为与加入集群的新节点的行为相同。
Stopping Master Nodes with Only Unsynchronised Mirrors
当您关闭一个主节点时,可能所有可用的镜像都是不同步的。出现这种情况的常见情况是rolling cluster upgrades。
默认情况下,RabbitMQ将拒绝在受控的master关闭时(即显式停止RabbitMQ服务或关闭操作系统)提升非同步镜像,以避免消息丢失;相反,整个队列将关闭,就好像不同步的mirror不存在一样。
当master出现不受控制的关闭情况(例如服务器或节点崩溃,或者网络中断),未同步的mirror会提升至master。
如果您希望在所有情况下都将队列master移动到非同步mirror(即您将选择队列的可用性,而不考虑由于非同步镜像升级而导致的消息丢失),那么将ha-promote-on-shutdown策略设置为always,而不是默认的when-synced。
如果将ha-promote-on-failure设置为when-synced,即使将ha-promote-on-shutdown设置为always,也不会提升未同步的mirror。这意味着在队列master节点失败的情况下,队列将在master节点恢复之前不可用。如果队列master上的队列永久丢失,则该队列将不可用,除非它被删除(这也将删除其所有内容)并重新声明。
注意,ha-promote-on-shutdown和ha-promote-on-failure有不同的默认行为。ha-promote-on-shutdown的默认值是when-synced,而ha-promote-on-failure的默认值是always。
所有mirrors停止,Master丢失
在关闭队列的所有mirrors时,可能会丢失队列的master。在正常操作中,队列中最后一个关闭的节点将成为master,我们希望该节点在再次启动时仍然是master(因为它可能已经接收到其他mirror没有看到的消息)。
然而,当调用rabbitmqctl forget_cluster_node,被移除的node上可能会存在镜像队列master,RabbitMQ将尝试为这些镜像队列找到一个当前已停止的mirror,并在重新启动时将该mirror“提升”为新的主节点。如果有多个候选项,将选择最晚停止的mirror。
重要的是要理解,RabbitMQ只能在forget_cluster_node期间提升stopped mirror,因为上文已经提到任何重新启动的mirror都将清除其内容。因此,在停止的集群中删除丢失的master时,必须在再次启动mirrors之前调用rabbitmqctl forget_cluster_node。
Batch Synchronization(批同步)
从RabbitMQ 3.6.0开始,master批量执行同步。批处理可以通过ha-sync-batch-size队列参数配置。默认情况下,早期的版本每次同步一条消息。通过批量同步消息,同步过程可以大大加快。
选择最优的ha-sync-batch-size需要考虑以下几个方面:
例如,如果设置ha-sync-batch-size大小为50000条消息,队列中每条消息大小为1KB,则每个节点间同步的每批消息大小为49MB。您需要确保队列镜像之间的网络能够容纳这种流量。如果网络发送一批消息的时间长于net_ticktime,那么集群中的节点可能会认为它们处于网络分区中。
配置同步(Configuring Synchronisation)
在同步队列时,将阻塞所有其他队列操作。根据多种因素,队列可能会被同步阻塞数分钟或数小时,在极端情况下甚至数天。
队列同步可通过如下方式配置:
ha-sync-mode:manual 默认模式。新的mirror不会收到已经存在的消息,只会收到新消息。一旦消费者耗尽仅存在于master上的消息,新的mirror将随着时间的推移成为master的精确副本。如果master在所有未同步的消息耗尽之前就发生故障,那么这些消息将丢失。
ha-sync-mode:automatic 当新的mirror加入时,队列将自动同步。队列同步是一个阻塞操作。如果队列很小,或者在RabbitMQ节点之间网速很快,并且优化了ha-sync-batch-size大小,那么这是一个很好的选择。