RabbitMQ系列(十一)分布式RabbitMQ(队列镜像)

3,462 阅读23分钟

概述

本指南覆盖了传统的队列镜像(队列内容复制)。Quorum队列是另一个提供复制功能的可选队列类型。我们强烈推荐读者也阅读Quorum队列指南。
本指南覆盖的主题:
1.传统的队列镜像以及如何工作;
2.如何启用队列镜像;
3.哪些镜像设置项可用;
4.推荐哪些复制因素;
5.数据局部性;
6.Master选举(镜像升级)和不同步镜像;
7.在节点失败的情况下,镜像与非镜像队列行为;
8.批量同步新增和恢复镜像;
更多...
本指南假定你已经熟悉RabbitMQ集群。

什么是队列镜像

默认情况下,RabbitMQ集群中的队列是位于单个节点上(声明队列的节点),这和Exchang/Binding在所有节点上形成鲜明对比。队列可以选择在多个节点上进行镜像。
每个镜像队列由一个主队列和一个或多个镜像组成。主队列托管在一个通常称为主节点的节点上。每个队列都有自己的主节点。指定队列上的所有操作首先应用于队列的主节点,然后传播到镜像。这涉及到消息发布、向消费者传递消息、跟踪来自消费者的确认等等。

队列镜像意味着一个节点集群。因此,不建议跨WAN使用它。
发布到主队列的消息被复制到所有的镜像。无论连接到哪个节点,消费者都将被连接到主服务器,镜像将丢弃在主服务器上已确认的消息。因此,队列镜像增强了可用性,但不跨节点分担负载。如果承载主队列的节点发生故障,则只要最早的镜像已经同步了,就会将其提升为新主服务器。根据队列镜像参数,也可以提升未同步的镜像。

如何配置镜像

使用策略来配置镜像参数。策略通过名称(使用正则表达式模式)匹配一个或多个队列,并包含一个定义(可选参数的映射),该定义添加到匹配队列的属性集合中。
有关策略的更多信息,请参见运行时参数和策略

控制镜像队列的参数

队列通过策略启用了镜像。策略可以随时改变;创建一个非镜像队列,然后在稍后的某个时间将其镜像(反之亦然)。非镜像队列和没有任何镜像的镜像队列是有区别的——前者缺少额外的镜像基础设施,并且可能提供更高的吞吐量。您应该注意向队列中添加镜像的行为。

要使队列成为镜像,您需要创建一个策略来匹配它们并设置策略键ha-mode和(可选)ha-params。下表解释了这些键的选项:

ha-mode ha-params 结果
exactly count 集群中队列副本的数量(主队列加上镜像)。count值为1表示一个副本:只是队列主机。如果运行队列主机的节点变得不可用,则其行为取决于队列持久性。count值为2表示两个副本:一个队列主队列和一个队列镜像。换句话说:“镜像数=节点数-1”。如果运行队列主服务器的节点变得不可用,队列镜像将根据配置的镜像提升策略自动提升到主服务器。如果集群中的节点数少于count,则将队列镜像到所有节点。如果集群中有多个计数节点,并且一个包含镜像的节点宕机,那么将在另一个节点上创建一个新镜像。使用' exactly '模式和' ha-promot-on-shutdown ': ' always '可能是危险的,因为队列可以跨集群迁移,并在停机时变得不同步。
all 不设置 队列跨集群中的所有节点镜像。当一个新节点被添加到集群中时,队列将被镜像到该节点。这个设置非常保守。建议镜像到集群节点的仲裁(N/2 + 1)。镜像到所有节点会给所有集群节点带来额外的负担,包括网络I/O、磁盘I/O和磁盘空间的使用。
nodes 节点名称 队列被镜像到节点名中列出的节点。节点名是在rabbitmqctl cluster_status中出现的Erlang节点名;它们的形式通常是“rabbit@hostname”。如果这些节点名中有任何一个不是集群的一部分,则不构成错误。如果在声明队列时列表中的节点都不在线,则将在声明客户机连接的节点上创建队列。

每当队列的HA策略发生更改时,它都将努力使现有的镜像尽可能与新策略保持一致。

复制因子:多少个镜像才是最优的

