真实项目开发中除了单体应用,一般分布式项目都会用到消息中间件,又由于需要保证整个应用体系的高可用,就有必须了解一下消息中间件MQ的高可用集群方案

  • 单一模式【单机模式,一般学习练习使用】
  • 普通集群模式
  • 镜像集群模式

MQ集群模式

RabbitMQ这款消息队列中间件产品本身是基于Erlang编写,Erlang语言天生具备分布式特性(通过同步Erlang集群各节点的magic cookie来实现)

一个rabbitmq集 群中可以共享 user,vhost,queue,exchange等,所有的数据和状态都是必须在所有节点上复制的,一个例外是,那些当前只属于创建它的节点的消息队列,尽管它们可见且可被所有节点读取。rabbitmq节点可以动态的加入到集群中,一个节点它可以加入到集群中,也可以从集群环境中移除。
集群中有两种节点

  1. 内存节点:只保存状态到内存(一个例外的情况是:持久的queue的持久内容将被保存到disk)
    【内存节点虽然不写入磁盘,但是它执行比磁盘节点要好。集群中,只需要一个磁盘节点来保存状态 就足够了如果集群中只有内存节点,那么不能停止它们,否则所有的状态,消息等都会丢失】
  2. 磁盘节点:保存状态到内存和磁盘。

    良好的集群设计 :在一个集群里,有3台以上机器,其中1台使用磁盘模式,其它使用内存模式。其它几台为内存模式的节点,无疑速度更快,因此客户端(consumer、producer)连接访问它们。而磁盘模式的节点,由于磁盘IO相对较慢,因此仅作数据备份使用。

普通集群模式

普通集群模式也是MQ集群默认的集群模式

RabbitMQ 集群模式 - 图1

上面图中采用三个节点组成了一个RabbitMQ的集群,Exchange A的元数据信息在所有节点上是一致的,而Queue(存放消息的队列)的完整数据则只会存在于它所创建的那个节点上。其他节点只知道这个queue的metadata信息和一个指向queue的owner node的指针。

默认的集群模式,queue创建之后,如果没有其它policy,则queue就会按照普通模式集群。对于Queue来说,消息实体只存在于其中一个节点,node1/2/3节点仅有相同的元数据,即队列结构,但队列的元数据仅保存有一份,即创建该队列的rabbitmq节点(node1节点),当node1节点宕机,你可以去其node2节点查看,发现该队列已经丢失,但声明的exchange还存在。

集群消息消费过程图:

RabbitMQ 集群模式 - 图2

  • 当消息存储在node1节点的时候,但是consumer从node2节点拉取,node1和node2节点就面临节点之间消息的传递,把node1节点的消息先传输到node2节点上,然后consumer才能拉取到对应消息

RabbitMQ集群元数据的同步:

  1. 队列元数据:队列名称和它的属性
  2. 交换器元数据:交换器名称、类型和属性
  3. 绑定元数据:一张简单的表格展示了如何将消息路由到队列
  4. vhost元数据:为vhost内的队列、交换器和绑定提供命名空间和安全属性

集群的元数据相同当用户访问其中任何一个RabbitMQ节点时,通过rabbitmqctl查询到的queue/user/exchange/vhost等信息都是相同的

