RabbitMQ的Mirror Queue集群高可用性

  RabbitMQ算是历史悠久的消息队列了,所以也算得上是在工业界久经考验的长者了。这些东西平常让他跑着一般不会出问题,但是生产环境真是不怕一万就怕万一,为了高可用高可靠而言最好还是使用其mirror特性建立集群做备份。其实RabbitMQ用起来在我们公司运维也没有什么经验,虽然数据库我们玩的很溜了,但是RabbitMQ遇到问题还是一脸懵逼,主要这东西是非主流Erlang所做,而且也涉及到什么Node的概念,上一次血泪的教训就是没事千万不要改节点的名字。这段时间闲着把RabbitMQ官方的文档看了一遍,对其mirror集群的知识翻译下来。
  我们说RabbitMQ的服务端通常都沿用broker术语,其表示一个或者多个Erlang Node,每个都运行着RabbitMQ的程序,并且他们之间共享users、virtual hosts、queues、exchanges、bindings和运行时参数,不过默认情况下queue是不会再每个节点上都存在的(这也是后面要说的mirror queue之所在),通常也把他们打包叫做cluster。
  当多态RabbitMQ运行的时候,他们之间默认没有联系,都运行在以自己为中心的小集群当中,创建一个大集群的时候可以选取其中的一个小集群,然后让其他的主机加入到这个集群当中。

1
2
3
rabbit2$ rabbitmqctl stop_app
rabbit2$ rabbitmqctl join_cluster rabbit@rabbit1
rabbit2$ rabbitmqctl start_app

  在集群运行的过程中,node可以随时加入和撤离开集群,不过当整个集群都停止之后,则必须由最后那个关闭的node开始重启集群,而此时如果先启其他的节点,则该节点将会等待30s那个最后关闭的节点启动,否则的话将会启动失败。这样设计是考虑到最后关闭的node可能保留最多的消息,如果想要强制启动非最后关闭的node,则需要添加forget_cluster_node参数才可以,此时新启动的node将直接被提升为master。

一、Mirror Queue概述

  默认情况下,RabbitMQ Cluster中所有的node都会同步状态和数据,不过queue是个例外——queue存在于那个最先声明他的node上面,而通过cluster中的所有node都可以访问之。在实践中为了增加cluster的高可用,将会手动建立Mirror Queue,然后这些Mirror Queue会生成一个master和多个slave的角色,当master不可用之后,最oldest的slave将会被提升为master。
  在运行的时候,所有发送到queue的消息都会被同步到所有的slave上面去,客户端可以连接到任何一个node,但集群实际上是让其连接到master队列,而当master收到ACK后删除消息的时候,所有slave都将该消息从自己的局部queue中删除。所有slave都按照相同的次序执行和master相同的操作,除了publish之外,所有的操作都先到达master执行,然后再由master广播到所有的镜像队列去执行。由此可见,Mirror Queue的作用是增加多个副本执行相同的操作,以提高整体集群的可用性,而没有在这些镜像中做负载均衡。因为这种机制,不推荐Cluster跨WAN部署,因为这样会导致所有的操作有较大的延时,导致集群的性能降低。

二、Cluster的配置

  RabbitMQ集群的Mirror Queue据说可以在声明队列的时候,通过x-arguments指定,但是在librabbitmq-c库中使用貌似没有生效。然而,官方推荐的方式还是通过policies的方式来指定:通过命令行或其他的方式,指明policy通过正则表达式可以匹配一条或者多条queue进行操作和更新,而且可以在任何时候执行操作。

1
rabbitmqctl set_policy ha-all "^paybank\." '{"hamode":"all"}'

  通过上面的命令,可以将集群中所有以”paybank.”开头的queue在集群中的所有node都进行mirror(这一效果还包括后面加入集群的新node),

三、内部实现原理和注意事项

3.1 当节点挂掉之后

  如果是slave节点挂掉,我们可以从日志中得到相关信息,但是master仍然是master,整个集群正常工作;而如果是master挂掉了,那么其中的一个slave将会被提升为master:
  最oldest(最长运行时间)的节点将会被提升为master,因为其被认为最大限度的和master同步的节点。如果新master和旧master有未同步的消息,那么那些消息将会永久的丢失掉。
  此时所有发送给客户端但是还没有被ACK的消息都会被重新排队安排发送(requeue)。很有可能客户端已经消费了该条消息,但是确认消息没有被新master接收,但是对此新master无能为力,只能重发没有被确认的消息。当然,如有必要客户端需要识别消息是否已经被消费过,必定MQ的消息防重是最基本的共识了。此处noACK=true和手动确认消息的差异就比较大了,对于这种消费者消息发送出去后,如果master挂掉后,消息可能会也可能不会被客户端收到,而且新的master不会requeue这些消息,这些消息如果客户都没有收到就会被永久丢失。
  即使旧master挂掉,但是那些已经被master收到然后发出到mirror队列的消息都不会被丢失,那些发送到某些节点的消息会被路由到新master节点,然后再由master节点同步到所有的slave节点;当master挂掉后,客户端仍然可以发送消息,然后这些消息仍然会被丢到mirror中,一直到新的master被选取出。
  客户端使用publisher confirm模式发送消息,即使在消息发送和confirm被收到过程中mster挂了,客户端还是会收到确认消息的,对于publisher看来向mirror队列发送消息和向一个非mirror队列发送消息没有区别。

3.2 Mirrors同步

  节点可能会在任意时刻加入到Cluster当中,当一个新node加入集群之后,根据policy将会在其上增加mirror queue,但此时的queue是空的。而且,如果是之前已经加入到该集群的节点,现在重新加入,该节点也会自动将之前所有的信息全部清空。默认情况下此时该queue会接收新发布过来的消息,即从当前时刻开始消息收集,而对之前集群的消息一无所知也一无所获。只有整个集群将之前的消息全部消费后,该新加入的节点才被认为是和整个集群完全同步了。
  对于高可靠性要求的集群来说,如果此时旧master挂掉而没有完全同步的节点被提升为新master,那么之前缺失的那段消息将会永久丢失。解决的方式就是新加入的节点显式要求完全同步(explicit sync):

1
2
rabbitmqctl list_queues name slave_pids synchronised_slave_pids
rabbitmqctl sync_queue name

  一旦queue进行同步,所有的操作都将被阻塞掉直到同步结束。根据消息的规模同步操作可能很快,也可能持续数天,此时操作需要甄别。
  当被mirror的queue是被声明为durable的队列,而当集群中所有的node都依次关闭后,该队列中durable的消息将会被保存,当下次最后关闭的机器重启成为master后,所有的持久化内容将会被复原。

本文完!

参考