镜像到所有节点会增加所有集群节点的负载,包括网络I/O、磁盘I/O和磁盘空间的使用。
在大多数情况下,在每个节点上都有一个副本是不必要的。对于3个或更多节点的集群,建议复制到一个仲裁节点(N/2+1),例如3个节点集群中的2个节点或5个节点集群中的3个节点。
由于某些数据可能天生是短暂的或对时间非常敏感,因此对某些队列使用较少的镜像(甚至不使用任何镜像)是完全合理的。

如何检查队列是否被镜像

在管理UI的队列页面上,镜像队列将有一个策略名称和副本(镜像)数量。 下面是一个名为two.replicas 的队列示例。有一个主队列和一个镜像:

队列的主节点及其镜像(如有)将在队列页面上列出来:
如果队列页面没有列出任何镜像,则队列不被镜像(或只有一个非在线的镜像):
当添加新的队列镜像时,将记录事件:

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列出队列主机和镜像。在这个例子中,我们也显示队列策略,因为它是高度相关的:

rabbitmqctl list_queues name policy pid slave_pids

# => Timeout: 60.0 seconds ...
# => Listing queues for vhost / ...
# => two.replicas ha-two <hare@warp10.1.2223.0> [<rabbit@warp10.3.1360.0>]

如果预期进行镜像的队列没有镜像,这通常意味着队列名称与控制镜像的策略中指定的名称不匹配,或者另一个策略具有优先级(并且不启用镜像)。有关更多信息,请参见运行时参数和策略。

主队列,主队列迁移,数据局部性

主队列位置

RabbitMQ中的每个队列都有一个主队列。该节点称为队列主服务器。所有队列操作首先经过主队列,然后复制到镜像。这对于保证消息的FIFO排序是必要的。
主队列可以使用几种策略在节点之间分布。使用哪种策略由三种方式控制,即:
1)使用x-queue-master-locator队列声明参数;
2)设置queue-master-locator策略键;
3)在配置文件中定义queue_master_locator键;

以下是一些可能的策略以及如何设置它们:

1.选择承载最小绑定主机数量的节点:min-masters;
2.选择客户机声明队列连接到的节点:client-local;
3.随机选择一个节点:min-masters

"nodes"策略和迁移主机

请注意,如果在新策略中没有列出现有的主策略,则设置或修改“nodes”策略会导致该主策略消失。为了防止消息丢失,RabbitMQ将保留现有的主服务器,直到至少有一个其他镜像同步(即使这是一个很长的时间)。然而,一旦同步发生,事情就会像节点失败一样继续进行:使用者将与主用户断开连接,需要重新连接。

例如,如果一个队列在[A B](A主节点)上,并且您给它一个"nodes"策略,告诉它在[C D]上,那么它最初将结束于[A C D]上。一旦队列在它的新镜像上同步[C D], A上的主进程就会关闭。

镜像独占队列

当声明独占队列的连接关闭时,独占队列将被删除。由于这个原因,镜像独占队列(或持久队列)是没有用的,因为当承载它的节点宕机时,连接将关闭,无论如何都需要删除队列。
由于这个原因,独占队列永远不会被镜像(即使它们匹配一个声明它们应该被镜像的策略)。它们也不是持久的(即使这样声明)。

集群中的非镜像队列行为

然而,本指南主要关注镜像队列,简要解释与镜像队列相比,非镜像队列在集群中的行为也是重要的。
如果队列的主节点(运行队列主节点)可用,则可以在任何节点上执行所有队列操作(例如声明、绑定和消费者管理、到队列的消息路由)。集群节点将透明地将操作路由到主节点。
如果队列的主节点不可用,则非镜像队列的行为取决于其持久性。在节点恢复之前,持久队列将不可用。在不可用主节点的持久队列上的所有操作都将失败,在服务器日志中会出现这样的消息:

operation queue.declare caused a channel exception not_found: home node 'rabbit@hostname' of durable queue 'queue-name' in vhost '/' is down or inaccessible

一个非持久的队列将会被删除。

示例

下面是一个策略,其中队列的名称以“two.”开头。同步到集群中的两个节点的镜像:

工具 操作
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"":""exactly"",""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 导航至Admin > Policies > Add / update a policy;
在名称栏输入"ha-two"并在模式中输入"^two.";
在策略后面第一行输入"ha-mode" = "exactly", 然后在第二行输入"ha-params" = 2, 然后第三行输入"ha-sync-mode" = "automatic"
, 并在类型中设置为"Number";
点击新增策略

