波斯马BOSSMA Information Technology

高可用(镜像)队列

发布时间:2017年10月28日 / 分类:RabbitMQ / 6,258 次浏览 / 评论

这篇文章翻译自:http://www.rabbitmq.com/ha.html

默认情况下,集群中的队列仅位于其中某一个节点上(队列首次定义的节点)。这与交换机和绑定不同,它们总被视为在所有的节点上。队列还可以在多个节点上设置镜像。每个镜像队列包含一个master和多个mirror,如果master由于某些原因消失了,最早创建的mirror将被提升为新的master。

发送到队列的消息会被复制到所有的镜像。不管消费者连接的是哪个节点,都会被连接到master,在镜像上删除消息都需要已经通过master确认。队列镜像因此提高了可用性,但是它不会在结点之间分发负载(每一个参与的节点都会处理所有的工作)。

这个解决方案需要一个RabbitMQ集群,这意味着它不能无缝的应对网络分区,也因此,不推荐在广域网使用(当然这种情况下,客户端仍可以按照需要就近连接)。

在分布式系统中有多种常用的说法用来表示主要的和从属的副本。本文通常使用master来代表队列的主要副本,使用mirror代表从属副本。然而,你也会看到slave被到处使用。这是因为RabbitMQ CLI工具的历史原因,它曾使用slave来代表从属副本。因此mirror和slave目前可以替换使用,但是我们最终还是会移除slave这个旧说法。

如何配置镜像

镜像使用参数policies进行配置。policy通过名称匹配一个或多个队列(使用正则表达式),然后将其包含的定义(一组可选参数)添加到匹配队列的属性集合中。更多关于策略的信息请看运行时参数和策略

镜像控制参数

正如上边说的,我们可以通过policy来启用镜像。策略可以在任意时刻进行修改;可以先创建一个非镜像的队列,然后再将它配置为镜像的,反之亦然。一个非镜像队列和一个没有任何镜像的镜像队列也是不同的,前者缺少镜像基础设施的扩展,将可能提供更高的吞吐量。

你可能已经琢磨出来添加镜像到队列的方式。

为了使队列变成镜像的,你需要创建一个policy ,通过它可以匹配到相应的队列,同时设置policy键:ha-mode和ha-params(可选的)。下面的表格将解释这些键的可选项:

ha-mode ha-params Result
all (无) 队列会被镜像到集群中所有的节点。当一个新的节点加入集群后,队列将被镜像到这个新节点。
exactly 数量 集群中镜像队列实例的数量。数字1表示只有队列master,没有mirror。如果运行队列master的节点不可用,队列将不可用。数字2表示1个队列master和1个队列mirror。如果运行队列master的节点不可用,队列mirror将自动提升为master。综上所述,mirror队列的数量=此配置数量-1。如果集群中节点数量少于此配置数量,队列将被镜像到所有的节点。如果集群中节点数量多于此配置数量,在一个包含mirror的节点宕掉时,一个新的mirror将在另一个其它节点上创建。通过配置“ha-promote-on-shutdown”: “always”使用完全模式是很危险的,因为在队列宕掉时镜像可能会是未同步的,但集群内的队列将进行迁移。
nodes 一组节点名称 队列将被镜像到列出的节点名称中。节点名称是Erlang节点名称,他们出现在rabbitmqctl cluster_status中;他们经常是这种格式“rabbit@hostname”。如果某个节点名称不在集群中,这不会被算作一个错误。如果声明队列时列表中没有任何一个节点是在线的,队列将在声明客户端所连接的节点创建。

当队列的HA策略改变时,它将尽力保持现有的镜像,直到适配新的策略。

镜像需要多少节点?

注意镜像对于所有的队列都是最保守的选项,在很多情况下都是不需要的。对于3个或更多节点数的集群,推荐镜像到法定数目(大多数)的节点,例如3个节点的集群是2个节点,5个节点的集群是3个节点。因为某些数据是天生瞬时的或者非常时间敏感的,所以对一些队列使用较少的mirror节点也是相当合理的(甚至不使用任何镜像)。

Master、Master迁移、数据位置

Master位置

