集群架构
写在前面
RabbitMQ集群是按照低延迟环境设计的,千万不要跨越WAN或者互联网来搭建RabbitMQ集群。如果一定要在高延迟环境下使用RabbitMQ集群,可以参考使用Shovel和Federation工具。
RabbitMQ社区中的传统观念要求集群中节点数量的上限在32至64个,因为每向集群添加一个节点,就添加了同步的复杂性。集群中的每个节点必须知道其他节点的信息,这种非线性的复杂度会拖慢消息投递和集群管理。
集群中的队列
RabbitMQ集群设计目的有两个:
- 允许消费者和生产者在RabbitMQ节点崩溃的情况下继续运行;
- 通过添加更多的节点来线性扩展消息通信的吞吐量;
当一个RabbitMQ集群节点崩溃时,该节点上队列的消息也会消失。这事因为RabbitMQ默认不会将队列的内容复制到整个集群上。如果不进行特别的配置,这些消息仅存在于队列所属的那个节点上。
RabbitMQ会始终记录以下四种类型的内部元数据:
- 队列元数据:队列名称和它们的属性;
- 交换器元数据:交换器名称、类型和属性;
- 绑定元数据:一张简单的表格展示了如何将消息路由到队列;
- vhost元数据:为vhost内的队列、交换器和绑定提供命名空间和安全属性;
当你引入集群时,RabbitMQ需要追踪新的元数据类型 —— 集群节点位置,以及节点与已记录的其他类型元数据的关系。集群也提供了将元数据存储到磁盘或内存中的选项。
不是每一个节点都有所有队列的完全拷贝。
在集群中创建队列的话,集群只会在单个节点(而不是所有节点)上创建完整的队列信息。
结果是,只有队列的所有者节点,知道有关队列的所有信息。
其他非队列所有者的节点们,只知道队列的元数据和指向队列存在的那个节点的指针。
因此,当集群节点崩溃时,该节点的队列和关联的绑定就都消失了。
附加在那些队列上的消费者丢失了订阅。
并且任何匹配该队列绑定信息的新消息也丢失了。
如果消费者要消费的队列的所在节点故障了,而该队列是持久化的,那么想要继续消费,唯一的办法就是恢复故障节点。当失败节点恢复后加入集群,该节点上的队列消息不会丢失。
尝试在持久化队列节点故障后,重新声明队列,会得到一个404 NOT_FOUND错误。但如果队列不是持久化的,那么重新声明就会成功。
RabbitMQ设计上不将队列内容和状态复制到所有的节点上,主要有两个原因:
存储空间 —— 如果一个节点可以存储1GB的消息,那么在多个节点之间复制消息则会浪费多个G的空间;
性能因素 —— 消息的发布需要将消息复制到每一个节点,对于持久化消息来说,每次都会触发磁盘活动,每次新增节点,网络和磁盘负载都会增加;
另外,在向队列发送消息时,只有队列的所有节点,才会收到磁盘操作的影响。其他非所有节点,只需要将接收到的消息传递给所有者节点即可。
因此,往Rabbit集群中添加更多的节点,就意味着将拥有更多的节点来传播消息,多个节点会带来性能的提升。
集群中的交换器
交换器,本质上就是一个名称和队列绑定的列表。
当你将消息发布到交换器时,实际上是由连接的信道,根据消息上的路由键,去交换器绑定列表中进行比较,然后路由消息到队列中。
由于交换器只不过是一张表,因此将这张表在集群中复制很简单,当创建一个新的交换器时,RabbitMQ所要做的是将交换器添加到集群中的所有节点上。
集群的消息可靠性与单点模式基本一致。AMQP的Basic.Publish命令不会返回消息的状态。这意味着当信道节点崩溃时,信道可能仍然在路由消息,解决方案是使用事务,或发布确认模式。
集群节点类型
RabbitMQ的集群大致上可分为3类:
- 磁盘节点;
- 内存节点;
- 统计节点;
当创建集群时,至少保证一个磁盘节点和若干个内存节点。无论哪种节点,都不会影响消息持久化。
当有多个磁盘节点时,就能在发生硬件故障时更加游刃有余。
若存在多个磁盘节点,在发生故障恢复时可能会出现状态不一致的情况,这种时候,可关闭集群并按照顺序重新启动节点。
统计节点,只能和磁盘节点搭配使用。它负责收集集群中每个节点的全部统计数据和状态数据。在任何时候,一个集群只能有一个统计节点。
在拥有两个磁盘节点的集群中,如果主节点发生故障,统计节点将指派给备用的磁盘节点。
集群的节点搭配
如果只有一个磁盘节点,当这个节点发生故障后,集群还是可以继续路由消息,但是不能做以下操作:
- 创建队列;
- 创建交换器;
- 创建绑定关系;
- 添加用户;
- 更改权限;
- 添加或删除集群节点;
最好是设置一个以上的磁盘节点,在某个磁盘节点故障时,还有备用的磁盘节点可用。
当内存节点添加到集群或重新加入到集群时,他们会连接到预先配置的磁盘节点,下载集群元数据。当添加内存节点时,确保告知该内存节点所有的磁盘节点。
集群节点升级
集群的升级是半自动化的,在没有准备的情况下解压新版本的RabbitMQ覆盖旧版本,会抹去集群上所有配置和数据。如果要保留这些,需要进行一些操作。
首先,通过Management插件备份配置;
然后,关闭所有生产者并等待所有消费者消费完队列中所有的消息(使用rabbitmqctl观察队列状态);
接着,关闭节点并解压新版本的RabbitMQ;
随后,选择其中一个磁盘节点作为升级节点,当它启动时,该节点会将持久化的集群数据升级到新版本,然后在启动其他的磁盘节点;
最后,启动内存节点,这样就会让集群中运行新版本的RabbitMQ了,而且元数据都会保留;
镜像队列
在RabbitMQ2.6版本后,RabbitMQ团队给集群带来了内建的双冗余选项:镜像队列。
想普通队列那样,镜像队列的主拷贝仅存在于一个节点(主队列,Master)上,但与普通队列不同的是,镜像节点在集群中的其他节点上拥有从队列(slave)拷贝。一旦队列主节点不可用,最老的从队列将被选举为主队列。
在定义队列时,加入参数:
x-ha-policy = all
这意味着队列被镜像到集群中的所有节点上。如果在该队列声明之后,集群又新增了节点,那么该节点也会自动托管一份队列的从拷贝。
镜像队列原理
消息发送到队列时,如果队列是镜像队列,那么也要将消息投递到镜像队列的从拷贝。
想确保消息没有丢失的话,一样可以使用发布确认模式,不同的是,RabbitMQ会在所有队列安全的接收了消息后在通知你。
这里需要有从发布者和消费者两方面说一下异常情况:
- 发布者:如果消息在路由到从拷贝前,主拷贝发生了故障,并且从拷贝升级成了主拷贝,这种情况下,发布确认永远不会到达;
- 消费者:如果镜像队列失去一个节点,则附加在镜像队列上的任何消费者都不会注意到这一样。但是如果主拷贝节点发生故障,那么所有该节点的消费者需要重新附加并监听新的队列主拷贝;
注意:如果消费者连接在从拷贝,当主拷贝发生变化时,RabbitMQ会发送给客户端一个Consumer Cancellation的通知,告知客户端发生了变化。如果你的客户端不支持这个通知处理的话,你的程序会空跑,以为队列中没有消息可以消费了。
如果你的客户端不支持这个消费者取消通知处理,那么应该避免使用镜像队列!!!