下面的示例声明了一个名为ha-all的策略,它匹配名称以“ha.”开头的队列,并将镜像配置为集群中的所有节点(查看要镜像多少节点?以上):

工具 操作
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 导航至Admin > Policies > Add / update a policy;
名称栏中输入"ha-all",模式中输入"^ha." 并在策略后面第一行中输入"ha-mode" = "all";
点击新增策略

一个策略,其中队列的名称以“nodes.”开始,被镜像到集群中的特定节点:

工具 操作
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"":""nodes"",""ha-params"":[""rabbit@nodeA"", ""rabbit@nodeB""]}"
HTTP API PUT /api/policies/%2f/ha-nodes
{"pattern":"^nodes.", "definition":{"ha-mode":"nodes", "ha-params":["rabbit@nodeA",
"rabbit@nodeB"]}
Web UI 导航至Admin > Policies > Add / update a policy;
在名称旁边输入“ha-nodes”,在模式旁边输入“^nodes.”。在Policy旁边的第一行输入“ha-mode”=“nodes”,然后在第二行输入“ha-params”,设置第二行的类型为“List”,然后在出现的子列表中输入“rabbit@nodeA”和“rabbit@nodeB”;
点击新增策略

镜像队列实现和语义

如前所述,对于每个镜像队列,有一个主队列和几个镜像,每个镜像位于不同的节点上。镜像以与主服务器完全相同的顺序应用主服务器上发生的操作,从而保持相同的状态。除了发布之外的所有操作都只发送给主服务器,然后主服务器将操作的效果广播给镜像。因此,从镜像队列消费的客户机实际上是从主队列消费的。
如果镜像发生故障,除了做一些记录之外,几乎没有什么可做的:主镜像仍然是主镜像,客户机不需要采取任何操作或了解故障。
注意,可能无法立即检测到镜像故障,每个连接流控制机制的中断可能会延迟消息发布。这里描述了细节。

如果主服务器失败,则其中一个镜像将提升为主服务器,如下所示:
1.运行时间最长的镜像被提升为主镜像,假设它最有可能与主镜像完全同步。如果没有与主服务器同步的镜像,仅存在于主服务器上的消息将丢失;
2.镜像认为之前所有的消费者都被突然断开连接。它需要所有已发送到客户端但正在等待确认的消息。这可能包括客户端已发出确认的消息,例如,如果确认在到达节点托管队列主机之前在网络上丢失,或者在从主机广播到镜像时丢失。在这两种情况下,新主服务器别无选择,只能重新发送所有它没有看到确认的消息;
3.请求在队列故障转移时得到通知的使用者将被通知取消;
4.作为请求的结果,从队列中重新消费的客户端必须知道,它们随后可能会收到它们已经收到的消息;
5.当选择的镜像成为主队列时,在此期间发布到镜像队列的消息将不会丢失(除非提升节点上的后续故障)。发布到承载队列镜像的节点的消息被路由到队列主机,然后复制到所有镜像。如果主服务器失败,消息将继续发送到镜像,并在镜像升级到主服务器完成后添加到队列中;
6.即使主服务器(或任何镜像)在发布的消息和发布服务器收到的确认之间发生故障,客户端使用publisher confirm发布的消息仍然会被确认。从发布服务器的角度来看,发布到镜像队列与发布到非镜像队列没有什么不同;

如果使用者使用自动确认模式,则消息可能会丢失。当然,这与非镜像队列没有什么不同:代理将在消息以自动确认模式发送给消费者时立即确认消息。
如果客户机突然断开连接,则可能永远收不到消息。在镜像队列的情况下,如果主队列死亡,那些以自动确认模式发送给消费者的消息可能永远不会被那些客户端接收,也不会被新主队列重新请求。由于消费客户端可能连接到幸存的节点,所以消费者取消通知对于识别此类事件何时发生非常有用。当然,在实践中,如果数据安全性不如吞吐量重要,那么自动确认模式就是解决之道。

发布确认和事务

镜像队列同时支持发布服务器的确认和事务。所选择的语义是在确认和事务的情况下,操作跨队列的所有镜像。因此,在事务的情况下,只有当事务跨队列的所有镜像应用时,才会将tx.commit-ok返回给客户机。同样,在发布者确认的情况下,消息只有在被所有镜像接受后才会被确认给发布者。可以正确地将其视为与将消息路由到多个正常队列相同的语义,以及将具有其中的发布的事务类似地路由到多个队列。

流量控制

RabbitMQ使用基于授信的算法来限制消息发布的速度。当发布服务器从队列的所有镜像中获得授信时,他们才被允许发布。在这种情况下,授信意味着发布的许可。未能发放授信的镜像可能会导致发布服务器停滞不前。发布者将一直被阻塞,直到所有的镜像都发出授信,或者直到其余的节点认为镜像与集群断开连接。
Erlang通过定期向所有节点发送标记来检测这种断开。时间间隔可以通过net_ticktime配置设置来控制。

Master失效和消费者取消

使用镜像队列的客户端可能希望知道他们使用的队列发生了故障转移。当镜像队列发生故障时,已经发送到消费者的确认信息将丢失,因此所有未确认的消息都将使用重新发送标志集重新发送。消费者可能希望知道这将会发生。如果是,则可以将参数x-cancel-on-ha-failover设置为true。然后在故障转移和发送消费者取消通知时取消其消费。消费者有责任重新发行basic.consume重新消费。
示例(Java)

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);

