一、概述

1. 高吞吐量:对等集群横向拓展

  • 与其他中间件产品类似,RabbitMQ也是通过集群的方式来解决单节点在处理海量消息时的性能瓶颈,通过集群的方式来实现高吞吐量,如单个RabbitMQ节点每秒只能处理1000个消息,而通过集群方式拓展,则可以进一步达到每秒10万个消息的处理或者更高的吞吐量。
  • 不过RabbitMQ的集群在处理海量消息时,是通过在集群的多个节点建立多个不同的队列来分散消息到多个不同节点的,所以在业务层需要对消息进行细分类别来放到不同队列中。
  • RabbitMQ集群不是一个主从模式的集群,而是一个对等集群,即节点之间是对等,不存在主从关系,每个节点存放的是不同的队列,所以存放的是不同的消息,由集群内所有节点的所有队列的消息共同组成该集群的所有消息。

2. 高可用:镜像队列

  • 不过如果使用了镜像队列,则集群中的某些节点作为该队列的主节点的从节点,存在主从关系,这个主从主要用于实现队列消息的冗余存储,实现队列和队列内部消息的高可用,不是用于读写分离实现高并发,即此时还是主节点负责处理读写请求。

3. 高并发:对等集群和“逻辑”单点

  • 与大部分中间件的集群,如 Redis,MySQL,Zookeeper 等,采用读写分离的方式来应对高并发数据请求不同的是,RabbitMQ 集群是一个对等集群,集群内部每个节点存放不同队列(所以也就存放着不同的数据)。
  • 集群内部每个节点在应对客户端的数据请求方面不存在主从关系,对于某个消息的请求,如生产者写入该消息或者消费者读取该消息,都需要首先定位到该消息所在队列的节点,然后交给该节点来处理,其他节点由于不存在这个队列,或者是镜像队列的从节点,故不能处理这个消息的请求。
  • 不过从客户端的角度来看,RabbitMQ 集群其实就是一个逻辑上的单节点,即客户端可以连接 RabbitMQ 集群的任何一个节点,而不需要约束为只能连接该客户端所要请求的消息对应的队列所在的节点。
  • 任何一个节点接收到客户端的请求后,如果该请求对应的消息属于自身节点的队列,则由该节点处理,否则将该请求转发给该消息所属队列所在的节点处理,这个过程对客户端来说是透明的。所以通过这种方式 RabbitMQ 集群可以通过增加节点个数来应对高并发请求,因为客户端可以连接任意一个节点。
  • 所以出于性能考虑,RabbitMQ 集群一般需要部署在一个局域网LAN内部,这样才能保证不同节点之间的请求转发的高性能。

二、集群的设计

以上分析了 RabbitMQ 通过集群的方式来实现高吞吐量和高并发处理,以下针对 RabbitMQ 集群如何做到这些进行分析。

1. 节点的名字:集群的唯一标识

  • RabbitMQ 集群中的每个节点通过名字来唯一确定一个节点,所以集群内部的每个节点的名字不能一样。RabbitMQ 的集群节点的命名规范为:前戳 + 域名 hostname,前戳一般为 rabbit,如单机集群的两个不同节点为:rabbit1@localhost,rabbit2@localhost;不同机器的两个不同节点可以为:rabbit@node1.xyz.com,rabbit@node2.xyz.com。
  • 节点名字可以通过环境变量 RABBITMQ_NODENAME 来配置,如果不配置,则 RabbitMQ 节点的默认名字:rabbit@当前机器的hostname。

2. 集群的元数据广播

  • 每个RabbitMQ节点通常包含以下元数据:
  1. 虚拟主机vhost的元数据:该vhost内部包含的交换器,队列,交换器与队列绑定关系;
  2. 交换器的元数据:交换器的名字,类型,属性;
  3. 队列元数据:队列名字,属性;注意这是队列元数据,不包括队列内部存放的消息。
  4. 交换器与队列之间的绑定关系。
  • 这些元数据是集群级别的,即是包含集群内部所有节点的所有vhost,交换器,队列的信息。而这些元数据在集群内部的每个节点都是存在的,当在某个节点新建了一个交换器和队列之后,需要广播给集群内部的所有节点,保证每个节点都拥有集群内部所有节点的元数据信息。
  • 通过节点之间的元数据广播,实现了当客户端连接到任意一个节点时,任意一个节点都能知道客户端请求的消息位于哪个节点的哪个队列当中,如果就是这个节点自身则自身处理,否则转发到对应的节点处理,存放该消息到该队列或者消费该队列的消息。

