RabbitMQ是流行的开源消息队列系统,本身已经具备了较强的并发处理速度及运行稳定性,然而在大规模的实际应用中,往往还需要使用集群配置来保证系统中消息通信部分的高可用性,并发处理性能及异常恢复能力。这里将介绍一种实用的消息集群架构,以及一种能够快速、高效、可靠地部署并配置消息集群的方式,通过这种方法,我们可以在系统部署时仅需短短几分钟就能完成规模化的消息集群架设,极大地提高了工作效率和部署成功率。
集群架构设计
RabbitMQ这款消息队列中间件产品本身是基于Erlang编写,Erlang语言天生具备分布式特性(通过同步Erlang集群各节点的magic cookie来实现)。因此,RabbitMQ天然支持Clustering。这使得RabbitMQ本身不需要像ActiveMQ、Kafka那样通过ZooKeeper分别来实现HA方案和保存集群的元数据。集群是保证可靠性的一种方式,同时可以通过水平扩展以达到增加消息吞吐量能力的目的。
名词说明:
Broker:它提供一种传输服务,它的角色就是维护一条从生产者到消费者的路线,保证数据能按照指定的方式进行传输
Exchange:消息交换机,它指定消息按什么规则,路由到哪个队列
Queue:消息的载体,每个消息都会被投到一个或多个队列
Binding:绑定,它的作用就是把exchange和queue按照路由规则绑定起来
Routing Key:路由关键字,exchange根据这个关键字进行消息投递
vhost:虚拟主机,一个broker里可以有多个vhost,用作不同用户的权限分离
Producer:消息生产者,就是投递消息的程序
Consumer:消息消费者,就是接受消息的程序
Channel:消息通道,在客户端的每个连接里,可建立多个channel
元数据
RabbitMQ 内部有各种基础构件,包括队列、交换器、绑定、虚拟主机等,他们组成了 AMQP 协议消息通信的基础,而这些构件以元数据的形式存在,它始终记录在 RabbitMQ 内部,它们分别是:
队列元数据:队列名称和它们的属性
交换器元数据:交换器名称、类型和属性
绑定元数据:一张简单的表格展示了如何将消息路由到队列
vhost元数据:为 vhost 内的队列、交换器和绑定提供命名空间和安全属性
队列
这里有个问题需要思考,RabbitMQ 默认会将消息冗余到所有节点上吗?这样听起来正符合高可用的特性,只要集群上还有一个节点存活,那么就可以继续进行消息通信,但这也随之为 RabbitMQ 带来了致命的缺点:
每次发布消息,都要把它扩散到所有节点上,而且对于磁盘节点来说,每一条消息都会触发磁盘活动,这会导致整个集群内性能负载急剧拉升。
如果每个节点都有所有队列的完整内容,那么添加节点不会给你带来额外的存储空间,也会带来木桶效应,举个例子,如果集群内有个节点存储了 3G 队列内容,那么在另外一个只有 1G 存储空间的节点上,就会造成内存空间不足的情况,也就是无法通过集群节点的扩容提高消息积压能力。
解决这个问题就是通过集群中唯一节点来负责任何特定队列,只有该节点才会受队列大小的影响,其它节点如果接收到该队列消息,那么就要根据元数据信息,传递给队列所有者节点(也就是说其它节点上只存储了特定队列所有者节点的指针)。这样一来,就可以通过在集群内增加节点,存储更多的队列数据。
exchange
交换器其实本质是一张查询表,里面包括了交换器名称和一个队列的绑定列表,当你将消息发布到交换器中,实际上是你所在的信道将消息上的路由键与交换器的绑定列表进行匹配,然后将消息路由出去。有了这个机制,那么在所有节点上传递交换器消息将简单很多,而 RabbitMQ 所做的事情就是把交换器拷贝到所有节点上,因此每个节点上的每条信道都可以访问完整的交换器了。
http://chyufly.github.io/images/RabbitMQ_cluster_exchanges.PNG
内存节点和磁盘节点
关于上面队列所说的问题与解决办法,又有了一个伴随而来的问题出现:如果特定队列的所有者节点发生了故障,那么该节点上的队列和关联的绑定都会消失吗?
如果是内存节点,那么附加在该节点上的队列和其关联的绑定都会丢失,并且消费者可以重新连接集群并重新创建队列;
如果是磁盘节点,重新恢复故障后,该队列又可以进行传输数据了,并且在恢复故障磁盘节点之前,不能在其它节点上让消费者重新连到集群并重新创建队列,如果消费者继续在其它节点上声明该队列,会得到一个 404 NOT_FOUND 错误,这样确保了当故障节点恢复后加入集群,该节点上的队列消息不回丢失,也避免了队列会在一个节点以上出现冗余的问题。
接下来说说内存节点与磁盘节点在集群中的作用,在集群中的每个节点,要么是内存节点,要么是磁盘节点,如果是内存节点,会将所有的元数据信息仅存储到内存中,而磁盘节点则不仅会将所有元数据存储到内存上, 还会将其持久化到磁盘。
在单节点 RabbitMQ 上,仅允许该节点是磁盘节点,这样确保了节点发生故障或重启节点之后,所有关于系统的配置与元数据信息都会重磁盘上恢复;而在 RabbitMQ 集群上,允许节点上至少有一个磁盘节点,在内存节点上,意味着队列和交换器声明之类的操作会更加快速。原因是这些操作会将其元数据同步到所有节点上,对于内存节点,将需要同步的元数据写进内存就行了,但对于磁盘节点,意味着还需要及其消耗性能的磁盘写入操作。
RabbitMQ 集群只要求至少有一个磁盘节点,这是有道理的,当其它内存节点发生故障或离开集群,只需要通知至少一个磁盘节点进行元数据的更新,如果是碰巧唯一的磁盘节点也发生故障了,集群可以继续路由消息,但是不可以做以下操作了:
创建队列
创建交换器
创建绑定
添加用户
更改权限
添加或删除集群节点
这是因为上述操作都需要持久化到磁盘节点上,以便内存节点恢复故障可以从磁盘节点上恢复元数据,解决办法是在集群添加 2 台以上的磁盘节点,这样其中一台发生故障了,集群仍然可以保持运行,且能够在任何时候保存元数据变更。
负载均衡集群部署搭建
负载均衡部署如下图所示: 4-5节点 如图所示
经过上面的RabbitMQ10个节点集群搭建和HAProxy软弹性负载均衡配置后即可组建一个中小规模的RabbitMQ集群了,然而为了能够在实际的生产环境使用还需要根据实际的业务需求对集群中的各个实例进行一些性能参数指标的监控,从性能、吞吐量和消息堆积能力等角度考虑,可以选择Kafka来作为RabbitMQ集群的监控队列使用。因此,一个中小规模RabbitMQ集群架构设计图如下图所示
服务器环境: centos7.x 系统 4c 16g 200g
rabbitmq 安装
每个节点修改host文件
配置各节点的hosts文件( vim /etc/hosts)
xxx.xxx.xxx.xxx mq-1
xxx.xxx.xxx.xxx mq-2
xxx.xxx.xxx.xxx mq-3
...
安装依赖
-
Jdk 1.8
-
Erlang运行时环境 erlang> = 19.3
-
socat
安装时候注意版本
### add erlang repo
[rabbitmq-erlang]
name=rabbitmq-erlang
baseurl=https://dl.bintray.com/rabbitmq/rpm/erlang/20/el/7
gpgcheck=1
gpgkey=https://dl.bintray.com/rabbitmq/Keys/rabbitmq-release-signing-key.asc
repo_gpgcheck=0
enabled=1
yum install erlang java-1.8.0-openjdk socat
# 查看erlang包版本
$ rpm -q erlang
erlang-20.3-1.el7.centos.x86_64
修改.erlang.cookie文件
erlang.cookie是erlang实现分布式的必要文件,erlang分布式的每个节点上要保持相同的.erlang.cookie文件,同时保证文件的权限是400
编辑每台RabbitMQ的cookie文件,以确保各个节点的cookie文件使用的是同一个值,可以scp其中一台机器上的cookie至其他各个节点,cookie的默认路径为/var/lib/rabbitmq/.erlang.cookie或者$HOME/.erlang.cookie,节点之间通过cookie确定相互是否可通信。
添加rabbitmq 3.7.x repo
# import the new PackageCloud key that will be used starting December 1st, 2018 (GMT)
rpm --import https://packagecloud.io/rabbitmq/rabbitmq-server/gpgkey
# import the old PackageCloud key that will be discontinued on December 1st, 2018 (GMT)
rpm --import https://packagecloud.io/gpg.key
## centos7.x add repo
[bintray-rabbitmq-server]
name=bintray-rabbitmq-rpm
baseurl=https://dl.bintray.com/rabbitmq/rpm/rabbitmq-server/v3.7.x/el/7/
gpgcheck=0
repo_gpgcheck=0
enabled=1
## 在每个节点执行install rabbitmq
yum install rabbitmq-server
加入集群
逐个节点启动RabbitMQ服务
systemctl start rabbitmq-server
查看各个节点和集群的工作运行状态
rabbitmqctl status, rabbitmqctl cluster_status
以mq-test1为主节点, 在其他节点执行如下命令, 以mq-test2为例
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl join_cluster rabbit@mq-test-2
rabbitmqctl start_app
设置节点类型
在RabbitMQ集群中的节点只有两种类型:内存节点/磁盘节点,单节点系统只运行磁盘类型的节点。而在集群中,可以选择配置部分节点为内存节点
内存节点将所有的队列,交换器,绑定关系,用户,权限,和vhost的元数据信息保存在内存中。而磁盘节点将这些信息保存在磁盘中,但是内存节点的性能更高,为了保证集群的高可用性,必须保证集群中有两个以上的磁盘节点,来保证当有一个磁盘节点崩溃了,集群还能对外提供访问服务。在上面的操作中,可以通过如下的方式,设置新加入的节点为内存节点还是磁盘节点
#加入时候设置节点为内存节点(默认加入的为磁盘节点)
[root@mq-testvm1 ~]# rabbitmqctl join_cluster rabbit@rmq-broker-test-1 --ram
#也通过下面方式修改的节点的类型
[root@mq-testvm1 ~]# rabbitmqctl change_cluster_node_type disc | ram
启用web管理plugin
rabbitmq-plugins enable rabbitmq_management
设置镜像队列
当节点发生故障时,尽管所有元数据信息都可以从磁盘节点上将元数据拷贝到本节点上,但是队列的消息内容就不行了,这样就会导致消息的丢失,那是因为在默认情况下,队列只会保存在其中一个节点上,我们在将集群队列时也说过。
聪明的 RabbitMQ 早就意识到这个问题了,在 2.6以后的版本中增加了,队列冗余选项:镜像队列。镜像队列的主队列(master)依然是仅存在于一个节点上,其余从主队列拷贝的队列叫从队列(slave)。如果主队列没有发生故障,那么其工作流程依然跟普通队列一样,生产者和消费者不会感知其变化,当发布消息时,依然是路由到主队列中,而主队列通过类似广播的机制,将消息扩散同步至其余从队列中,这就有点像 fanout 交换器一样。而消费者依然是从主队列中读取消息。
一旦主队列发生故障,集群就会从最老的一个从队列选举为新的主队列,这也就实现了队列的高可用了,但我们切记不要滥用这个机制,在上面也说了,队列的冗余操作会导致不能通过扩展节点增加存储空间,而且会造成性能瓶颈。
官网参考文档: http://www.rabbitmq.com/parameters.html#policies
命令如下
$ rabbitmqctl set_policy [-p Vhost] Name Pattern Definition [Priority]
-p Vhost: 可选参数,针对指定vhost下的queue进行设置
Name: policy的名称
Pattern: queue的匹配模式(正则表达式)
Definition: 镜像定义,包括三个部分ha-mode, ha-params, ha-sync-mode
ha-mode: 指明镜像队列的模式,有效值为 all/exactly/nodes
all: 表示在集群中所有的节点上进行镜像
exactly: 表示在指定个数的节点上进行镜像,节点的个数由ha-params指定
nodes: 表示在指定的节点上进行镜像,节点名称通过ha-params指定
ha-params: ha-mode模式需要用到的参数
ha-sync-mode: 进行队列中消息的同步方式,有效值为automatic和manual
priority: 可选参数,policy的优先级
举几个例子
以下示例声明名为ha-all的策略,它与名称以”ha”开头的队列相匹配,并将镜像配置到集群中的所有节点:
## 命令会将所有的队列冗余到所有节点上,一般可以拿来测试。
rabbitmqctl set_policy ha-all "^" '{"ha-mode":"all"}'
##策略的名称以”two”开始的队列镜像到群集中的任意两个节点,并进行自动同步:
$ rabbitmqctl set_policy ha-two "^two." '{"ha-mode":"exactly","ha-params":2,"ha-sync-mode":"automatic"}'
## 以”node”开头的队列镜像到集群中的特定节点的策略
$ rabbitmqctl set_policy ha-nodes "^nodes." '{"ha-mode":"nodes","ha-params":["rabbit@nodeA", "rabbit@nodeB"]}'
监控
RabbitMQ的Monitoring文档中介绍了各种监控RabbitMQ(https://www.rabbitmq.com/monitoring.html)的方式。 其中推荐了第三方的Prometheus Plugin(https://github.com/deadtrickster/prometheus_rabbitmq_exporter), 这是一个第三方实现RabbitMQ的管理插件,可以作为Prometheus的RabbitMQ Exporter。
安装RabbitMQ Prometheus Plugin
这里RabbitMQ的版本为3.7.7,Erlang版本为Erlang 21 [erts-10.0.5],从Release for latest RabbitMQ 3.7.x versions这个链接中下载以下插件:
https://github.com/deadtrickster/prometheus_rabbitmq_exporter/releases
## 这是我下载的plugin
[root@fg-rabbitmq-cluster02 mq_plugin]# ls -alh
total 704K
drwxrwxr-x 3 lebosa lebosa 283 Nov 11 09:22 .
drwx------. 4 lebosa lebosa 128 Nov 11 09:22 ..
-rw-rw-r-- 1 lebosa lebosa 14K Nov 11 09:22 accept-0.3.3.ez
-rw-rw-r-- 1 lebosa lebosa 195K Nov 11 09:22 prometheus-3.5.1.ez
-rw-rw-r-- 1 lebosa lebosa 14K Nov 11 09:22 prometheus_cowboy-0.1.4.ez
-rw-rw-r-- 1 lebosa lebosa 22K Nov 11 09:22 prometheus_httpd-2.1.8.ez
-rw-rw-r-- 1 lebosa lebosa 17K Nov 11 09:22 prometheus_process_collector-1.3.1.ez
drwxrwxr-x 5 lebosa lebosa 205 Nov 11 09:22 prometheus_rabbitmq_exporter-3.7.2.2
-rw-rw-r-- 1 lebosa lebosa 210K Nov 11 09:22 prometheus_rabbitmq_exporter-v3.7.2.2.ez
-rw-rw-r-- 1 lebosa lebosa 218K Nov 11 09:22 v3.7.2.2.tar.gz
将下载的插件拷贝到RabbitMQ的插件目录,rpm形式安装的RabbitMQ的插件目录位于-rabbit plugins_dir “/usr/lib/rabbitmq/plugins:/usr/lib/rabbitmq/lib/rabbitmq_server-3.7.7/plugins”下。
查看插件的目录可以通过
## 通过这个命令可以查看相应的plugin目录
systemctl status rabbitmq-server -l
## 启用插件
rabbitmq-plugins enable accept
rabbitmq-plugins enable prometheus
rabbitmq-plugins enable prometheus_httpd
rabbitmq-plugins enable prometheus_rabbitmq_exporter
rabbitmq-plugins enable prometheus_process_collector
## 重启MQ
systemctl restart rabbitmq-server
## 验证访问http://<ip>:15672/api/metrics可以得到RabbitMQ Exporter暴露的metrics。
curl http://127.0.0.1:15672/api/metrics
promethues 配置
Prometheus的配置文件中增加如下内容:
- job_name: 'rabbitmq'
metrics_path: /api/metrics
static_configs:
- targets:
- host1-ip:15672
labels:
instance: s3
- targets:
- host2-ip:15672
labels:
instance: s4
- targets:
- host3-ip:15672
labels:
instance: s5
Grafana配置
导入grafana dashborad https://github.com/deadtrickster/prometheus_rabbitmq_exporter/tree/master/priv/dashboards
参考文档:https://github.com/deadtrickster/prometheus_rabbitmq_exporter