这将创建一个新的消费者和参数集。

不同步的镜像

节点可以在任何时候加入集群。根据队列的配置,当节点加入集群时,队列可以在新节点上添加一个镜像。此时,新镜像将为空:它将不包含队列的任何现有内容。这样的镜像将接收发布到队列的新消息,因此随着时间的推移,它将准确地表示镜像队列的后面部分消息。当消息从镜像队列中取出时,新镜像缺少消息的队列的前面部分的消息将缩少,直到最终镜像的内容与主队列的内容完全匹配。此时,可以认为镜像是完全同步的,但需要注意的是,这是由于客户机在耗尽队列中已存在的前面消息时所采取的操作造成的。
除非队列已显式同步,否则新添加的镜像不会提供附加形式的冗余或添加镜像之前存在的队列内容的可用性。由于当显式同步发生时队列变得无响应,所以最好允许从其中取出消息的活动队列自然地进行同步,而只显式地同步不活动队列。 启用自动队列镜像时,请考虑所涉及的队列在磁盘上的预期数据集。具有相当大的数据集(比如,几十GB或更多)的队列必须将其复制到新添加的镜像,这可能会给网络带宽和磁盘I/O等集群资源带来很大的负载。例如,这是懒队列的常见场景。

要查看镜像状态(是否同步),请使用:

rabbitmqctl list_queues name slave_pids synchronised_slave_pids

可以手动同步队列:

rabbitmqctl sync_queue {name}

或取消正在进行的同步:

rabbitmqctl cancel_sync_queue {name}

这些特性也可以通过管理插件获得。

故障时不同步镜像的提升

默认情况下,如果队列的主节点发生故障、失去与其他节点的连接或从集群中删除,那么最老的镜像将被提升为新的主节点。在某些情况下,此镜像可能不同步,这将导致数据丢失。

从RabbitMQ 3.7.5开始,ha-promote-on-failure策略键控制是否允许不同步的镜像提升。当设置为when-synced时,它将确保不同步的镜像不会被提升。 默认值为always。应该小心使用同步时间的值。它牺牲了不同步镜像升级的安全性,增加了对队列管理器可用性的依赖。有时队列可用性比一致性更重要。

when-synced的提升策略避免了由于提升不同步镜像而造成的数据丢失,但使队列的可用性依赖于其主队列的可用性。在队列主节点失败的情况下,队列将变得不可用,直到队列主节点恢复。在永久丢失队列主控器的情况下,队列将不可用,除非删除并重新声明它。删除队列将删除它的所有内容,这意味着使用此提升策略永久丢失一个主队列等同于丢失所有队列内容。
使用when-synced提升策略的系统必须使用发布者确认,以检测队列不可用性和代理无法对消息排队。

停止节点和同步

如果您停止包含镜像队列主节点的RabbitMQ节点,则其他节点上的一些镜像将被提升到主节点(假设有一个同步镜像;见下文)。如果继续停止节点,则会到达镜像队列不再有镜像的点:它只存在于一个节点上,而该节点现在是它的主节点。如果镜像队列被声明为持久的,那么如果其最后一个剩余节点关闭,则队列中的持久消息将在该节点重启后继续存在。通常,在重新启动其他节点时,如果它们以前是镜像队列的一部分,那么它们将重新加入镜像队列。
但是,目前还没有方法让镜像知道它的队列内容是否已经从它重新加入的主服务器脱离(例如,这可能在网络分区期间发生)。因此,当一个镜像重新加入一个镜像队列时,它会丢弃它已经拥有的任何持久的本地内容,并开始为空。此时,它的行为与加入集群的新节点相同。

