RabbitMQ 有着非常不错的性能,和应对各种故障的能力。它能在集群中最大化的发挥自身优势,一起来看下吧。
RabbitMQ 最优秀的功能之一就是其内建集群,它能够将集群在5分钟内搭建并运行起来。RabbitMQ 内建集群的设计用于完成两个目标:
- 允许消费者和生产者在 Rabbit 节点崩溃的情况下继续运行。
- 通过添加更多的节点来线性扩展消息通信吞吐量。
为了能更好地理解,让我们看一下RabbitMQ集群架构。
集群架构
我们知道了队列和交换器的含义,以及如何将它们绑定到一起,也知道该使用哪些类型的交换器。但是这背后到底发生了什么呢?RabbitMQ 是如何记录所有你使用的各种基础构件,同时它们又如何装配成一个消息通信服务器的呢?
RabbitMQ 会始终记录以下四种类型的内部元数据:
- 队列元数据——队列名称和它们的属性(是否可持久化,是否自动删除?)
- 交换器元数据——交换器名称、类型和属性(可持久化等)
- 绑定元数据—— 一张简单的表格展示了如何将消息路由到队列
- vhost元数据——为vhost内的队列、交换器和绑定提供命名空间和安全属性
在单一节点内,RabbitMQ 会将所有这些信息存储在内存中,同时将那些标记为可持久化的队列和交换器(以及它们的绑定)存储到硬盘上。存储到硬盘上可以确保队列和交换器在重启 RabbitMQ 节点后重新创建叫;
当在集群中时,RabbitMQ 需要追踪新的元数据类型:集群节点位置,以及节点与已记录的其他类型元数据的关系。集群也提供了选择:将元数据存储到磁盘上(独立节点的默认设置)。
集群中的队列
在单一节点设置中,所有关于队列的信息(元数据、状态和内容)都完全存储在该节点上,见下图。但是如果在集群中创建队列的话,集群只会在单个节点而不是在所有节点上创建完整的队列信息(元数据、状态、内容)。结果是只有队列的所有者节点知道有关队列的所有信息。所有其他非所有者节点只知道队列的元数据和指向该队列存在的那个节点的指针。因此当集群节点崩溃时,该节点的队列和关联的绑定就都消失了。附加在那些队列上的消费者丢失了其订阅信息,并且任何匹配该队列绑定信息的新消息也都丢失了。
集群中的队列
当队列最开始没有被设置成可持久化时,可以让消费者重连到集群并重新创建队列,创建成功后重新新绑定它们并传输数据;如果重新创建的队列被标记成持久化了,那么在其他节点上重新声明它们的话会得到一个难看的404 NOT_FOUND错误。这样确保了当失败节点恢复后加入集群,该节点上的队列消息不会丢失。想要该指定队列重回集群的唯一方法是恢复故障节点。
为什么默认情况下RabbitMQ不将队列内容和状态复制到所有的节点上呢?有两个原因:
- 存储空间——如果每个集群节点都拥有所有队列的完整拷贝,那么添加新的节点不会给你带来更多存储空间。
- 性能——消息的发布需要将消息复制到每一个集群节点。对于持久化消息来说,每一条消息都会触发磁盘活动。每次新增节点,网络和磁盘负载都会增加,最终只能保持集群性能的平稳(甚至更糟)。
通过设置集群中的唯一节点来负责任何特定队列,只有该负责节点才会因队列消息而遭受磁盘活动的影响。所有其他节点需要将接收到的该队列的消息传递给该队列的所有者节点。因此,往Rabbit集群添加更多的节点意味着你将拥有更多的节点来传播队列,这些新增节点为你带来了性能的提升。当负载增加时,RabbitMQ 集群是性能扩展的最佳方案。
分布交换器
交换器事实上并不是一个活生生的实体,当将消息发布到交换器时,实际上是由你所连接到的信道将消息上的路由键同交换器的绑定列表进行比较,然后路由消息。正是信道(channel)按照绑定匹配的结果,将消息路由到队列。
注意:理解 RabbitMQ 背后的消息路由工作机制的方法是把每个队列想象成节点上运行的进程,每个进程拥有自己的进程 ID(PID)。交换器只不过是路由模式列表和匹配消息应发往的队列进程 ID 列表。当发布的消息匹配了交换器中的绑定规则时,实际上是由信道完成了匹配工作,并在匹配之后建立到队列 PID 的连接,然后将消息传送过去。队列的进程ID本质上是其在集群中的 Erlang 地址。
由于交换器只不过是一张查询表,而非实际上的消息路由器,因此将交换器在整个集群中进行复制会更加简单,见下图。举例来说,当创建一个新的交换器时,RabbitMQ 所要做的是将查询表添加到集群中的所有节点上。这时,每个节点上的每条信道都可以访问到新的交换器了。
- 对于集群中的单一节点来说,集群中的每个节点拥有每个交换器的所有信息。
-
就可用性来讲,不用担心在节点故障时重新声明交换器。只需让故障节点上的生产者重新连接到集群上,它们立即就能开始往交换器上发布消息了。
集群中的交换器
但在路由完成之前节点发生故障的话,这些消息会怎样呢?
AMQP 的 basic.publish
命令不会返回消息的状态。这意味着当信道节点崩溃时信道可能仍然在路由消息,而生产者已经继续创建下一条消息了。在这种情况下,你将承受丢失消息的风险。解决方法是:
- 使用AMQP事务,在消息路由到队列之前它会一直阻塞
- 使用发送方确认(publisher confirm)模式来记录连接中断时尚未被确认的消息。
这两种解决方案都能帮助你在节点故障并且目的队列不复存在时检测到消息无法路由的情况,并能确保应用程序一直发布而不丢失一条消息。
节点
RabbitMQ 节点只有两种,要么是内存节点(RAM node),要么是磁盘节点(disk node)。
- 内存节点将所有的队列、交换器、绑定、用户、权限和vhost的元数据定义都仅存储在内存中。
- 磁盘节点则将元数据存储在磁盘中。
单节点系统只允许磁盘类型的节点,这样能保证重启RabbitMQ之后,所有关于系统的配置信息都会丢失;在集群中,可以选择配置部分节点为内存节点。
集群中的交换器
当在集群中声明队列、交换器或者绑定的时候,这些操作会直到所有集群节点都成功提交元数据变更后才返回。对于内存节点来说,这意味着将变更写入内存;而对于磁盘节点来说,这意味着昂贵的磁盘写入操作,直到完成。
RabbitMQ只要求在集群中至少有一个磁盘节点。所有其他节点可以是内存节点。记住,当节点加入或者离开集群时,它们必须要将该变更通知到至少一个磁盘节点。如果只有一个磁盘节点,而且不凑巧的是它刚好又崩溃了,那么集群可以继续路由消息,但是不能做以下的操作了:
- 创建队列
- 创建交换器
- 创建绑定
- 添加用户
- 更改权限
- 添加或删除集群节点
换句话说,如果集群中唯一的磁盘节点崩溃的话,集群仍然可以保持运行,但是直到将该节点恢复到集群前,你无法更改任何东西。解决方案是在集群中设置两个磁盘节点,因此它们中至少有一个是可用的,能在任何时候保存元数据变更。只有一个需要所有的磁盘节点必须在线的操作是添加或者删除集群节点。当内存节点重启后,它们会连接到预先配置的磁盘节点,下载当前集群元数据拷贝。如果你只将两个磁盘节点中的一个告诉了该内存节点,而不凑巧的是当内存节点供电中断时该磁盘节点也发生故障的话,那么内存节点在重启之后就无法找到集群了。所以当添加内存节点时,确保告知其所有的磁盘节点(内存节点唯一存储到磁盘的元数据信息是集群中磁盘节点的地址)。只要内存节点可以找到至少一个磁盘节点,那么它就能在重启后重新加入集群。
网友评论