高可用(镜像)队列
默认情况下,queues存放在RabbitMQ集群的单个节点之上。exchanges和bindings恰恰相反,在集群中的所有节点中都有存档。queues可以配置镜像以此可以在多个节点中有备份。每个镜像队列包含一个master节点和一个或者多个slave节点。如果master节点由于某种原因失效,那么“资历最老”的slave节点将被提升为新的master节点。(译者注:根据进入的时间排序,时间最长的节点即为资历最老的节点。)
发送的消息将被复制到队列的所有镜像节点。Consumer连接的是master节点,而不是其他的slave节点(译者注:就算程序连接的是slave节点,也会路由到master节点,然后建立tcp连接),被消费并被确认的消息将会被清除。镜像队列增强了其可用性,但是并没有分摊负载。
这里并不推荐在局域网之外搭建RabbitMQ集群,这样会有网络分区的风险。同时,客户端程序最好也部署在相同的局域网之内。
How Mirroring is Configured
在我们演示怎么配置镜像队列之前先看下之前是怎么使用的,然后我们再来陈述现在推荐怎么使用。
除了mandatory这个属性(亦或者durable,exclusive)之外,RabbitMQ中的queue也有可选的参数(parameters, 也可以称之为arguments), 有时会涉及到类似x-arguments这个参数。x-arguments可以用来配置镜像参数,但是现在又更好的办法,那就是通过policies(策略)。
Queue Arguments that Control Mirroring
Policies可以在任何适合改变,比如期初你创建了一个无镜像的queue,在之后的某个节点你可以为这个queue配置镜像,反之亦然。对于没有配置镜像的queue(non-mirrored)和配置了镜像但是又没有slave镜像节点的queue之间是有区别的,前者却反了额外的镜像机制,但是可以有更高的吞吐量。
通过policy创建对象,这里包括了两个关键的参数:ha-mode和ha-params(可选)。下表做了相应的解释
ha-mode |
ha-params |
Result |
all |
(absent) |
在集群中的每个节点都有镜像。当一个节点添加到集群中时,这个节点同样会有相应的镜像 |
exactly |
count |
指定在集群中镜像的个数。如果集群中节点的个数小于count的值,那么所有的节点都会配置镜像。如果其中一个镜像挂掉,那么会在另一个节点生成新的镜像。ha-mode:exactly和ha-promote-on-shutdown:always一起使用将会很危险。 |
nodes |
node names |
在指定的节点列表中配置镜像。节点名称可以通过rabbitmqctl cluster_status命令获取,通常名称是“rabbit@hostname”的这种形式。如果这些指定的节点都处于不可用状态(宕机或者关闭服务等),那么客户端程序会在自己所连接的那么节点上创建queue。 |
(译者注:当所有slave都出在(与master)未同步状态时,并且ha-promote-on-shutdown设置为when-synced(默认)时,如果master因为主动的原因停掉,比如是通过rabbitmqctl stop命令停止或者优雅关闭OS,那么slave不会接管master,也就是此时镜像队列不可用;但是如果master因为被动原因停掉,比如VM或者OS crash了,那么slave会接管master。这个配置项隐含的价值取向是保证消息可靠不丢失,放弃可用性。如果ha-promote-on-shutdown设置为always,那么不论master因为何种原因停止,slave都会接管master,优先保证可用性。
试想一下,如果ha-mode设置为exactly,ha-params设置为2,当其中一个镜像节点挂掉,那么在集群中的另一个节点将会被设置为镜像,此镜像尚未与master同步,此时master节点也挂掉,那么这个镜像将被提升为master,造成数据丢失。)
To How Many Nodes to Mirror?
ha-mode:all是一种非常保守且不必要选择。在有三个或者更多节点的集群中推荐配置大多数个数即可. 比如3个几点的集群配置为2, 或者5个节点的集群配置为3。有些数据是瞬时性的,对延迟要求比较高,可以配置更少的镜像个数,甚至可以不配置镜像。
Queue Masters, Master Migration, Data Locality
Queue Master Location
每个queue都有一个master节点,所有对于queue的操作都是事先在master上完成,之后再slave上进行相同的操作。保证消息的FIFO顺序是非常必要的。
每个不同的queue可以坐落在不同的集群节点上,这些queue如果配置了镜像队列,那么会有1个master和多个slave。基本上所有的操作都落在master上,那么如果这些queues的master都落在个别的服务节点上,那么势必会影响性能,而其他的节点又很空闲,这样就无法做到负载均衡。
关于master queue host 的分配有几种策略。你可以在queue声明的时候使用x-queue-master-locator参数,或者在policy上设置queue-master-locator,或者直接在rabbitmq的配置文件中定义queue_master_locator。这里有三种可供选择的策略:
- min-masters:选择master queue 数最少的那个服务节点host
- client-local:选择与client相连接的那个服务节点host
- random:随机分配
“nodes" Policy and Migrating Masters
当policy的ha-mode设置为nodes时,可以在指定列表中配置镜像队列,如果新配置或者修改的nodes列表中没有当前的master,那么势必会造成数据的丢失。然而RabbitMQ会保持现有的master直到其他的镜像至少有一个节点已经完全同步。但是如果发生同步的操作,队列会出现假死的现象:consumer需要和master重新建立连接。
举例,queue在节点[A, B]中,并且A为master,此时设置节点的策略为在[C,D]中,那么首先queue会存在在[A,C,D]中,等到完全同步之后,A会被shutdown,进而在[C,D]中选择一个master。
Exclusive Queues
当一个connection关闭的时候,其上声明的 exclusive queues将会被删除。对于一个排他队列来说,为它设置镜像队列是没有用的。
排他队列是不能被镜像的,也不能被持久化。
Non-mirrored Queue Behavior in a Cluster
本指南主要关注镜像队列,然而 与镜像队列相比,简要解释非镜像队列在集群中的行为非常重要。
如果队列的主节点(运行队列主节点)可用,则可以在任何节点上执行所有队列操作(例如声明、绑定和消费者管理、到队列的消息路由)。集群节点将透明地将操作路由到客户机的主节点。
如果队列的主节点不可用,则非镜像队列的行为取决于其持久性。在节点返回之前,持久队列将不可用。在一个不可用主节点的持久队列上的所有操作都将失败,服务器日志中的消息如下:
操作queue.declare导致通道异常not_found: vhost '/'中的持久队列'queue-name'的主节点'rabbit@hostname'关闭或不可访问
一个非持久的将被删除。
如果希望队列始终可用,可以将镜像配置为即使在不同步的情况下也可以提升为master。
Examples
下面的例子是为所有以"ha."开头的队列设置名为“ha-all"的policy。
mode |
description |
rabbitmqctl |
rabbitmqctl set_policy ha-all “^ha.” ‘{“ha-mode”:“all”}’ |
rabbitmqctl(Windows) |
rabbitmqctl set_policy ha-all “^ha.” “{”“ha-mode”":"“all”"}" |
HTTP API |
PUT /api/policies/%2f/ha-all {“pattern”:"^ha.", “definition”:{“ha-mode”:“all”}} |
Web UI |
Navigate to Admin > Policies> Add/ update a policy. Enter “ha-all” next to Name, “^ha.” next to Pattern, and “ha-mode”=“all” in the first line next to Policy. Click Add policy. |
为每个以“two.”开头的队列设置两个节点的镜像,并且设置为自动同步模式:
mode |
description |
rabbitmqctl |
rabbitmqctl set_policy ha-two “^two.” ‘{“ha-mode”:“exactly”, “ha-params”:2, “ha-sync-mode”:“automatic”}’ |
rabbitmqctl (Windows) |
rabbitmqctl set_policy ha-two “^two.” “{”“ha-mode”":"“all”","“ha-params”":2, ““ha-sync-mode””:"“automatic”"}" |
HTTP API |
PUT /api/policies/%2f/ha-two {“pattern”:"^two.",“definition”:{“ha-mode”:“exactly”,“ha-params”:2,“ha-sync-mode”:“automatic”}} |
Web UI |
Navigate to Admin > Policies > Add /update a policy. Enter ‘ha-two’ next to Name and “^two.” next to Pattern. Enter “ha-mode”=“exactly” in the first line next to Policy, then “ha-params”=2 in the second line, then “ha-sync-mode”=“automatic” in the third, and the type on the second line to “Number”. Click Add policy. |
为每个以“node."开头的队列分配指定的节点做镜像
mode |
description |
rabbitmqctl |
rabbitmqctl set_policy ha-nodes “^nodes.” ‘{“ha-mode”:“nodes”,“ha-params”:[“rabbit@nodeA”,“rabbit@nodeB”]}’ |
rabbitmqctl (Windows) |
rabbitmqctl set_policy ha-nodes “^nodes.” “{”“ha-mode”":"“node”","“ha-params”":["“rabbit@nodeA”","“rabbit@nodeB”"]}" |
HTTP API |
PUT /api/policies/%2f/ha-nodes {“pattern”:"^node.",“definition”:{“ha-mode”:“nodes”,“ha-params”:[“rabbit@nodeA”,“rabbit@nodeB”]}} |
Web UI |
Navigate to Admin > Policies > Add / update a policy. Enter “ha-nodes” next to Name and “^nodes.” next to Pattern. Enter “ha-mode” = “nodes” in the first line next to Policy, then “ha-params” in the second line, set the second line’s type to “List”, and then enter “rabbit@nodeA” and “rabbit@nodeB” in the sublist which appears. Click Add policy. |
Mirrored Queue Implementation and Semantics
正如先前所论述的,每个镜像队列中拥有一个master和多个slave,这些都分布在不同的节点上。在master上的操作会在slave上一样的执行,这样才能保持一致的状态。对于镜像队列,客户端Basic.Publish操作会同步到所有节点(消息同时发送到master和所有slave上,如果此时master宕掉了,消息还发送slave上,这样当slave提升为master的时候消息也不会丢失),而其他操作则是通过master中转,再由master将操作作用于slave。比如一个Basic.Get操作,假如客户端与slave建立了TCP连接,首先是slave将Basic.Get请求发送至master,由master备好数据,返回至slave,投递给消费者。
All actions other than publishes go only to the master, and the master then broadcasts the effect of the actions to the mirrors.
如果某个slave失效了,系统处理做些记录外几乎啥都不做:master依旧是master,客户端不需要采取任何行动或者被通知slave已失效。注意slave的失效不会被立刻检测出来,
如果master失效了,那么slave中的一个必须被选中为master。此时会发生如下的情形:
- 被选中作为新的master的slave通常是最老的那个,因为最老的slave与前任master之间的同步状态应该是最好的。然而,需要注意的是,如果存在没有任何一个slave与master完全同步的情况,那么前任master中未被同步的消息将会丢失。
- slave节点认为目前所有的消费者都已经突然的disconnect了。它会requeue所有被发送到客户端但没有被ack的消息。这里包括客户端以及发送ack但是丢失在返回broker的路上,或者master已经收到但是其他的slave没有收到,这些消息都会被requeue。无论何种清理,新的master只能requeue所有没有被ack的消息。
- 消费端如果引入了Consumer Cancellation Notification,那么当当前的queue挂掉的时候应该被通知到。
- 由于requeue的存在,客户端当重新消费queue的时候,有可能将之前消费过的消息又顺序的消费一遍。
- 当一个slave提升为master的时候,发送到当前镜像队列的消息将不会丢失(除非这个新的master紧接着挂了)。消息发送到一个slave的时候,将会被路由到master上,进而又被复制到所有的slave上。此时如果master挂了,消息将会继续发送到slave上,当一个slave提升为master的时候,这些消息会被存入queue中。
- 客户端如果在发送消息是采用了publisher confirm机制,那么在消息发送和消息确认之间 master挂掉(或者任何slave挂掉)都不会影响confirm机制的正确运行。
如果你在消费一个镜像队列的时候这是autoAck=true(客户端不会进行消息确认),那么消息有可能会丢失。broker中的消息一旦发送出去就会被立刻确认(被确认的消息不能再被消费,且broker内部线程会执行清理工作将此消息清除),如果与客户端建立的连接突然中断,那么消息将会永远丢失。所以为了确保消息不丢失,还是建议你在消费时将autoAck设置为false。
Publisher Confirms and Transactions
RabbitMQ的镜像队列同时支持publisher confirm和事务两种机制。在事务机制中,只有当前事务在全部镜像queue中执行之后,客户端才会收到Tx.CommitOk的消息。同样的,在publisher confirm机制中,向publisher进行当前message确认的前提是该message被全部镜像所接受了。
Flow Control
基于credit的算法来实现限制消息发送的速率。
Master Failures and Consumer Cancellation
若客户端在消费的时候执行了参数x-cancel-on-ha-failover=true,那么当在故障处理的时候将会停止消费,并且会受到一个"consumer cancellation notification". 这样消费需要重新发送Basic.Consume进而可以重新消费。
举例:
- Channel channel = ...;
- Consumer consumer = ...;
- Map<String, Object> args = new HashMap<String, Object>();
- args.put("x-cancel-on-ha-failover", true);
- channel.basicConsume("my-queue", false, args, consumer);
Unsynchronised Mirrors
一个节点可以在任何时候加入集群之中。根据queue的配置,当新节点加入进来的时候,这个queue有可能在这个新的节点上添加一个镜像,此时这个镜像(slave)是空的,它不包含任何queue中已经存在的内容。新加入的镜像可以收到生产者新发送过来的消息,其内容与其他镜像的尾部保持一致。随着queue中的消息被逐渐的消费,新加入的镜像中“错失”的消息逐渐减少,直到与其他镜像保持一致,既而就已经完全处于同步状态。但是需要注意的是,上述的同步行为是基于客户端的操作而触发的。
所以新加入的镜像并没有提供额外的冗余和可靠性保障,除非它能精确的同步。将新节点加入已存在的镜像队列是,默认情况下ha-sync-mode=manual,镜像队列中的消息不会主动同步到新节点,除非显式调用同步命令。当调用同步命令后,队列开始阻塞,无法对其进行操作,直到同步完毕。当ha-sync-mode=automatic时,新加入节点时会默认同步已知的镜像队列。由于同步过程的限制,所以不建议在生产的active队列(有生产消费消息)中操作。
可以使用下面的命令来查看那些slaves已经完成同步:
rabbitmqctl list_queues
nameslave_pids synchronised_slave_pids
可以通过手动的方式同步一个queue:
rabbitmqctl sync_queue
name
同样也可以取消某个queue的同步功能:
rabbitmqctl cancel_sync_queue
name
当然这些都可以通过management插件来设置。
Promotion of Unsynchronised Mirrors on Failure
默认情况下,如果队列的主节点失败、失去与其他节点的连接或从集群中删除,那么最老的镜像将被提升为新主节点。在某些情况下,此镜像可能不同步,这将导致数据丢失。
从RabbitMQ 3.7.5开始,ha-promote-on-failure策略键控制是否允许不同步镜像提升。当设置为When -synced时,它将确保不提升不同步的镜像。
默认值always。应该谨慎使用when-synced值。它牺牲了不同步镜像升级带来的安全性,增加了对队列管理器可用性的依赖。有时队列可用性比一致性更重要。
when-synced的提升策略避免了由于提升不同步镜像而导致的数据丢失,但是使队列的可用性依赖于master 节点的可用性。在队列主节点发生故障时,队列将不可用,直到队列主节点恢复。在永久丢失队列主控器的情况下,除非删除并重新声明队列,否则队列将不可用。删除队列将删除它的所有内容,这意味着使用此提升策略的永久主服务器等同于丢失所有队列内容。
使用when-synced提升策略的系统必须使用发布者confirm,以便检测队列不可用性和代理无法对消息进行排队。
Stopping nodes and synchronisation
如果你关闭了镜像队列中的master节点,那么剩余的镜像中会选举一个作为新的master节点(假设都处于同步的状态)。如果你继续关闭节点直到没有多余镜像了,那么此时只有一个节点可用,这个节点也是master节点。如果这个镜像队列配置了持久化属性(durable=true)。那么当最后的节点重启之后,消息不会丢失。然后你再重启其他的节点,它们会陆续的加入到镜像队列中来。
然而,目前还没有方法判断一个重新加入的镜像是否保持和master同步的状态,因此每当一个节点加入或者重新加入(例如从网络分区中恢复过来)镜像队列,之前保存的队列内容会被清空。
Stopping Master Nodes with Only Unsynchronised Mirrors
当您关闭一个主节点时,可能所有可用的镜像都不同步。发生这种情况的常见情况是滚动集群升级。
默认情况下,为了避免消息丢失,RabbitMQ将拒绝在受控master关机(即显式停止RabbitMQ服务或关闭操作系统)时提升不同步镜像;相反,整个队列将关闭,就好像不存在未同步的镜像一样。
不受控制的master关闭(即服务器或节点崩溃或网络中断)仍然会触发不同步镜像的升级。
如果你宁愿master queue 在所有情况下 move to 一个未同步的镜像 ,就设置ha-promote-on-shutdown 值为always
如果将故障时启动的ha策略键设置为when-synced,即使将关闭时启动的ha键设置为always,也不会启动不同步的镜像。这意味着,在队列主节点发生故障时,队列将不可用,直到主节点恢复。在永久丢失队列主控器的情况下,除非删除队列(也将删除其所有内容)并重新声明,否则队列将不可用。
请注意,ha-promote-on-shutdown和ha-promot -on-failure有不同的默认行为。在默认情况下,ha-promot -on-shutdown设置为when-synced,而在默认情况下,ha-promot -on-failure设置为always。
参数ha-promote-on-shutdown
镜像队列的另外一个参数ha-promote-on-shutdown,也在可用性和可靠性之间做了一个平衡。ha-promote-on-shutdown有2个取值:when-synced,always。默认是when-synced。ha-promote-on-shutdown是用来控制选主的行为的。
当取值为when-synced时,在可控的master关闭时(比如停止RabbitMQ服务或者关闭操作系统),RabbitMQ会拒绝故障恢复(fail over)到一个非同步slave,也即拒绝把一个非同步的slave提升成新的master。只有在非可控的master关闭时(比如server crash, 断网),才会故障恢复到一个非同步的slave。
当取值为always时,则在所有情况下,都不会拒绝故障恢复到非同步的slave。
很明显,这个参数也是平衡可用性和可靠性的,当when-synced,可靠性更好,可用性降低了,因为如果所有的slave都是非同步状态,那就没有符合条件的slave可以被提升成master,这时队列就处在不可用状态。
Loss of Master While All Mirrors are Stopped
在关闭队列的所有镜像时,可能会丢失master queue 。在正常操作中,队列关闭的最后一个节点将成为主节点,并且我们希望该节点在重新启动时仍然是主节点(因为它可能接收到其他镜像没有看到的消息)。
但是,当您调用rabbitmqctl forget get_cluster_node时,RabbitMQ将尝试为每个队列找到一个当前已停止的镜像并且该队列的主队列位于我们正在遗忘的节点上,并且在重新启动时将该镜像“提升”为新的master队列。如果有多个候选项,则选择最近停止的镜像。
重要的是要理解RabbitMQ只能提升stopped镜像在forget_cluster_node的时候,因为任何重新启动的镜像都将清除它们的内容,如上面的“停止节点和同步”所述。因此,在停止的集群中删除丢失的主服务器时,必须在重新启动镜像之前调用rabbitmqctl forget_cluster_node。
Batch Synchronization
RabbitMQ 3.6.0引入了一个与镜像队列有关的参数:ha-sync-batch-size。可以批量的进行消息同步,进而非常可观的提升同步处理的效率。之前的版本默认只能同步一条消息。
关于ha-sync-batch-size的取值,你需要考虑一下几个方面:
- 消息的平均大小
- RabbitMQ节点间的网络吞吐量
- net_ticktime的值(参考:http://www.rabbitmq.com/nettick.html)
举个例子,如果你需要每次同步50000条消息,每条消息平均大小为1KB,那么ha-sync-batch-size设置为约49MB左右。你需要确保你的网络在镜像节点之间能够支持这样的吞吐。如果你批量发送一批消息所使用的时间大于net_ticktime,那么集群有可能认为发生了网络分区。
Configuring Synchronisation
如果一个queue正在同步,所有对于其他的queues的操作将会被阻塞。一个queue有可能因为同步而被阻塞几分钟,几小时甚至几天。
将新节点加入已存在的镜像队列时,默认情况下ha-sync-mode=manual,镜像队列中的消息不会主动同步到新节点,除非显式调用同步命令。当调用同步命令后,队列开始阻塞,无法对其进行操作,直到同步完毕。当ha-sync-mode=automatic时,新加入节点时会默认同步已知的镜像队列。由于同步过程的限制,所以不建议在生产的active队列(有生产消费消息)中操作。
RabbitMQ的mirror queue(镜像队列)机制是最简单的队列HA方案,它通过在cluster的基础上增加ha-mode、ha-param等policy选项,可以根据 需求将cluster中的队列镜像到多个节点上,从而实现高可用,消除cluster模式中队列内容单点带来的风险。
在使用镜像队列之前,有几点注意事项必须熟记于心(下文中将混用主节点和master,从节点和slave):
1. 镜像队列不能作为负载均衡使用,因为每个操作在所有节点都要做一遍。
2. ha-mode参数和durable declare对exclusive队列都不生效,因为exclusive队列是连接独占的,当连接断开,队列自动删除。所以实际上这两个参数对exclusive队列没有意义。
3. 将新节点加入已存在的镜像队列时,默认情况下ha-sync-mode=manual,镜像队列中的消息不会主动同步到新节点,除非显式调用同步命令。当 调用同步命令(via rabbitmqctl or web-based ui)后,队列开始阻塞,无法对其进行操作,直到同步完毕。当ha-sync-mode=automatic时,新加入节点时会默认同步已知的镜像队列。 由于同步过程的限制,所以不建议在生产环境的active队列(有生产消费消息)中操作。
4. 每当一个节点加入或者重新加入(例如从网络分区中恢复回来)镜像队列,之前保存的队列内容会被清空。
5. 镜像队列有主从之分,一个主节点(master),0个或多个从节点(slave)。当master宕掉后,会在slave中选举新的master。选举算法为最早启动的节点。
6. 当所有slave都处在(与master)未同步状态时,并且ha-promote-on-shutdown policy设置为when-syned(默认)时,如果master因为主动的原因停掉,比如是通过rabbitmqctl stop命令停止或者优雅关闭OS,那么slave不会接管master,也就是说此时镜像队列不可用;但是如果master因为被动原因停掉,比如VM 或者OS crash了,那么slave会接管master。这个配置项隐含的价值取向是优先保证消息可靠不丢失,放弃可用性。如果ha-promote-on- shutdown policy设置为alway,那么不论master因为何种原因停止,slave都会接管master,优先保证可用性。
7. 镜像队列中最后一个停止的节点会是master,启动顺序必须是master先起,如果slave先起,它会有30秒的等待时间,等待master启动, 然后加入cluster。当所有节点因故(断电等)同时离线时,每个节点都认为自己不是最后一个停止的节点。要恢复镜像队列,可以尝试在30秒之内同时启 动所有节点。
8. 对于镜像队列,客户端basic.publish操作会同步到所有节点;而其他操作则是通过master中转,再由master将操作作用于salve。 比如一个basic.get操作,假如客户端与slave建立了TCP连接,首先是slave将basic.get请求发送至master,由 master备好数据,返回至slave,投递给消费者。
9. 由8可知,当slave宕掉时,除了与slave相连的客户端连接全部断开之外,没有其他影响。当master宕掉时,会有以下连锁反应:1)与 master相连的客户端连接全部断开。2)选举最老的slave为master。若此时所有slave处于未同步状态,则未同步部分消息丢失。3)新的 master节点requeue所有unack消息,因为这个新节点无法区分这些unack消息是否已经到达客户端,亦或是ack消息丢失在到老 master的通路上,亦或是丢在老master组播ack消息到所有slave的通路上。所以处于消息可靠性的考虑,requeue所有unack的消 息。此时客户端可能受到重复消息。4)如果客户端连着slave,并且basic.consume消息时指定了x-cancel-on-ha- failover参数,那么客户端会收到一个Consumer Cancellation Notification通知,Java SDK中会回调Consumer接口的handleCancel()方法,故需覆盖此方法。如果不指定x-cancel-on-ha-failover参 数,那么消费者就无法感知master宕机,会一直等待下去