停止只有不同步镜像的主节点

当您关闭主节点时,所有可用的镜像可能不同步。发生这种情况的常见情况是滚动集群升级。
默认情况下,为了避免消息丢失,RabbitMQ将拒绝在受控主关机(即显式停止RabbitMQ服务或关闭操作系统)时提升不同步镜像;相反,整个队列将关闭,就好像不存在不同步的镜像一样。不受控制的主机关闭(即服务器或节点崩溃或网络中断)仍然会触发不同步镜像的升级。
如果你更喜欢队列的主队列迁移到一个在所有情况下对各个镜像不同步(即您可以选择队列的可用性,而不是避免由于不同步镜像升级而导致的消息丢失)然后设置ha-promote-on-shutdown策略键为“aways”而不是when-synced的默认值。
如果ha-promot-on-failure策略键设置为when-synced,则即使将ha-promot -on-shutdown键设置为always,也不会提升不同步的镜像。这意味着在发生队列主节点故障时,队列将变得不可用,直到主节点恢复。在永久丢失队列主队列的情况下,除非删除它(也将删除它的所有内容)并重新声明,否则队列将不可用。

注意,ha-promote-on-shutdown和ha-promote-on-failure有不同的默认行为。ha-promote-on-shutdown默认设置为when-synced,而ha-promote-on-failure默认设置为always。

在所有镜像停止时丢失一个主镜像

在关闭队列的所有镜像时,可能会丢失队列的主队列。在正常操作中,要关闭队列的最后一个节点将成为主节点,并且我们希望该节点在再次启动时仍然是主节点(因为它可能已经收到了其他镜像没有看到的消息)。
但是,当您调用rabbitmqctl forget_cluster_node时,RabbitMQ将尝试为每个在我们遗忘的节点上有其主队列的队列查找一个当前停止的镜像,并在该队列重新启动时将该镜像“提升”为新主队列。如果有多个候选,则选择最近停止的镜像。
重要的是要理解RabbitMQ只能在get_cluster_node期间提升已停止的镜像,因为任何重新启动的镜像都将清除它们的内容,如上面“停止节点和同步”所述。因此,在停止的集群中删除丢失的主服务器时,必须在重新启动镜像之前调用rabbitmqctl forget_cluster_node。

批量同步

典型的队列管理器批量执行同步。批处理可以通过ha-sync-batch-size队列参数进行配置。在默认情况下,早期版本将一次同步一条消息。通过批量同步消息,可以显著加快同步过程。

要为ha-sync-batch-size选择合适的值,您需要考虑:
1.平均消息大小
2.RabbitMQ节点之间的网络吞吐量
3.net_ticktime值

例如,如果您将ha-sync-batch-size设置为50000条消息,并且队列中的每个消息大小为1KB,那么节点之间的每个同步消息大小约为49MB。您需要确保您的队列镜像之间的网络能够容纳这种流量。如果网络发送一批消息的时间比net_ticktime长,那么集群中的节点可能认为它们存在一个网络分区。

配置同步

让我们从队列同步最重要的方面开始:当一个队列被同步时,所有其他队列操作都将被阻塞。根据多种因素,队列可能会被同步阻塞数分钟或数小时,在极端情况下甚至几天。 队列同步可以配置如下:

配置 说明
ha-sync-mode: manual 这是默认模式。新队列镜像将不接收现有消息,它只接收新消息。一旦使用者耗尽了仅存在于主服务器上的消息,新的队列镜像将随着时间的推移成为主服务器的精确副本。如果主队列在所有未同步的消息耗尽之前失败,则这些消息将丢失。您可以手动完全同步队列,详情请参阅未同步的镜像部分。
ha-sync-mode: automatic 当新镜像加入时,队列将自动同步。值得重申的是,队列同步是一个阻塞操作。如果队列很小,或者您在RabbitMQ节点和ha-sync-batch-size之间有一个快速的网络,那么这是一个很好的选择。

上一篇:RabbitMQ系列(十)分布式RabbitMQ(集群) 下一篇;