欢迎支持笔者新作:《深入理解Kafka:核心设计与实践原理》和《RabbitMQ实战指南》,同时欢迎关注笔者的微信公众号:朱小厮的博客。
本文翻译RabbitMQ官方文档:Highly Available (Mirrored) Queues,原文地址:http://www.rabbitmq.com/ha.html。(翻译水平有限,不喜轻喷~~)
高可用(镜像)队列
默认情况下,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声明的时候使用x-queue-master-locator参数,或者在policy上设置queue-master-locator,或者直接在rabbitmq的配置文件中定义queue_master_locator。这里有三种可供选择的策略:
- min-masters:选择master数最少的那个服务节点
- client-local:选择与client相连接的那个服务节点
- random:随机分配
“nodes" Policy and Migrating Masters
当policy的ha-mode设置为nodes时,可以在指定列表中配置镜像队列,如果新配置或者修改的nodes列表中没有当前的master,那么势必会造成数据的丢失。然而RabbitMQ会保持现有的master直到其他的镜像至少有一个节点已经完全同步。但是如果发生同步的操作,队列会出现假死的现象:consumer需要和master重新建立连接。
(译者注:当调用同步命令后,队列开始阻塞,无法对其进行操作,直到同步完毕。当ha-sync-mode=automatic时,新加入节点时会默认同步已知的镜像队列。由于同步过程的限制,所以不建议在生产的active队列(有生产消费消息)中操作。)
举例,queue在节点[A, B]中,并且A为master,此时设置节点的策略为在[C,D]中,那么首先queue会存在在[A,C,D]中,等到完全同步之后,A会被shutdown,进而在[C,D]中选择一个master。
Exclusive Queues
当一个connection关闭的时候,其上锁declare的排他(exclusive)queues将会被删除。对于一个排他队列来说,为它设置镜像队列是没有用的。
排他队列是不能被镜像的,也不能被持久化。
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 name slave_pids synchronised_slave_pids
可以通过手动的方式同步一个queue:
rabbitmqctl sync_queue name
同样也可以取消某个queue的同步功能:
rabbitmqctl cancel_sync_queue name
当然这些都可以通过management插件来设置。
Stopping nodes and synchronisation
如果你关闭了镜像队列中的master节点,那么剩余的镜像中会选举一个作为新的master节点(假设都处于同步的状态)。如果你继续关闭节点直到没有多余镜像了,那么此时只有一个节点可用,这个节点也是master节点。如果这个镜像队列配置了持久化属性(durable=true)。那么当最后的节点重启之后,消息不会丢失。然后你再重启其他的节点,它们会陆续的加入到镜像队列中来。
然而,目前还没有方法判断一个重新加入的镜像是否保持和master同步的状态,因此每当一个节点加入或者重新加入(例如从网络分区中恢复过来)镜像队列,之前保存的队列内容会被清空。
Stopping Master Nodes with Only Unsynchronised Mirrors
当所有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,优先保证可用性。
Loss of Master While All Mirrors are Stopped
(略。实际上是不知道这段要表达什么gui。)
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左右。你需要确tim保你的网络在镜像节点之间能够支持这样的吞吐。如果你批量发送一批消息所使用的时间大于net_ticktime,那么集群有可能认为发生了网络分区。
Configuring Synchronisation
如果一个queue正在同步,所有对于其他的queues的操作将会被阻塞。一个queue有可能因为同步而被阻塞几分钟,几小时甚至几天。
将新节点加入已存在的镜像队列是,默认情况下ha-sync-mode=manual,镜像队列中的消息不会主动同步到新节点,除非显式调用同步命令。当调用同步命令后,队列开始阻塞,无法对其进行操作,直到同步完毕。当ha-sync-mode=automatic时,新加入节点时会默认同步已知的镜像队列。由于同步过程的限制,所以不建议在生产的active队列(有生产消费消息)中操作。
欢迎支持笔者新作:《深入理解Kafka:核心设计与实践原理》和《RabbitMQ实战指南》,同时欢迎关注笔者的微信公众号:朱小厮的博客。