普通集群出现的问题:

  • 在sonsumer在node2节点上拉取消息的,此时node1节点宕机了,那么久无法获取msg1消息,即使node1做了消息的持久化,那么要正确获取到msg1消息也需要等node1恢复才可被消费,如果没有进行消息的持久化,那么msg1消息就丢失了
  • 为什么RabbitMQ不将队列复制到集群里每个节点呢
    • 这与它的集群的设计本意相冲突,集群的设计目的就是增加更多节点时,能线性的增加性能(CPU、内存)和容量(内存、磁盘)。当然RabbitMQ新版本集群也支持队列复制(有个选项可以配置)。比如在有五个节点的集群里,可以指定某个队列的内容在2个节点上进行存储,从而在性能与高可用性之间取得一个平衡(应该就是指镜像模式)
    • 存储空间,如果每个集群节点都拥有所有Queue的完全数据拷贝,那么每个节点的存储空间会非常大,集群的消息积压能力会非常弱(无法通过集群节点的扩容提高消息积压能力
    • 性能,消息的发布者需要将消息复制到每一个集群节点,对于持久化消息,网络和磁盘同步复制的开销都会明显增加
  • 普通集群模式搭建

镜像集群模式

简单一句话:把需要的队列做成镜像队列,存在于多个节点,属于RabbitMQ的HA方案,镜像模式是在普通模式的基础上,增加一些镜像策略

镜像模式解决了普通模式的问题,其实质和普通模式不同之处在于,消息实体会主动在镜像节点间同步,而不是在consumer取数据时临时拉取。该模式带来的副作用也很明显,除了降低系统性能外,如果镜像队列数量过多,加之大量的消息进入,集群内部的网络带宽将会被这种同步通讯大大消耗掉。所以在对可靠性要求较高的场合中适用,一个队列想做成镜像队列,需要先设置policy,然后客户端创建队列的时候,rabbitmq集群根据“队列名称”自动设置是普通集群模式或镜像队列,具体如下:

队列通过策略来使能镜像。策略能在任何时刻改变,rabbitmq队列也近可能的将队列随着策略变化而变化;非镜像队列和镜像队列之间是有区别的,前者缺乏额外的镜像基础设施,没有任何slave,因此会运行得更快。为了使队列称为镜像队列,你将会创建一个策略来匹配队列,设置策略有两个键“ha-mode和 ha-params(可选)”

ha-params根据ha-mode设置不同的值,下面表格说明这些key的选项:

RabbitMQ 集群模式 - 图3

语法讲解: 在cluster中任意节点启用策略,策略会自动同步到集群节点

  1. rabbitmqctl set_policy-p/ha-all"^"'{"ha-mode":"all"}'

这行命令在vhost名称为hrsystem创建了一个策略,策略名称为ha-allqueue,策略模式为 all 即复制到所有节点,包含新增节点,策略正则表达式为 “^” 表示所有匹配所有队列名称。例如:

  1. rabbitmqctl set_policy-p/ha-all"^message"'{"ha-mode":"all"}'

注意:^message这个规则要根据自己修改,这个是指同步“message”开头的队列名称,我们配置时使用的应用于所有队列,所以表达式为“^”

官方set_policy说明参见 :set_policy [-p vhostpath] {name} {pattern} {definition} [priority]

  1. nodes策略和迁移master

    需要注意的是设置和修改一个“nodes”策略将不会引起已经存在的master离开,尽管你让其离开。比如:如果一个队列在{A},并且你给它一个节点策略告知它在{B C},它将会在{A B C}。如果节点A失败或者停机了,那个节点上的镜像将不回来且队列将继续保持在{B C}(注:当队列已经是镜像队列且同步到其它节点,就算原节点宕机,也不影响其它节点对此队列使用)

  2. 创建策略例子

队列名称以“ha.”开头的队列都是镜像队列,镜像到集群内所有节点:
RabbitMQ 集群模式 - 图4

列名称以“two.”开头的队列,其策略镜像到集群内任何两个节点:

RabbitMQ 集群模式 - 图5

队列同步到指rabbitmq 节点 ,rabbitmqctl:

  1. ./rabbitmqctl set_policy sa-specify "^sa\.specify\." '{"ha-mode":"nodes","ha-params":["rabbit@is137","rabbit@raxtone"]}'

切记,需要把队列同步到的节点都写进去。

镜像模式结构图

RabbitMQ 集群模式 - 图6

  • 镜像队列基本上就是一个特殊的BackingQueue,它内部包裹了一个普通的BackingQueue做本地消息持久化处理,在此基础上增加了将消息和ack复制到所有镜像的功能。所有对mirror_queue_master的操作,会通过可靠组播GM的方式同步到各slave节点。GM负责消息的广播,mirror_queue_slave负责回调处理,而master上的回调处理是由coordinator负责完成。mirror_queue_slave中包含了普通的BackingQueue进行消息的存储,master节点中BackingQueue包含在mirror_queue_master中由AMQQueue进行调用。
  • 消息的发布(除了Basic.Publish之外)与消费都是通过master节点完成。master节点对消息进行处理的同时将消息的处理动作通过GM广播给所有的slave节点,slave节点的GM收到消息后,通过回调交由mirror_queue_slave进行实际的处理。
  • 对于Basic.Publish,消息同时发送到master和所有slave上,如果此时master宕掉了,消息还发送slave上,这样当slave提升为master的时候消息也不会丢失。

GM(Guarenteed Multicast)

GM模块实现的一种可靠的组播通讯协议,该协议能够保证组播消息的原子性,即保证组中活着的节点要么都收到消息要么都收不到。

它的实现大致如下:

将所有的节点形成一个循环链表,每个节点都会监控位于自己左右两边的节点,当有节点新增时,相邻的节点保证当前广播的消息会复制到新的节点上;当有节点失效时,相邻的节点会接管保证本次广播的消息会复制到所有的节点。在master节点和slave节点上的这些gm形成一个group,group(gm_group)的信息会记录在mnesia中。不同的镜像队列形成不同的group。消息从master节点对于的gm发出后,顺着链表依次传送到所有的节点,由于所有节点组成一个循环链表,master节点对应的gm最终会收到自己发送的消息,这个时候master节点就知道消息已经复制到所有的slave节点了。

新增节点

RabbitMQ 集群模式 - 图7

每当一个节点加入或者重新加入(例如从网络分区中恢复过来)镜像队列,之前保存的队列内容会被清空。

节点的失效

如果某个slave失效了,系统处理做些记录外几乎啥都不做。master依旧是master,客户端不需要采取任何行动,或者被通知slave失效。 如果master失效了,那么slave中的一个必须被选中为master。被选中作为新的master的slave通常是最老的那个,因为最老的slave与前任master之间的同步状态应该是最好的。然而,需要注意的是,如果存在没有任何一个slave与master完全同步的情况,那么前任master中未被同步的消息将会丢失。

消息的同步

  • 将新节点加入已存在的镜像队列是,默认情况下ha-sync-mode=manual镜像队列中的消息不会主动同步到新节点,除非显式调用同步命令。
  • 当调用同步命令后,队列开始阻塞,无法对其进行操作,直到同步完毕。
  • ha-sync-mode=automatic时,新加入节点时会默认同步已知的镜像队列。由于同步过程的限制,所以不建议在生产的消费队列中操作。

镜像节点在集群中的其他节点拥有从队列拷贝,一旦主节点不可用,最老的从队列将被选举为新的主队列。但镜像队列不能作为负载均衡使用,因为每个操作在所有节点都要做一遍。该模式带来的副作用也很明显,除了降低系统性能外,如果镜像队列数量过多,加之大量的消息进入,集群内部的网络带宽将会被这种同步通讯大大消耗掉。所以在对可靠性要求较高的场合中适用。

完善集群结构图:

RabbitMQ 集群模式 - 图8