3. 节点的类型:内存和磁盘节点(元数据存储方式来划分)

  • 针对节点存放集群元数据的方式不同,可以将集群的节点分为RAM内存节点和DISK磁盘节点,其中内存节点将元数据存放在内存中,磁盘节点将元数据存放在磁盘中。
  • 由于内存中的数据在节点重启时会丢失,故需要从集群中的磁盘节点同步过来,所以集群中至少需要存在一个磁盘节点,否则如果全是内存节点,则 RabbitMQ 集群将会禁止任何添加元数据的操作,如新建交换器,队列等。
  • 出于内存操作性能高于磁盘操作性能和元数据可靠性方面的考虑,一般需要设置两个或者以上节点作为磁盘节点,这样可以避免磁盘节点挂了,集群元数据容易丢失问题。

4. 数据安全性:节点间的认证与通信

a. 节点之间的通信
  • 由于集群内部的每个节点需要包含不同的名字,而名字命名规范是后面使用域名 hostname。这样设计的主要目的是集群内部的节点通过其他节点的名字包含的域名 hostname 来定位其他节点,即跟普通通过 URL 访问某个web资源类似,该节点通过 DNS 域名解析定位到其他节点后,可以发起对其他节点的请求,如转发消息请求给其他节点。其中域名解析主要包含:本地配置DNS或者在/etc/hosts文件配置其他节点的域名与ip的映射关系。
b. 基于共享私钥的身份认证
  • 集群的节点之间需要通信则需要一个认证信息,这样才能判断通信的节点是否属于同一个集群。RabbitMQ 在认证方面,主要是通过设计一个统一的 cookies 来实现,这个 cookies 相当于一个共享私钥。同一个RabbitMQ集群内部的节点在请求其他节点时都要带上这个cookies信息,这样其他节点通过检查这个cookies与自身的cookies是否一致来判断是否来自同一个集群的其他节点的请求。
  • 认证 cookies 默认放在文件/var/lib/rabbitmq/.erlang.cookie中,在部署集群之前,需要在各个节点的该文件设置好该统一的cookies的内容。

5. 节点的对等性:高并发与高吞吐量

  • 由客户端可以连接集群的任意一个节点的分析可知,集群的节点是对等节点,如果不存在镜像队列,集群的每个节点存放不同的队列,所以也就存放不同的消息。
  • 从消息的角度而言,每个消息只存放在某个节点的某个队列中,当客户端需要请求某个消息,如生产者客户端写入消息或者消费者客户端消费消息,客户端将请求其所连接的节点,如果消息对应的队列就在这个节点则直接处理,否则该节点根据集群元数据定位到该消息对应的队列所在的节点,将该消息请求转发给这个节点处理。
  • 所以通过节点的对等性设计,RabbitMQ 集群可以通过增加节点的方式来应对高并发客户端连接请求,不过在具体实施时还需要结合对消息类型进行细分,设计出更多不同的队列从而分散队列到不同的节点来实现,因为任何一个队列只能存在于集群的一个节点中。(如果某个队列包含镜像队列,则该消息存放在多个队列中,但是消息读写方面还是只能由队列主节点来处理,镜像队列主要用于备份和高可用)。

6. 镜像队列与消息持久化

  • 由以上分析可知,如果该队列没有镜像队列,每个消息只能存放在这个队列中,如果存在镜像队列,则队列主节点需要将该队列的消息同步给镜像队列,实现消息的冗余存储,实现高可用,即当队列主节点宕机时,升级一个镜像队列作为新的队列主节点继续提供消息请求处理。
  • 在高可靠性方面,队列的消息是否需要持久化到磁盘中,由生产者生产这条消息时指定,持久化到磁盘后,节点在重启时,该消息能继续加载会队列中,而不会丢失。