每个队列都有一个home节点,这个节点叫做队列master。所有的队列操作都通过master,然后复制到所有的mirror。这需要保证消息的先进先出顺序。

通过策略可以改变队列master的分布。使用这个策略可以通过三种方式,使用队列声明参数“x-queue-master-locator”,设置策略“queue-master-locator”,在配置文件定义“queue_master_locatorkey”。这个策略的可选项:

  • min-masters:选择master数量最少的节点
  • client-local:选择声明队列的客户端连接的节点
  • random:随机节点

节点策略和迁移Master

?注意如果设置或者修改策略“nodes”(ha-mode=nodes)时新的策略不包括当前master,会使当前的master离开。为了防止消息丢失,RabbitMQ将会保持现有的master,直到至少一个mirror已经同步(即使这需要很长时间)。然而一旦同步发生,事情将会像节点失败一样进行:消费者将从master断开连接,然后需要进行重连。例如,如果队列在[A B](A是master)节点,你设置了一个新的nodes策略为[C D],它首先会变为[A C D]。只要队列同步到新的mirror[C D],A节点上的master就会关闭。

专用队列

当声明它们的连接关闭时,专用队列将被删除。因此,对于专用队列设置镜像(或者对master持久化)是没有用的,这就像它寄宿的节点关闭时,连接将被关闭,队列将被某种方式删除。

因此,专用队列永远不会被镜像,即使它们匹配到某个策略。他们永远不会持久化,即使声明如此。

例子

下面的例子将声明一个名字为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界面
  • 进入 Admin > Policies > Add / update a policy.
  • 在Name中输入”ha-all”,在Pattern中输入 “^ha\.”, 在Definition中添加 “ha-mode” = “all” 。
  • 点击Add policy.

下边的策略匹配名字以“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界面
  • 进入 > Policies > Add / update a policy.
  • 在Name中输入 “ha-two”,在 Pattern中输入”^two\.” 。
  • 在Definition中添加三行:”ha-mode” = “exactly” , “ha-params” = 2? “Number”, “ha-sync-mode” = “automatic”
  • 点击 Add policy.

下边的策略匹配名字以“nodes.”开头的队列,并镜像到集群中指定的节点:

abbitmqctl
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界面
  • 进入 Admin > Policies > Add / update a policy。
  • 在Name中输入”ha-nodes” ,在Pattern中输入”^nodes\.”。
  • 在Definition中添加两行”ha-mode” = “nodes” , “ha-params” “rabbit@nodeA” 和 “rabbit@nodeB” “List”。
  • 点击 Add policy。

镜像队列的实现和语义

正如上文所述,每个镜像队列都有一个master和若干个mirror,它们每个都在不同的节点上。mirror严格按照master上相同的顺序应用发生在master上的操作,因此master和mirror保持相同的状态。除了发布以外的所有动作都仅在master执行,然后master广播动作的影响到mirror。因此客户端从一个镜像队列消费实际上是从master消费。

如果某个mirror失败,除了记账之外不需要做什么:master仍旧是master,客户端也不需要执行任何动作或者去了解这个故障。注意镜像故障可能不会被立即检测到,同时连接中断的流程控制机制会延迟消息发布。详细的描述看这里

如果master失败了,其中一个mirror将如下文所述被提升为master:

1、运行时间最长的mirror被提升为master,采取这个方案基于它最可能完整的同步了master。如果没有镜像和master是同步的,master上存在的消息将丢失。

2、这个mirror将关注之前被突然中断连接的所有消费者。它会将之前已经投递给客户端但是没有ACK的消息重新入队列。这包括客户端已经处理了消息的ACK,但是ACK在到达队列master所在节点的线路上丢失,或者ACK在从master到mirror的广播过程中丢失。所有这种情况下,新的master没有别的选择,只能重新将没有收到ACK的所有消息重新入队列。

3、请求当队列故障转移时被通知的消费者将收到取消通知。

4、由于重新入队列,客户端从队列重新进行消费时必须意识到它们可能会收到已经接收过的消息。

5、由于被选中的mirror成为master,在这个过程中发送到此mirror队列的消息将不会丢失(除非这个节点随后也故障了)。消息发送到mirror队列节点时,将被路由到队列master,然后再被复制到所有的mirror。如果master挂掉时,消息被继续发送到了mirror,一旦提升mirror成为master完成,此消息将被添加到队列。

6、使用发布者确认机制情况下,在消息已经发布和发布者收到确认消息之间,即使master(或者任何mirror)挂掉,消息仍旧会被确认。站在发布者的角度看,发布到一个镜像队列和发布到一个非镜像队列没有什么不同。

如果你正在从一个镜像队列消费,使用noAck=true(也就是客户端不会发送消息ACK),消息将可能丢失。这与标准规范没有区别:一旦消息被发向一个noAck=true的消费者,代理就会认为消息已经Ack了。假设此时客户端突然断开连接,消息可能没有被接收到。在镜像队列场景下,如果master挂掉,发向noAck=true的消费者的正在路上的消息可能永远不会被客户端接收到,消息也不会被新的master重新入队列。由于消费者客户端连接的节点可能幸存下来,消费者取消通知将可用来确认此类事件的发生。当然,实际操作中,如果你希望不丢失消息,应该在消费时使用noAck=true。

发布者确认和事务

镜像队列支持发布者确认事务。在确认和事务的情况下,其语义都是操作跨越队列的所有镜像。在事务的场景下,当事务已经应用到队列的所有镜像,一个tx.commit-ok将返回给客户端。同样的,在发布者确认场景下,当所有镜像已经接受了某个消息,确认消息将发送给发布者。这个语义可以认为是消息被路由到多个普通队列,发布事务也类似被路由到多个队列。

流量控制

RabbitMQ使用一种基于授信(Credit)的算法来限制消息发布的速度。当发布者收到队列所有镜像的授信时,它才被允许进行发布。授信在上下文中意味着允许发布。如果镜像授信失败,则会引起发布者暂停。发布者将被阻塞,直到所有的镜像提供了授信,或者直到剩余的节点认为出现问题的镜像已经从集群断开。Erlang通过周期性的发送一个心跳到所有节点,来检测这种断开连接的情况。心跳间隔可以通过配置net_ticktime设置。

Master故障和消费者取消

正在从镜像队列消费的客户端可能希望知道他们消费的队列是否发生了故障转移。当镜像队列故障转移时,到消费者的消息发送记录会丢失,此时所有未确认的消息将设置redelivered标识重新投递。消费者可能希望知道这将要发生。

如果这样,客户端可以在消费时使用参数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);

这将创建使用上边的参数创建一个新的消费者。

未同步镜像

节点可以在任意时间加入集群。根据队列配置,当新的节点加入集群时,队列可能在这个新的节点添加一个mirror。此时,新的mirror将是空的:它不会包含队列已经存在的任何内容。这个mirror将会接收发送到队列的新消息,因此一段时间后它将可以准确的代表镜像队列的队尾。由于消息逐渐从镜像队列取走,新mirror错过的消息的队列头部将逐渐缩小,直到mirror的内容与master的内容完全匹配。此时,这个mirror可以被认为是完全同步的,需要重点说明的是这是因为客户端不断的从已有队列头部取走消息的行为而发生的。

一个新添加的mirror不会提供其加入之前已经存在于队列的内容的冗余或可用性,除非这个队列是显式同步的。因为当显式同步发生时队列会变得迟钝,所以最好在正在消耗消息的活动队列上自然的同步,仅在不活动的队列上执行显式的同步。

你可以使用下边的rabbitmqctl命令了解到哪些镜像是同步的:

rabbitmqctl list_queues name slave_pids synchronised_slave_pids

你可以手动同步一个队列:

rabbitmqctl sync_queue name

你还可以取消同步:

rabbitmqctl cancel_sync_queue name

这些功能在管理插件中也是可用的。

停止节点和同步

如果你停止一个包含镜像队列master的RabbitMQ节点,其它节点上的某个mirror将被提升为master(假设存在一个已经同步的mirror)。如果你继续关闭节点,镜像队列将只有一个节点,一个master。在镜像队列声明为持久化的情况下,如果最后保留的这个节点关闭,持久化的消息将在节点重启后保留下来。通常情况下,当你重启其它节点时,如果它们之前是某个镜像队列的一部分,他们将重新加入这个镜像队列。

然而,现在没有方法使mirror知道它的队列内容与重新加入的master是否偏离(这可能发生在网络分区时)。这种情况下,当mirror重新加入镜像队列时,它会扔掉所有已持久化到本地的内容,然后空启动。这与一个新的节点加入集群的行为是相同的。

只有未同步的Mirror时停止Master节点

当Master节点停止时,可能所有可用的mirror都是未同步的。一种常见的场景是回滚集群升级时。默认情况下,当master可控关闭时,为了避免消息丢失,RabbitMQ将拒绝故障转移到一个未同步的mirror(例如:显式关闭RabbitMQ服务或者关闭操作系统);此时整个队列将关闭,就像未同步的mirror不存在一样。当mater不可控的关闭时(例如:服务器或者节点崩溃,或者网络中断),仍将触发故障转移到一个未同步的mirror。

如果你愿意在所有的场景下master节点都可以故障转移到未同步的mirror(例如:你选择可用性超过避免消息丢失),你可以设置ha-promote-on-shutdown策略为always替换默认的when-synced

所有Mirror停止时失去Master

当队列的所有Mirror关闭时可能丢失队列的Master。正常的操作下,队列最后关闭的节点将成为master,并且我们想要这个节点重启后仍旧是master(因为它可能有其它Mirror没有收到的消息)。

然而,当你调用rabbitmqctl forget_cluster_node时,RabbitMQ将尝试为每个队列找到一个最近停止的mirror(这些队列的master在我们正在忘却的节点上),然后当这个mirror重新启动时提升为新的master。如果有超过1个的候选者,最近停止的mirror将被选中。

需要重点理解的是,在forget_cluster_node执行时,RabbitMQ只能提升已经停止的mirror,因为被重新启动的mirror将如上文“停止节点和同步”所述清除它们自己的内容(译注:如果能提升重新启动后的mirror,就意味着允许丢失所有消息,而这不是默认想要的)。因此当从一个停止的集群中移除master时,你必须在重新启动mirror前调用rabbitmqctl forget_cluster_node(译注:否则消息将全部丢失)

批量同步

从RabbitMQ3.6.0开始,master使用批量方式进行同步。批量方式可以通过队列参数ha-sync-batch-size进行配置。之前的版本默认情况下同时只同步一条消息。通过批量同步消息,同步处理被相当的加快。

为给ha-sync-batch-size设置正确的值,你需要考虑:

  • 消息平均大小
  • RabbitMQ节点之间的网络吞吐量
  • net_ticktime的值

例如,如果你设置ha-sync-batch-size为50000条消息,并且队列中每个消息的大小是1KB,那么每个同步消息的节点之间大约是49M。你需要确认队列mirror之间的网络可以容纳这个通信量。如果网络需要比net_ticktime更长的时间来发送一包批量数据,集群中的节点会认为他们存在于网络分区的不同部分。

配置同步

让我们从队列同步最重要的方面开始:当某个队列正在被同步时,所有其它的队列操作将被阻塞。考虑到多种因素,队列可能被同步阻塞若干分钟或小时,极端情况下甚至数天。

队列同步可以如下配置:

ha-sync-mode: manual – 这是默认模式。新的队列mirror将不能收到已存在的消息,它将只能收到新的消息。一段时间后,当消费者已经全部取走只存在于master上的消息,新的队列mirror将成为master的精确复制品。如果master队列在未同步消息被耗尽之前失败,这些消息将丢失。你完全可以手动同步队某个队列,详细参考“未同步镜像”。

ha-sync-mode: automatic – 当新的mirror加入时,队列将自动同步。需要反复重申的是队列同步是一个阻塞操作。如果队列比较小,或者RabbitMQ节点之间的网络比较快而且ha-sync-batch-size是优化过的,这是一个好的选择。

本博客所有文章如无特别注明均为原创。
复制或转载请以超链接形式注明转自波斯马,原文地址《高可用(镜像)队列

关键字:

建议订阅本站,及时阅读最新文章!
【上一篇】 【下一篇】

发表评论