RabbitMQ 运维 & 扩展

1、集群管理与配置

1.1、集群搭建

        关于Rabbitmq 集群的搭建,详见以下文章。简单说来就是将多个单机rabbitmq服务,通过给到一致的密钥(.erlang.cookie)并且开放rabbitmq服务的 25672 端口,允许多节点间进行互相通讯,就完成了集群的搭建。

        当多个单机服务正常部署可运行的时候,则需要进行多节点的配置。假设这里一共有三台物理主机, 均己正确地安装了RabbitMQ ,且主机名分别为myblnp1 , myblnp2和myblnp3 。接下来需要按照以下步骤执行。

第一步,配置各个节点的hosts 文件,让各个节点都能互相识别对方的存在。比如在LÏnux 系统中可以编辑 /etc/hosts 文件,在其上添加IP地址与节点名称的映射信息:

第二步,编辑RabbitMQ 的cookie 文件,以确保各个节点的cookie 文件使用的是同一个值。可以读取myblnp1 节点的cookie 值, 然后将其复制到myblnp2 和myblnp3 节点中。cookie 文件默认路径为 /var/lib/rabbitmq/.erlang . cookie 或者$HOME/.erlang.cookie。cookie 相当于密钥令牌,集群中的RabbitMQ 节点需要通过交换密钥令牌以获得相互认证。如果节点的密钥令牌不一致,那么在配置节点时就会有如下的报错:

[rabbit@myblnp2 ~]$ rabbitmqctl join_cluster rabbit@myblnp1
Clustering node rabbit@myblnp2 with rabbit@myblnp1
Error: unable to perform an operation on node 'rabbit@myblnp1'. Please see diagnostics information and suggestions below.

Most common reasons for this are:

 * Target node is unreachable (e.g. due to hostname resolution, TCP connection or firewall issues)
 * CLI tool fails to authenticate with the server (e.g. due to CLI tool's Erlang cookie not matching that of the server)
 * Target node is not running

In addition to the diagnostics info below:

 * See the CLI, clustering and networking guides on https://rabbitmq.com/documentation.html to learn more
 * Consult server logs on node rabbit@myblnp1
 * If target node is configured to use long node names, don't forget to use --longnames with CLI tools

DIAGNOSTICS
===========

attempted to contact: [rabbit@myblnp1]

rabbit@myblnp1:
  * connected to epmd (port 4369) on myblnp1
  * epmd reports node 'rabbit' uses port 25672 for inter-node and CLI tool traffic 
  * can't establish TCP connection to the target node, reason: ehostunreach (host is unreachable)
  * suggestion: check if host 'myblnp1' resolves, is reachable and ports 25672, 4369 are not blocked by firewall

Current node details:
 * node name: 'rabbitmqcli-784-rabbit@myblnp2'
 * effective user's home directory: /home/rabbit
 * Erlang cookie hash: bDQvLYhXS0F44DcLh/J1lg==

[rabbit@myblnp2 ~]$ 

 第三步, 配置集群。配置集群有三种方式:通过rabbitmqctl 工具配置:通过rabbitmq.config 配置文件配置; 通过rabbitmq-autocluster插件配置。这里主要讲的是通过rabbitmqctl 工具的方式配置集群,这种方式也是最常用的方式。其余两种方式在实际应用中用之甚少, 所以不多做介绍。

        首先启动myblnp1, myblnp2 和myblnp3 这3 个节点的RabbitMQ 服务。

[root@myblnp1 ~]# rabbitmq-server -detached
[root@myblnp2 ~]# rabbitmq-server -detached
[root@myblnp3 ~]# rabbitmq-server -detached

        这样, 这3个节点目前都是以独立节点存在的单个集群。通过rabbitmqctl cluster_status 命令来查看各个节点的状态。

[rabbit@myblnp1 ~]$ rabbitmqctl cluster_status
Cluster status of node rabbit@myblnp1 ...
Basics

Cluster name: rabbit@myblnp1

Disk Nodes

rabbit@myblnp1

Running Nodes

rabbit@myblnp1

Versions

rabbit@myblnp1: RabbitMQ 3.8.16 on Erlang 24.1.4

Maintenance status

Node: rabbit@myblnp1, status: not under maintenance

Alarms

(none)

Network Partitions

(none)

Listeners

Node: rabbit@myblnp1, interface: [::], port: 15672, protocol: http, purpose: HTTP API
Node: rabbit@myblnp1, interface: [::], port: 25672, protocol: clustering, purpose: inter-node and CLI tool communication
Node: rabbit@myblnp1, interface: [::], port: 5672, protocol: amqp, purpose: AMQP 0-9-1 and AMQP 1.0

Feature flags

Flag: drop_unroutable_metric, state: enabled
Flag: empty_basic_get_metric, state: enabled
Flag: implicit_default_bindings, state: enabled
Flag: maintenance_mode_status, state: enabled
Flag: quorum_queue, state: enabled
Flag: user_limits, state: enabled
Flag: virtual_host_metadata, state: enabled
[rabbit@myblnp1 ~]$

        接下来为了将3 个节点组成一个集群,需要以myblnp1节点为基准,将myblnp2 和myblnp3节点加入myblnp1节点的集群中。这3 个节点是平等的,如果想调换彼此的加入顺序也未尝不可。首
先将myblnp2 节点加入myblnp3 节点的集群中,需要执行如下4 个命令步骤。

[rabbit@myblnp3 ~]$ rabbitmqctl stop_app
Stopping rabbit application on node rabbit@myblnp3 ...
[rabbit@myblnp3 ~]$ rabbitmqctl reset
Resetting node rabbit@myblnp3 ...
[rabbit@myblnp3 ~]$ 
[rabbit@myblnp3 ~]$ rabbitmqctl join_cluster rabbit@myblnp1
Clustering node rabbit@myblnp3 with rabbit@myblnp1
[rabbit@myblnp3 ~]$ 
[rabbit@myblnp3 ~]$ 
[rabbit@myblnp3 ~]$ rabbitmqctl start_app
Starting node rabbit@myblnp3 ...
[rabbit@myblnp3 ~]$ 

        同理在其余节点也执行以上步骤,即可完成集群的构建。此时查看集群状态信息可见如下所示:

[rabbit@myblnp1 rabbitmq_server-3.8.16]$ rabbitmqctl cluster_status
Cluster status of node rabbit@myblnp1 ...
Basics

Cluster name: rabbit@myblnp1

Disk Nodes

rabbit@myblnp1
rabbit@myblnp2
rabbit@myblnp3

Running Nodes

rabbit@myblnp1
rabbit@myblnp2
rabbit@myblnp3

Versions

rabbit@myblnp1: RabbitMQ 3.8.16 on Erlang 24.1.4
rabbit@myblnp2:   on Erlang 
rabbit@myblnp3:   on Erlang 

Maintenance status

Node: rabbit@myblnp1, status: not under maintenance
Node: rabbit@myblnp2, status: unknown
Node: rabbit@myblnp3, status: unknown

Alarms

(none)

Network Partitions

(none)

Listeners

Node: rabbit@myblnp1, interface: [::], port: 15672, protocol: http, purpose: HTTP API
Node: rabbit@myblnp1, interface: [::], port: 25672, protocol: clustering, purpose: inter-node and CLI tool communication
Node: rabbit@myblnp1, interface: [::], port: 5672, protocol: amqp, purpose: AMQP 0-9-1 and AMQP 1.0

Feature flags

Flag: drop_unroutable_metric, state: enabled
Flag: empty_basic_get_metric, state: enabled
Flag: implicit_default_bindings, state: enabled
Flag: maintenance_mode_status, state: enabled
Flag: quorum_queue, state: enabled
Flag: user_limits, state: enabled
Flag: virtual_host_metadata, state: enabled
[rabbit@myblnp1 rabbitmq_server-3.8.16]$

 特别说明:这里尤其需要注意三点:

        1、必须保证各节点之间,网络可互通。验证方式可以使用 ping 命令,来 ping 对接节点的 hostname 确保通讯正常

        2、确保主节点的集群通讯端口(25672)与4369端口是开放的

        3、确保各节点的 .erlang.cookie 是一致的

        此外,如果使用 自启服务的方式来启动rabbitmq节点时,可能会出现一种情况。就是服务有时启动正常,有时启动失败。并且启动成功的时候,也无法使用 rabbitmq的任何相关命令,执行命令时也会返回 cookie问题。出现这种问题的原因的主要是两方面:

        1、当前服务器节点内,存在多个 .erlang.cookie 文件并且不一致。可以通过命令查找所有的 .erlang.cookie 文件

[rabbit@myblnp2 ~]$ sudo find / -name '.erlang.cookie'
/home/rabbit/rabbitmq_server-3.8.16/.erlang.cookie
/home/rabbit/.erlang.cookie
[rabbit@myblnp2 ~]$ 

解决方式:清除多余的 cookie文件,保证cookie文件值的一致性

        2、注册的自启服务存在问题,可通过 ‘sudo systemctl status [serviceName]’ 或 'sudo journalctl -xe' 等命令查看服务输出日志,在根据日志进行问题处理。

解决方式:我这里根据日志查看确定问题是因为权限问题,因为自启服务无法在指定目录下读取到 lockFile 文件,导致服务异常。解决方式很简单,首先将服务停止,然后将自启服务创建的整个 var 目录清除,然后进入rabbitmq 的 sbin 目录,使用指令通过前台方式运行服务。当服务运行正常后,停止服务并保留生成的 var 目录配置。此时在开启自启服务运行,则以上问题都解决了。

         现在己经完成了集群的搭建。如果集群中某个节点关闭了,那么集群会处于什么样的状态?这里我们在node2 节点上执行rabbitmqctl stop app 命令来主动关闭RabbitMQ 应用。此时node1 上看到的集群状态可以参考下方信息,可以看到在running_nodes 这一选项中已经没有了rabbit@node2 这一节点。

        如果关闭了集群中的所有节点,则需要确保在启动的时候最后关闭的那个节点是第一个启动的。如果第一个启动的不是最后关闭的节点,那么这个节点会等待最后关闭的节点启动。这个等待时间是30 秒,如果没有等到,那么这个先启动的节点也会失败。在最新的版本中会有重试机制, 默认重试10 次30 秒以等待最后关闭的节点启动。

        在重试失败之后,当前节点也会因失败而关闭自身的应用。比如nodel 节点最后关闭,那么此时先启动node2 节点,在等待若干时间之后发现nod巳l 还是没有启动,则会有如下报错:

        如果最后一个关闭的节点最终由于某些异常而无法启动,则可以通过rabbitrnqctl
forget_cluster node 命令来将此节点剔出当前集群,详细内容可以参考后续章节。如果集群中的所有节点由于某些非正常因素,比如断电而关闭,那么集群中的节点都会认为还有其他节点在它后面关闭,此时需要调用rabbitrnqctl force_boot 命令来启动一个节点,之后集群才能正常启动。

1.1.1、集群节点类型

        在使用 rabbitrnqctl cluster_status 命令来查看集群状态时会有 Disk Nodes 这一项信息,其中的disk 标注了RabbitMQ 节点的类型。RabbitMQ 中的每一个节点,不管是单一节点系统或者是集群中的一部分,要么是内存节点(ram),要么是磁盘节点(disc)。内存节点将所有的队列、交换器、绑定关系、用户、权限和vhost的元数据定义都存储在内存中,而磁盘节点则将这些信息存储到磁盘中。单节点的集群中必然只有磁盘类型的节点,否则当重启RabbitMQ 之后,所有关于系统的配置信息都会丢失。不过在集群中,可以选择配置部分节点为内存节点,这样可以获得更高的性能。

[rabbit@myblnp3 ~]$ rabbitmqctl cluster_status
Cluster status of node rabbit@myblnp3 ...
Basics

Cluster name: rabbit@myblnp1

Disk Nodes

rabbit@myblnp1
rabbit@myblnp2
rabbit@myblnp3

Running Nodes

rabbit@myblnp1
rabbit@myblnp2
rabbit@myblnp3

Versions

rabbit@myblnp1: RabbitMQ 3.8.16 on Erlang 24.1.4
rabbit@myblnp2: RabbitMQ 3.8.16 on Erlang 24.1.4
rabbit@myblnp3: RabbitMQ 3.8.16 on Erlang 24.1.4

Maintenance status

Node: rabbit@myblnp1, status: not under maintenance
Node: rabbit@myblnp2, status: not under maintenance
Node: rabbit@myblnp3, status: not under maintenance

Alarms

(none)

Network Partitions

(none)

Listeners

Node: rabbit@myblnp1, interface: [::], port: 15672, protocol: http, purpose: HTTP API
Node: rabbit@myblnp1, interface: [::], port: 25672, protocol: clustering, purpose: inter-node and CLI tool communication
Node: rabbit@myblnp1, interface: [::], port: 5672, protocol: amqp, purpose: AMQP 0-9-1 and AMQP 1.0
Node: rabbit@myblnp2, interface: [::], port: 15672, protocol: http, purpose: HTTP API
Node: rabbit@myblnp2, interface: [::], port: 25672, protocol: clustering, purpose: inter-node and CLI tool communication
Node: rabbit@myblnp2, interface: [::], port: 5672, protocol: amqp, purpose: AMQP 0-9-1 and AMQP 1.0
Node: rabbit@myblnp3, interface: [::], port: 15672, protocol: http, purpose: HTTP API
Node: rabbit@myblnp3, interface: [::], port: 25672, protocol: clustering, purpose: inter-node and CLI tool communication
Node: rabbit@myblnp3, interface: [::], port: 5672, protocol: amqp, purpose: AMQP 0-9-1 and AMQP 1.0

Feature flags

Flag: drop_unroutable_metric, state: enabled
Flag: empty_basic_get_metric, state: enabled
Flag: implicit_default_bindings, state: enabled
Flag: maintenance_mode_status, state: enabled
Flag: quorum_queue, state: enabled
Flag: user_limits, state: enabled
Flag: virtual_host_metadata, state: enabled
[rabbit@myblnp3 ~]$

        比如将node2 节点加入node1 节点的时候可以指定node2 节点的类型为内存节点。

[root@node2 ~]# rabbitmqctl join_cluster rabbit@nodel --ram
Clustering node rabbit@node2 with rabbit@nodel

        这样在以nodel 和node2 组成的集群中就会有一个磁盘节点和一个内存节点,可以参考下
面的打印信息。默认不展示 "RAM NODES" 选项。如果集群已经搭建好了,那么也可以使用rabbitmqctl change_cluster_node_type {disc , ram) 命令来切换节点的类型,其中disc 表示磁盘节点,而ram 表示内存节点。举例,这里将上面 myblnp3 节点由内存节点转变为磁盘节点。完整执行步骤如下所示:

[rabbit@myblnp3 ~]$ rabbitmqctl cluster_status
Cluster status of node rabbit@myblnp3 ...
Basics

Cluster name: rabbit@myblnp1

Disk Nodes

rabbit@myblnp1
rabbit@myblnp2
rabbit@myblnp3

Running Nodes

rabbit@myblnp1
rabbit@myblnp2
rabbit@myblnp3

Versions

rabbit@myblnp1: RabbitMQ 3.8.16 on Erlang 24.1.4
rabbit@myblnp2: RabbitMQ 3.8.16 on Erlang 24.1.4
rabbit@myblnp3: RabbitMQ 3.8.16 on Erlang 24.1.4

Maintenance status

Node: rabbit@myblnp1, status: not under maintenance
Node: rabbit@myblnp2, status: not under maintenance
Node: rabbit@myblnp3, status: not under maintenance

Alarms

(none)

Network Partitions

(none)

Listeners

Node: rabbit@myblnp1, interface: [::], port: 15672, protocol: http, purpose: HTTP API
Node: rabbit@myblnp1, interface: [::], port: 25672, protocol: clustering, purpose: inter-node and CLI tool communication
Node: rabbit@myblnp1, interface: [::], port: 5672, protocol: amqp, purpose: AMQP 0-9-1 and AMQP 1.0
Node: rabbit@myblnp2, interface: [::], port: 15672, protocol: http, purpose: HTTP API
Node: rabbit@myblnp2, interface: [::], port: 25672, protocol: clustering, purpose: inter-node and CLI tool communication
Node: rabbit@myblnp2, interface: [::], port: 5672, protocol: amqp, purpose: AMQP 0-9-1 and AMQP 1.0
Node: rabbit@myblnp3, interface: [::], port: 15672, protocol: http, purpose: HTTP API
Node: rabbit@myblnp3, interface: [::], port: 25672, protocol: clustering, purpose: inter-node and CLI tool communication
Node: rabbit@myblnp3, interface: [::], port: 5672, protocol: amqp, purpose: AMQP 0-9-1 and AMQP 1.0

Feature flags

Flag: drop_unroutable_metric, state: enabled
Flag: empty_basic_get_metric, state: enabled
Flag: implicit_default_bindings, state: enabled
Flag: maintenance_mode_status, state: enabled
Flag: quorum_queue, state: enabled
Flag: user_limits, state: enabled
Flag: virtual_host_metadata, state: enabled
[rabbit@myblnp3 ~]$ 
[rabbit@myblnp3 ~]$ 
[rabbit@myblnp3 ~]$ 
[rabbit@myblnp3 ~]$ rabbitmqctl stop_app
Stopping rabbit application on node rabbit@myblnp3 ...
[rabbit@myblnp3 ~]$ rabbitmqctl change_cluster_node_type disc
Turning rabbit@myblnp3 into a disc node
Node type is already disc
[rabbit@myblnp3 ~]$ rabbitmqctl change_cluster_node_type ram
Turning rabbit@myblnp3 into a ram node
[rabbit@myblnp3 ~]$ rabbitmqctl start_app
Starting node rabbit@myblnp3 ...
[rabbit@myblnp3 ~]$ 
[rabbit@myblnp3 ~]$ rabbitmqctl cluster_status
Cluster status of node rabbit@myblnp3 ...
Basics

Cluster name: rabbit@myblnp1

Disk Nodes

rabbit@myblnp1
rabbit@myblnp2

RAM Nodes

rabbit@myblnp3

Running Nodes

rabbit@myblnp1
rabbit@myblnp2
rabbit@myblnp3

Versions

rabbit@myblnp1: RabbitMQ 3.8.16 on Erlang 24.1.4
rabbit@myblnp2: RabbitMQ 3.8.16 on Erlang 24.1.4
rabbit@myblnp3: RabbitMQ 3.8.16 on Erlang 24.1.4

Maintenance status

Node: rabbit@myblnp1, status: not under maintenance
Node: rabbit@myblnp2, status: not under maintenance
Node: rabbit@myblnp3, status: not under maintenance

Alarms

(none)

Network Partitions

(none)

Listeners

Node: rabbit@myblnp1, interface: [::], port: 15672, protocol: http, purpose: HTTP API
Node: rabbit@myblnp1, interface: [::], port: 25672, protocol: clustering, purpose: inter-node and CLI tool communication
Node: rabbit@myblnp1, interface: [::], port: 5672, protocol: amqp, purpose: AMQP 0-9-1 and AMQP 1.0
Node: rabbit@myblnp2, interface: [::], port: 15672, protocol: http, purpose: HTTP API
Node: rabbit@myblnp2, interface: [::], port: 25672, protocol: clustering, purpose: inter-node and CLI tool communication
Node: rabbit@myblnp2, interface: [::], port: 5672, protocol: amqp, purpose: AMQP 0-9-1 and AMQP 1.0
Node: rabbit@myblnp3, interface: [::], port: 25672, protocol: clustering, purpose: inter-node and CLI tool communication
Node: rabbit@myblnp3, interface: [::], port: 15672, protocol: http, purpose: HTTP API
Node: rabbit@myblnp3, interface: [::], port: 5672, protocol: amqp, purpose: AMQP 0-9-1 and AMQP 1.0

Feature flags

Flag: drop_unroutable_metric, state: enabled
Flag: empty_basic_get_metric, state: enabled
Flag: implicit_default_bindings, state: enabled
Flag: maintenance_mode_status, state: enabled
Flag: quorum_queue, state: enabled
Flag: user_limits, state: enabled
Flag: virtual_host_metadata, state: enabled
[rabbit@myblnp3 ~]$ 

        在集群中创建队列、交换器或者绑定关系的时候,这些操作直到所有集群节点都成功提交元数据变更后才会返回。对内存节点来说,这意味着将变更写入内存;而对于磁盘节点来说,这意味着昂贵的磁盘写入操作。内存节点可以提供出色的性能,磁盘节点能够保证集群配置信息的高可靠性,如何在这两者之间进行抉择呢?

        RabbitMQ 只要求在集群中至少有一个磁盘节点,所有其他节点可以是内存节点。当节点加入或者离开集群时,它们必须将变更通知到至少一个磁盘节点。如果只有一个磁盘节点,而且不凑巧的是它刚好崩溃了,那么集群可以继续发送或者接收消息,但是不能执行创建队列、交换器、绑定关系、用户,以及更改权限、添加或删除集群节点的操作了。也就是说,如果集群中唯一的磁盘节点崩溃,集群仍然可以保持运行, 但是直到将该节点恢复到集群前,你无法更改任何东西。所以在建立集群的时候应该保证有两个或者多个磁盘节点的存在

        在内存节点重启后,它们会连接到预先配置的磁盘节点,下载当前集群元数据的副本。当在集群中添加内存节点时,确保告知其所有的磁盘节点(内存节点唯一存储到磁盘的元数据信息是集群中磁盘节点的地址)。只要内存节点可以找到至少一个磁盘节点,那么它就能在重启后重新加入集群中。
        除非使用的是RabbitMQ 的RPC 功能,否则创建队列、交换器及绑定关系的操作确是甚少,大多数的操作就是生产或者消费消息。为了确保集群信息的可靠性,或者在不确定使用磁盘节点或者内存节点的时候,建议全部使用磁盘节点。

1.1.2、剔除单个节点

        创建集群的过程可以看作向集群中添加节点的过程。那么如何将一个节点从集群中剔除呢?这样可以让集群规模变小以节省硬件资源,或者替换一个机器性能更好的节点。同样以nodel、node2 和node3 组成的集群为例,这里有两种方式将node2 剥离出当前集群。

第一种,首先在node2 节点上执行rabbitmqctl stop_app 或者rabbitmqctl stop命令来关闭RabbitMQ 服务。之后再在nodel 节点或者node3 节点上执行rabbitmqctl forget_cluster_node rabbit@node2 命令将nodel 节点剔除出去。这种方式适合node2节点不再运行RabbitMQ 的情况。

[rabbit@myblnp1 rabbitmq_server-3.8.16]$ rabbitmqctl forget_cluster_node rabbit@myblnp2
Removing node rabbit@myblnp2 from the cluster
Error:
RabbitMQ on node rabbit@myblnp2 must be stopped with 'rabbitmqctl -n rabbit@myblnp2 stop_app' before it can be removed
[rabbit@myblnp1 rabbitmq_server-3.8.16]$ 
#待剔除节点尚在运行中,是无法使用该方式进行剔除的
[rabbit@myblnp1 rabbitmq_server-3.8.16]$ 
[rabbit@myblnp1 rabbitmq_server-3.8.16]$ rabbitmqctl cluster_status
Cluster status of node rabbit@myblnp1 ...
Basics

Cluster name: rabbit@myblnp1

Disk Nodes

rabbit@myblnp1
rabbit@myblnp2

RAM Nodes

rabbit@myblnp3

Running Nodes

rabbit@myblnp1
rabbit@myblnp3

Versions

rabbit@myblnp1: RabbitMQ 3.8.16 on Erlang 24.1.4
rabbit@myblnp3:   on Erlang 

Maintenance status

Node: rabbit@myblnp1, status: not under maintenance
Node: rabbit@myblnp3, status: unknown

Alarms

(none)

Network Partitions

(none)

Listeners

Node: rabbit@myblnp1, interface: [::], port: 15672, protocol: http, purpose: HTTP API
Node: rabbit@myblnp1, interface: [::], port: 25672, protocol: clustering, purpose: inter-node and CLI tool communication
Node: rabbit@myblnp1, interface: [::], port: 5672, protocol: amqp, purpose: AMQP 0-9-1 and AMQP 1.0

Feature flags

Flag: drop_unroutable_metric, state: enabled
Flag: empty_basic_get_metric, state: enabled
Flag: implicit_default_bindings, state: enabled
Flag: maintenance_mode_status, state: enabled
Flag: quorum_queue, state: enabled
Flag: user_limits, state: enabled
Flag: virtual_host_metadata, state: enabled
[rabbit@myblnp1 rabbitmq_server-3.8.16]$ 
[rabbit@myblnp1 rabbitmq_server-3.8.16]$ 
[rabbit@myblnp1 rabbitmq_server-3.8.16]$ rabbitmqctl forget_cluster_node rabbit@myblnp2
Removing node rabbit@myblnp2 from the cluster
[rabbit@myblnp1 rabbitmq_server-3.8.16]$ 

在上一章节中有提到,在关闭集群中的每个节点之后,如果最后一个关闭的节点最终由于某些异常而无法启动,则可以通过rabbitmqctl forget_cluster_node 命令来将此节点剔除出当前集群。举例,集群中节点按照node3 、node2 、node1 的顺序关闭,此时如果要启动集群, 就要先启动node1节点。

[root@node3 ~]# rabbitmqctl stop
Stopping and halting node rabbit@node3
[root@node2 ~j# rabbitmqctl stop
Stopping and halting node rabbit@node2
[root@nodel ~]# rabbitmqctl stop
Stopping and halting node rabbit@nodel

第二种,可以在node2 节点中执行命令将nodel 节点剔除出当前集群。

[root@node2 ~]# rabbitmqctl forget_cluster_node rabbit@nodel -offline
Removing node rabbit@nodel from cluster
* Impersonating node : rabbit@node2 ... done
* Mnesia directory : /opt/rabbitmq/var/lib/rabbitmq/mnesia/rabbit@node2
[root@node2 ~] # rabbitmq-server -detached
Warning: PID file not written ; -detached was passed.
[root@node2 ~]#

        注意上面在使用 rabbitmqctl forget_cluster_node 命令的时候用到了 " -offline" 参数,如果不添加这个参数,就需要保证node2 节点中的RabbitMQ 服务处于运行状态,而在这种情况下,node2 无法先行启动, 则" -offline" 参数的添加让其可以在非运行状态下将nodel 剥离出当前集群。

        第二种方式是在node2 上执行rabbitmqctl reset 命令。如果不是像上面由于启动顺序的缘故而不得不删除一个集群节点,建议采用这种方式。

[root@node2 ~] # rabbitmqctl stop app
Stopping rabbit application on node rabbit@node2
[root@node2 ~]# rabbitmqctl reset
Resetting node rabbit@node2
[root@node2 ~] # rabbitmqctl start app
Starting node rabbit@node2

        如果从node2 节点上检查集群的状态, 会发现它现在是独立的节点。同样在集群中剩余的节点nodel 和node3 上看到node2 已不再是集群中的一部分了。正如之前所说的, rabbitmqctl reset 命令将清空节点的状态, 并将其恢复到空白状态。当重设的节点是集群中的一部分时, 该命令也会和集群中的磁盘节点进行通信, 告诉它们该节点正在离开集群。不然集群会认为该节点出了故障, 并期望其最终能够恢复过来。

1.1.3、集群节点的升级

        如果RabbitMQ 集群由单独的一个节点组成,那么升级版本很容易,只需关闭原来的服务,然后解压新的版本再运行即可。不过要确保原节点的Mnesia 中的数据不被变更,且新节点中的Mnesia 路径的指向要与原节点中的相同。或者说保留原节点Mnesia 数据, 然后解压新版本到相应的目录,再将新版本的Mnesia 路径指向保留的Mnesia 数据的路径(也可以直接复制保留的Mnesia 数据到新版本中相应的目录) ,最后启动新版本的服务即可。如果RabbitMQ 集群由多个节点组成,那么也可以参考单个节点的情形。具体步骤:

  1. 关闭所有节点的服务, 注意采用rabbitmqctl stop 命令关闭。
  2. 保存各个节点的Mnesia 数据。
  3. 解压新版本的RabbitMQ 到指定的目录。
  4. 指定新版本的Mnesia 路径为步骤2 中保存的Mnesia 数据路径
  5. 启动新版本的服务,注意先重启原版本中最后关闭的那个节点

        其中步骤4 和步骤5 可以一起操作,比如执行RABBITMQ_MNESIA_BASE=/opt/mnesia
rabbitmq-server-detached 命令,其中/opt/mnesia 为原版本保存Mnesia 数据的路径。

        RabbitMQ 的版本有很多, 难免会有数据格式不兼容的现象, 这个缺陷在越旧的版本中越发凸显,所以在对不同版本升级的过程中,最好先测试两个版本互通的可能性,然后再在线上环境中实地操作。

        如果原集群上的配置和数据都可以舍弃,则可以删除原版本的RabbitMQ ,然后再重新安装
配置即可:如果配置和数据不可丢弃,则先保存元数据,之后再关闭所有生产者并等待消费者消费完队列中的所有数据,紧接着关闭所有消费者,然后重新安装RabbitMQ 并重建元数据等。

1.1.4、单机多节点配置

        由于某些因素的限制,有时候不得不在单台物理机器上去创建一个多RabbitMQ 服务节点的集群。或者只想要实验性地验证集群的某些特性,也不需要浪费过多的物理机器去实现。

        在一台机器上部署多个RabbitMQ 服务节点,需要确保每个节点都有独立的名称、数据存储位置、端口号(包括插件的端口号)等。我们在主机名称为nodel 的机器上创建一个由rabbitl @nodel 、rabbit2@nodel 和rabbit3 @nodel 这3 个节点组成RabbitMQ 集群。

        首先需要确保机器上己经安装了Erlang 和RabbitMQ 的程序。其次,为每个RabbitMQ 服务节点设置不同的端口号和节点名称来启动相应的服务。

[root@nodel ~]# RABBITMQ_NODE_PORT=5672 RABBITMQ_NODENAME=rabbitl
rabbitmq-server -detached
[root@nodel ~]# RABBITMQ_NODE_PORT=5673 RABBITMQ_NODENAME=rabbit2
rabbitmq-server -detached
[root@nodel ~]# RABBITMQ_NODE_PORT=5674 RABBITMQ_NODENAME=rabbit3
rabbitmq-server -detached

        在启动rabbitl@node1节点的服务之后, 继续启动rabbit2@nodel 和rabbit@nodel 服务节点会遇到启动失败的情况。这种情况大多数是由于配置发生了冲突而造成后面的服务节点启动失败, 需要进一步确认是否开启了某些功能,比如RabbitMQ Management 插件。如果开启了RabbitMQ Management 插件,就需要为每个服务节点配置一个对应插件端口号, 具体内容如下所示。

[root@nodel ~]# RABBITMQ_NODE_PORT=5672 RABBITMQ_NODENAME=rabbitl
RABBITMQ_SERVER_START_ARGS="-rabbitmq_management listener [{port , 156721] "
rabbitmq-server -detached
[root@nodel ~]# RABBITMQ_NODE_PORT=5673 RABBITMQ_NODENAME=rabbit2
RABBITMQ_SERVER_START_ARGS= " -rabbitmq_management listener [{port , 1567 3 1] "
rabbitmq-server -detached
[root@nodel ~]# RABBITMQ_NODE_PORT=5674 RABBITMQ_NODENAME=rabbit3
RABBITMQ_SERVER_START_ARGS=" -rabbitmq_management listener [{port , 156741]"
rabbitmq-server -detached

 启动各节点服务之后, 将rabbit2@nodel 节点加入rabbit l @nodel 的集群之中:

[root@nodel ~]# rabbitmqctl -n rabbit2@nodel stop_app
Stopping rabbit application on node rabbit2@nodel
[root@nodel ~]# rabbitmqctl -n rabbit2@nodel reset
Resetting node rabbit2@nodel
[root@nodel ~]# rabbitmqctl -n rabbit2@nodel join_cluster rabbitl@nodel
Clustering node rabbit2@nodel with rabbitl@nodel
[root@nodel ~]# rabbitmqctl -n rabbit2@nodel start_app
Starting node rabbit2@nodel

紧接着可以执行相似的操作将rabbit3@nodel 也加入进来。最后通过rabbitmqctl cluster_status 命令来查看各个服务节点的集群状态。

1.2、查看服务日志

        如果在使用RabbitMQ 的过程中出现了异常情况,通过翻阅RabbitMQ 的服务日志可以让你在处理异常的过程中事半功倍。RabbitMQ 日志中包含各种类型的事件,比如连接尝试、服务启动、插件安装及解析请求时的错误等。本节首先举几个例子来展示一下RabbitMQ 服务日志的内容和日志的等级,接着再来阐述如何通过程序化的方式来获得日志及对服务日志的监控。

        RabbitMQ 的日志默认存放在$RABBITMQ_HOME/var/log/rabbitmq 文件夹内。在这个文件夹内RabbitMQ 会创建两个日志文件: RABBITMQ_NODENAME-sasl.log 和RABBITMQ_NODENAME.log 。

        SASL ( System Application Support Libraries ,系统应用程序支持库)是库的集合,作为Erlang-OTP 发行版的一部分。它们帮助开发者在开发Erlang 应用程序时提供一系列标准,其中之一是日志记录格式。所以当RabbitMQ 记录Erlang 相关信息时,它会将日志写入文件RABBITMQ_NODENAME-sasl . log 中。举例来说,可以在这个文件中找到Erlang 的崩横报告,有助于调试无法启动的RabbitMQ 节点。

        如果想查看RabbitMQ 应用服务的日志,则需要查阅RABBITMQ_NODENAME.log 这个文
件,所谓的RabbitMQ 服务日志指的就是这个文件。各位读者实际使用的RabbitMQ 版本各有差异, 这里我们挑选一个稍旧版本(3 .6.2) 来统筹说明。所幸各个版本的日志大致相同,只是有略微变化。读者需要培养一种使用服务日志来解决问题的思路。

1.2.1、启动RabbitMQ服务

        启动RabbitMQ 服务可以使用rabbitmq-server-detached 命令, 这个命令会顺带启动Erlang 虚拟机和RabbitMQ 应用服务, 而rabbitmqctl start_app 用来启动RabbitMQ应用服务。注意, RabbitMQ 应用服务启动的前提是Erlang 虚拟机是运转正常的。首先来看一下在执行完rabbitmq-server -detached 命令后其相应的服务日志是什么。

Starting Rabb 工tMQ 3.6.2 on Er1ang 19.1
Copyright (C) 2007-2016 Pivota1 Software , Inc .
Licensed under the MPL . See http : //www . rabbitmq.com/
=INFO REPORT==== 3-0ct-2017 : : 10 : 52:08 ===
node : rabbit@node1
home dir : /root
config fi1e(s) : /opt/rabbitmq/etc/rabbitmq/rabbitmq.config (not found)
cookie hash : VCwbL3S9/ydrGgVsrLjVkA==
10g : /opt/rabbitmq/var/1og/rabbitmq/rabbit@node1 . 1og
sas1 10g : /opt/rabbitmq/var/ 1og/rabbitmq/rabbit@node1-sas1 .1og
database dir : /opt/rabbitmq/var/1ib/rabbitmq/mnesia/rabbit@node1
=INFO REPORT==== 3- Oct -2017::10 : 52 : 09 ===

Memory limit set to 3148MB of 7872MB total .
=INFO REPORT==== 3- Oct - 2017 : : 10 : 52 : 09 ===
Disk free l imit set to 50MB
=INFO REPORT==== 3 - Oct - 2017 : :10 : 52 : 09 ===
Limiting to approx 924 file handles (829 sockets)
=INFO REPORT==== 3 - Oct -2017::10 : 52 : 09 ===
FHC read buffering: OFF
FHC write bufferin g : ON
=INFO REPORT==== 3 - Oct - 2017: : 10:52 : 09 ===
Database d 工rectory at /opt/rabbitmq/var/lib/rabbitmq/mnesia/rabbit@node1 is
empty . Initialising from scratch.. .
=INFO REPORT==== 3 - Oct - 2017: : 10:52 : 10 ===
Pr 工ority queues enabled, real BQ 工s rabb 工t variable queue
=INFO REPORT==== 3- Oct - 2017: : 10 : 52 : 10 ===
Adding vhost ' / '
=INFO REPORT==== 3 - Oct - 2017 : : 10 : 52:10 ===
Creating user ' guest '
=INFO REPORT==== 3 - Oct - 2017 : : 10 : 52:10 ===
Setting user tags for user ' guest ' to [adm工n 工stratorl
=INFO REPORT==== 3 - Oct - 2017 : :10 : 52 : 10 ===
Setting permiss 工ons for ' guest ' in ' / ' to ' .*','. *','. *'
=INFO REPORT==== 3 - Oct - 2017 : :10 : 52 : 10 ===
msg store transient: usi 口9 rabbit msg store ets index to provide index
=INFO REPORT==== 3 - Oct - 2017 : :10 : 52 : 10 ===
msg store pers 工stent : using rabb 工t msg store ets index to provide index
=WARNING REPORT==== 3 - Oct - 2017 : :10 : 52 : 10 ===
msg store pers 工stent : rebuild 工ng 工ndices from scratch
=INFO REPORT==== 3 - Oct - 2017 : :10 : 52 : 10 ===
started TCP Listener on [ : :1 : 5672
=INFO REPORT==== 3 - Oct - 2017 :: 10:52 : 10 ===
Server startup complete ; 0 plugins started .

        这段日志包含了RabbitMQ 的版本号、Erlang 的版本号、RabbitMQ 服务节点名称、cookie的hash 值、RabbitMQ 配置文件地址、内存限制、磁盘限制、默认账户guest 的创建及权限配置等。

        注意到上面日志中有"WARNING REPORT" 和"INFO REPORT" 这些字样, 有过编程经验的读者应该可以猜出这与日志级别有关。在RabbitMQ 中,日志级别有none 、error、waming 、info 、debug 这5 种,下一层级别的日志输出均包含上一层级别的日志输出,比如warning 级别的日志包含warning 和error 级别的日志, none 表示不输出日志。日志级别可以通过rabbitmq.config 配置文件中的log levels 参数来进行设置,默认为[ {connection , info) 。

        如果开启了RabbitMQ Management 插件, 则在启动RabbitMQ 的时候会多打印一些日志:

=INFO REPORT==== 3- Oct -2017 : :10 : 57:05 ===
Server startup complete; 6 plugins started.
* rabbitmq management
* rabbitmq_management agent
* rabbitmq_web_dispatch
* webmachine
* mochiweb
* amqp client

#当然还包括一些统计值信息的初始化日志,类似如下:

=INFO REPORT==== 3- Oct -2017 : :10:57:05 ===
Statistics garbage collector started for table {aggr queue stats fine stats ,
5000} .

        与aggr_queue_stats_fine_stats 日志一起的还有很多项指标,比如aggr_queue_stats_deliver_get 和aggr_queue_stats_queue_msg_counts ,由于篇幅所限,这里不再赘述,有兴趣的读者可以自行查看相关版本的统计指标。不同的版本可能略有差异, 一般情况下对此无须过多探究。

        如果使用rabbitmqctl stop_app 命令关闭的RabbitMQ 应用服务, 那么在使用rabbitmqctl start_app 命令开启RabbitMQ 应用服务时的启动日志和rabbitmq-server的启动日志相同。

1.2.2、关闭RabbitMQ服务

        如果使用rabbitmqctl stop 命令,会将Erlang 虚拟机一同关闭,而rabbitmqctl stop_app 只关闭RabbitMQ 应用服务,在关闭的时候要多加注意它们的区别。下面先看一下rabbitmqctl stop_app 所对应的服务日志:

=INFO REPORT==== 3- 0ct-2017 : :10:54:01 ===
Stopping RabbitMQ
=INFO REPORT==== 3- Oct -2017: : 10 : 54 : 01 ===
stopped TCP Listener on [ :: ] : 5672
=INFO REPORT==== 3- Oc t -2017: :10:54:01 ===
Stopped RabbitMQ app1ication

        如果使用rabbitmqctl stop 来进行关闭操作,则会多出下面的日志信息,即关闭Erlang虚拟机。

=INFO REPORT==== 3- Oct -2017: : 10:54: 01 ===
Halting Erlang VM

1.2.3、建立集群

        建立集群也是一种常用的操作。这里举例将节点rabbit@node2 与rabbit@nodel 组成一个集
群,有关如何建立RabbitMQ 集群的细节可以参考上文。

        首先在节点rabbit@node2 中执行rabbitmq-server -detached 开启Erlang 虚拟机和RabbitMQ 应用服务,之后再执行rabbitmqctl stop_app 来关闭RabbitMQ 应用服务,具体的日志可以参考前面的内容。之后需要重置节点rabbit@node2 中的数据rabbitmqctl reset ,相应地在节点rabbit@node2 上输出的日志如下:

=INFO REPORT==== 3- Oct -2017: :11:25:01 ===
Resetting Rabbit

        在rabbit@node2 节点上执行rabbitmqctl join_clcuster rabbit@nodel ,将其加入rabbit@nodel 中以组成一个集群,相应地在rabbit@node2 节点中会打印日志:

=INFO REPORT==== 3- Oct -2017: : 11:30:46 ===
C1ustering with [rabbit@node1] as disc node

与此同时在rabbit@nodel 中会有以下日志:

=INFO REPORT==== 3- Oct -2017: : 11:30 : 56 ===
node rabbit@node2 up

如果此时在rabbit@node2 节点上执行rabbitmqctl stop_app 的动作,那么在rabbit@nodel 节点中会有如下信息:

=INFO REPORT==== 3- Oct - 2017: :11 :54 : 01 ===
rabbit on node rabbit@node2 down
=INFO REPORT==== 3- Oct -2017: :11: 54 : 01 ===
Keep rabbit@node2 listeners : the node is already back

1.2.4、其他

再比如客户端与RabbitMQ 建立连接:

=INFO REPORT==== 14-0ct- 2017 : :1 6 : 24 : 55 ===
accepting AMQP connection <0 . 5865.0> (192.168.0 . 9:61601 - > 192.168.0.2:5672)

当客户端强制中断连接时:

=WARNING REPORT==== 14-Ju1-2017 : : 16 : 36 :57 ===
closing AMQP connection <0 . 5909 . 0> (1 92 . 168 . 0 . 9 : 61629 - > 192 . 168 . 0 . 2:5672 )
connect 工on_closed_abruptly

        有时候RabbitMQ 服务持久运行,其对应的日志也越来越多,尤其是在遇到故障的时候会打
印很多信息。有时候也需要对日志按照某种规律进行切分,以便于后期的管理。RabbitMQ 中可
以通过rabbitmqctl rotate_logs {suffix} 命令来轮换日志,比如手工切换当前的日志:

rabbitmqct1 rotate logs .bak

        之后可以看到在日志目录下会建立新的日志文件,并且将老的日志文件以添加".bak" 后缀的方式进行区分保存:

[root@nodel rabbitmql# ls - a1
-rw-r--r-- 1 root root 0 Ju1 23 00 : 50 rabbit@nodel.1og
-rw-r--r-- 1 root root 22646 Ju1 23 00 : 50 rabbit@nodel.1og.bak
-rw-r--r-- 1 root root o Ju1 23 00:50 rabbit@nodel-sas1.1og
-rw-r--r-- 1 root root o Ju1 23 00 : 50 rabbit@nodel-sas1.log.bak

        也可以执行一个定时任务,比如使用Linux crontab ,以当前日期为后缀,每天执行一次切
换日志的任务,这样在后面需要查阅日志的时候可以根据日期快速定位到相应的日志文件。

1.3、单节点故障恢复

        在RabbitMQ 使用过程中,或多或少都会遇到一些故障。对于集群层面来说,更多的是单点故障。所谓的单点故障是指集群中单个节点发生了故障,有可能会引起集群服务不可用、数据丢失等异常。配置数据节点冗余(镜像队列)可以有效地防止由于单点故障而降低整个集群的可用性、可靠性。

         单节点故障包括:机器硬件故障、机器掉电、网络异常、服务进程异常。单节点机器硬件故障包括机器硬盘、内存、主板等故障造成的死机,无法从软件角度来恢复。此时需要在集群中的其他节点中执行rabbitmqctl forget_cluster_node {nodename} 命令来将故障节点剔除,其中nodename 表示故障机器节点名称。如果之前有客户端连接到此故障节点上,在故障发生时会有异常报出,此时需要将故障节点的 IP 地址从连接列表里删除,并让客户端重新与集群中的节点建立连接,以恢复整个应用。如果此故障机器修复或者原本有备用机器,那么也可以选择性的添加到集群中。

        当遇到机器掉电故障,需要等待电源接通之后重启机器。此时这个机器节点上的RabbitMQ
处于stop 状态,但是此时不要盲目重启服务,否则可能会引起网络分区 。此时同样需要在其他节点上执行rabbitmqctl forget_cluster_node {nodename} 命令将此节点从集群中剔除,然后删除当前故障机器的RabbitMQ 中的Mnesia数据(相当于重置),然后再重启RabbitMQ 服务,最后再将此节点作为一个新的节点加入到当前集群中。

        网线松动或者网卡损坏都会引起网络故障的发生。对于网线松动,无论是彻底断开,还是"藕断丝连",只要它不降速, RabbitMQ 集群就没有任何影响。但是为了保险起见,建议先关闭故障机器的RabbitMQ 进程,然后对网线进行更换或者修复操作,之后再考虑是否重新开启RabbitMQ 进程。而网卡故障极易引起网络分区的发生,如果监控到网卡故障而网络分区尚未发生时,理应第一时间关闭此机器节点上的RabbitMQ 进程,在网卡修复之前不建议再次开启。如果己经发生了网络分区,可以参考4.5 节进行手动恢复网络分区。对于服务进程异常,如RabbitMQ 进程非预期终止,需要预先思考相关风险是否在可控范围之内。如果风险不可控,可以选择抛弃这个节点。一般情况下,重新启动RabbitMQ 服务进程即可。

1.4、集群迁移

        对于RabbitMQ 运维层面来说,扩容和迁移是必不可少的。扩容比较简单,一般向集群中加入新的集群节点即可,不过新的机器节点中是没有队列创建的,只有后面新创建的队列才有可能进入这个新的节点中。或者如果集群配置了镜像队列,可以通过一点"小手术"将原先队列"漂移"到这个新的节点中,具体可以参考第4.5 节。
        迁移同样可以解决扩容的问题,将旧的集群中的数据(包括元数据信息和消息〉迁移到新的且容量更大的集群中即可。RabbitMQ 中的集群迁移更多的是用来解决集群故障不可短时间内修复而将所有的数据、客户端连接等迁移到新的集群中,以确保服务的可用性。相比于单点故障而言,集群故障的危害性就大得多,比如IDC 整体停电、网线被挖断等。这时候就需要通过集群迁移重新建立起一个新的集群。RabbitMQ 集群迁移包括元数据重建、数据迁移,以及与客户端连接的切换。

1.4.1、元数据重建

        元数据重建是指在新的集群中创建原集群的队列、交换器、绑定关系、vhost、用户、权限和Parameter 等数据信息。元数据重建之后才可将原集群中的消息及客户端连接迁移过来。

        有很多种方法可以重建元数据,比如通过手工创建或者使用客户端创建。但是在这之前最耗时耗力的莫过于对元数据的整理,如果事先没有统筹规划, 通过人工的方式来完成这项工作是极其烦琐、低效的,且时效性太差,不到万不得已不建议使用。高效的手段莫过于通过Web管理界面的方式重建,在Web 管理界面的首页最下面有如图所示的内容,这里展示的是 3.8.16 版本的界面,之前的很多版本下面的两项是井排排列的,而非竖着排列,但总体上没有任何影响。

        可以在原集群上点击" Download broker defmitions" 按钮下载集群的元数据信息文件,此文件是一个JSON 文件,比如命名为metadata.json . 其内部详细内容可以参考附录A 。之后再在新集群上的Web 管理界面中点击" Upload broker defmitions " 按钮上传metadata.j son 文件,如果导入成功则会跳转到如图7-3 所示的页面,这样就迅速在新集群中创建了元数据信息。注意,如果新集群有数据与metadata.json 中的数据相冲突,对于交换器、队列及绑定关系这类非可变对象而言会报错,而对于其他可变对象如Parameter、用户等则会被覆盖,没有发生冲突的则不受影响。如果过程中发生错误,则导入过程终止,导致metadata扣on 中只有部分数据加载成功。

        上面这种方式需要考虑三个问题。

第一,如果原集群突发故障,又或者开启RabbitMQ Management 插件的那个节点机器故障不可修复,就无法获取原集群的元数据metadata.json ,这样元数据重建就无从谈起。这个问题也很好解决, 我们可以采取一个通用的备份任务, 在元数据有变更或者达到某个存储周期时将最新的metadata.json 备份至另一处安全的地方。这样在遇到需要集群迁移时, 可以获取到最新的元数据。

第二, 如果新旧集群的RabbitMQ 版本不一致时会出现异常情况, 比如新建立了一个3.6.10 版本的集群, 旧集群版本为3.5.7 ,这两个版本的元数据就不相同。3.5.7 版本中的user 这一项的内容如下, 与3.6.10 版本的加密算法是不一样的。可以参考附录A 中的相关项以进行对比。

{
	"users": [
		{
			"name": "guest",
			"password_hash": "131asf353sd46436ere=",
			"tags": "administrator"
		},
		{
			"name": "root",
			"password_hash": "sdgsdyer6te34534tsdet3ezxf23=",
			"tags": "administrator"
		}
	]
}

再者, 3.6.10 版本中的元数据JSON 文件比3.5.7 版本中多了global_parameters 这一项。一般情况下, RabbitMQ 是能够做到向下兼容的, 在高版本的RabbitMQ 中可以上传低版本的元数据文件。然而如果在低版本中上传高版本的元数据文件就没有那么顺利了, 就以3.6.10版本的元数据加载到3.5.7 版本中就会出现用户登录失败的情况为例, 因为密码加密方式变了,这里可以简单地在Shell 控制台输入变更密码的方式来解决这个问题:

rabbitmqctl change_password {username) {new_password)

        如果还是不能成功上传元数据,那么就需要进一步采取措施了。在此之前,我们首选需要明确一个概念,就是对于用户、策略、权限这种元数据来说内容相对固定,且内容较少,手工重建的代价较小。而且在一个新集群中要能让Web 管理界面运作起来,本身就需要创建用户、设置角色及添加权限等。相反, 集群中元数据最多且最复杂的要数队列、交换器和绑定这三项的内容,这三项内容还涉及其内容的参数设置,如果采用人工重建的方式代价太大,重建元数据的意义其实就在于重建队列、交换器及绑定这三项的相关信息。

        这里有个小窍门,可以将3.6.10 的元数据从queues 这一项前面的内容,包括rabbit_version 、users 、vhosts 、permissions 、parameters 、global_parameters 和 policies这几项内容复制后替换3.5.7 版本中的queues 这一项前面的所有内容, 然后再保存。之后将修改并保存过后的3.5.7 版本的元数据JSON 文件上传到新集群3.6.10 版本的Web 管理界面中,至此就完成了集群的元数据重建(阅读这一段落时可以对照着附录A 的内容来加深理解) 。

第三,就是如果采用上面的方法将元数据在新集群上重建,则所有的队列都只会落到同一个集群节点上,而其他节点处于空置状态,这样所有的压力将会集中到这单台节点之上。举个例子,新集群由nodel 、node2 、node3 节点组成, 其节点的IP 地址分别为192.168.0.2 、192 .1 68.0.3和192.168 . 0 .4 。当访问http://192.168.0.2:15672 页面时,并上传了原集群的元数据metadata.json,
那么原集群的所有队列将只会在nodel 节点上重新建立:

        处理这个问题,有两种方式,都是通过程序(或者脚本)的方式在新集群上建立元数据,而非简单地在页面上上传元数据文件而己。第一种方式是通过HTTP API 接口创建相应的数据。第二种则是通过一个相对完整的Java 程序来处理,这里主要是创建队列、交换器和绑定关系, 而其他内容则忽略。

1.4.2、数据迁移 & 客户端连接切换

        首先需要将生产者的客户端与原RabbitMQ 集群的连接断开,然后再与新的集群建立新的连接,这样就可以将新的消息流转入到新的集群中。

        之后就需要考虑消费者客户端的事情, 一种是等待原集群中的消息全部消费完之后再将连接断开,然后与新集群建立连接进行消费作业。可以通过Web 页面查看消息是否消费完成,可以参考下图 。也可以通过rabbitmqctl list_queues name messages messages_ready messages_unacknowledged 命令来查看是否有未被消费的消息。

[rabit@myblnp ~]$ sudo rabbitmqctl list_queues name messages messages_ready messages_unacknowledged
Timeout: 60.0 seconds ...
Listing queues for vhost / ...
name	messages	messages_ready	messages_unacknowledged
queue.normal	0	0	0
myAe_test	0	0	0
queue_demo	0	0	0
queue	0	0	0
queue.dlx	1	1	0
queue.priority	1	1	0
[rabit@myblnp ~]$ 

        当原集群服务不可用或者出现故障造成服务质量下降而需要迅速将消息流切换到新的集群中时,此时就不能等待消费完原集群中的消息,这里需要及时将消费者客户端的连接切换到新的集群中,那么在原集群中就会残留部分未被消费的消息,此时需要做进一步的处理。如果原集群损坏,可以等待修复之后将数据迁移到新集群中,否则会丢失数据。

        如图所示,数据迁移的主要原理是先从原集群中将数据消费出来,然后存入一个缓存区中,另一个线程读取缓存区中的消息再发布到新的集群中, 如此便完成了数据迁移的动作。作者将此命名为" RabbitMQ ForwardMaker",读者可以自行编写一个小工具来实现这个功能。RabbitMQ 本身提供的Federation 和Shovel 插件都可以实现RabbitMQ ForwardMaker 的功能,确切地说Shovel插件更贴近RabbitMQ ForwardMaker ,详细可以参考后续章节,不过自定义的RabbitMQ ForwardMaker 工具可以让迁移系统更加高效、灵活。

1.4.3、自动化迁移

        要实现集群自动化迁移,需要在使用相关资源时就做好一些准备工作,方便在自动化迁移过程中进行无缝切换。与生产者和消费者客户端相关的是交换器、队列及集群的信息,如果这3 种类型的资源发生改变时需要让客户端迅速感知,以便进行相应的处理,则可以通过将相应的资源加载到ZooKeeper 的相应节点中,然后在客户端为对应的资源节点加入watcher 来感知变化,当然这个功能使用 etcd (是一个分布式一致性的 k-v 存储系统,可用于服务注册发现与共享配置)或者集成到公司层面的资源配置中心中会更加标准、高效。如下图所示,将整个RabbitMQ 集群资源的使用分为三个部分:客户端、集群、ZooKeeper配置管理。

        在集群中创建元数据资源时都需要在ZooKeeper 中生成相应的配置,比如在cluster1 集群中创建交换器exchange1之后,需要在/rmqNode/exchanges 路径下创建实节点exchange1,并赋予节点的数据内容为:

cluster=cluster1 #表示此交换器所在的集群名称
exchangeType=direct #表示此交换器的类型
vhost=vhost1 #表示此交换器所在的vhost
username=root #表示用户名
password=root123 #表示密码

        同样,在cluster1集群中创建队列queue1 之后,需要在/rmqNode/queues 路径下创建实节点queuel1,并赋予节点的数据内容为:

cluster=cluster1
bindings=exchange1 #表示此队列所绑定的交换器
#如果有需要,也可以添加一些其他信息,比如路由键等
vhost=vhost1
userni王me=root
password=root123

        对应集群的数据在/rmqNode/clusters 路径下,比如cluster1 集群,其对应节点的数据内容包含IP 地址列表信息:

ipList=192.168.0.2,192.168.0.3,192.168.0.4 #集群中各个节点的IP 地址信息

        客户端程序如果与其上的交换器或者队列进行交互,那么需要在相应的ZooKeeper 节点中添加watcher,以便在数据发生变更时进行相应的变更,从而达到自动化迁移的目的。

        生产者客户端在发送消息之前需要先连接ZooKeeper ,然后根据指定的交换器名称如exchange1 到相应的路径/rmqNode/exchanges 中寻找exchange1 的节点,之后再读取节点中的数据,井同时对此节点添加watcher。在节点的数据第一条"cluster=cluster1 "中找到交换器所在的集群名称,然后再从路径/rmqNode/clusters 中寻找cluster1节点,然后读取其对应的IP 地址列表信息。这样整个发送端所需要的连接串数据(lp 地址列表、vhost 、usemame 、password 等)都己获取,接下就可以与RabbitMQ 集群clusterl 建立连接然后发送数据了。

        对于消费者客户端而言,同样需要连接ZooKeeper,之后根据指定的队列名称(queue1)到相应的路径/rmqNode/queues 中寻找queue1 节点,继而找到相应的连接串,然后与RabbitMQ 集群cluster1建立连接进行消费。当然对/rmqNode/queues/queuel 节点的watcher必不可少。

        当cluster1 集群需要迁移到cluster2 集群时,首先需要将cluster1集群中的元数据在cluster2集群中重建。之后通过修改zk 的channel 和queue 的元数据信息,比如原cluster1 集群中有交换器exchange1 、exchange2 和队列queue1 、queue2 ,现在通过脚本或者程序将其中的"cluster=cluster1 " 数据修改为" cluster=cluster2 。客户端会立刻感知节点的变化,然后迅速关闭当前连接之后再与新集群cluster2 建立新的连接后生产和消费消息,在此切换客户端连接的过程中是可以保证数据零丢失的。迁移之后,生产者和消费者都会与cJuster2 集群进行互通,此时原cluster1 集群中可能还有未被消费完的数据,此时需要使用上一节所述的RabbitMQ ForwardMaker 工具将 cluster1 集群中未被消费完的数据同步到cluster2 集群中。

        如果没有准备RabbitMQ ForwardMaker 工具,也不想使用Federation 或者Shovel 插件,那么在变更完交换器相关的ZooKeeper 中的节点数据之后,需要等待原集群中的所有队列都消费完全之后,再将队列相关的ZooKeeper 中的节点数据变更,进而使得消费者的连接能够顺利迁移到新的集群之上。可以通过下面的命令来查看是否有队列中的消息未被消费完:

[rabit@myblnp ~]$ sudo rabbitmqctl list_queues -p / -q | awk '{if($2>0) print $0}'
name	messages
queue.dlx	1
queue.priority	1
[rabit@myblnp ~]$ 

        上面的自动化迁移立足于将现有集群迁移到空闲的备份集群,如果由于原集群硬件升级等原因迁移也无可厚非。很多情况下,自动化迁移作为容灾手段中的一种,如果有很多个正在运行的RabbitMQ 集群,为每个集群都配备一个空闲的备份集群无疑是一种资源的浪费。当然可以采取几个集群共用一个备份集群来减少这种浪费,那么有没有更优的解决方案呢?

        就以4 个RabbitMQ 集群为例,其被分配4 个独立的业务使用。如图下图所示, cluster1 集群中的元数据备份到cluster2 集群中,而cluster2 集群中的元数据备份到cluster3 集群中,如此可以两两互备。比如在cluster1 集群中创建了一个交换器exchange1,此时需要在cluster2 集群中同样创建一个交换器exchange1 。在正常情况下,使用的是cluster1 集群中的exchange1,而exchange1 在cluster2 集群中只是一份记录,并不消耗cluster2 集群的任何性能。而当需要将cluster1 迁移时,只需要将交换器及队列相对应的ZooKeeper 节点数据项变更即可完成迁移的工作。如此既不用耗费额外的硬件资源,又不用再迁移的时候重新建立元数据信息。

数据备份

         为了更加稳妥起见,也可以准备一个空闲的备份集群以备后用。当cluster1 集群需要迁移到cluster2 集群中时, cluster2 集群己经发生故障被关闭或者被迁移到cluster3 集群中了,那么这个空闲的备份集群可以当作"Plan B" 来增强整体服务的可靠性。如果既想不浪费多余的硬件资源又想具备更加稳妥的措施,可以参考下图 ,将cluster1 中的元数据备份到cluster2 和cluster3 中,这样"以1 备2" 的方式即可解决这个难题。

以 1 备 2 的备份方式

1.5、集群监控

1.5.1、HTTP API接口监控

        假设集群中一共有4 个节点node 1 、node2 、node3 和node4 , 有一个交换器exchange 通过同一个路由键" rk" 绑定了3 个队列queue 1 、queue2 和queue3 。

        下面首先收集集群节点的信息, 集群节点的信息可以通过 /api/nodes 接口来获取。有关从 /api/ nodes 接口中获取到数据的结构可以参考附录B ,其中包含了很多的数据统计项,可以挑选感兴趣的内容进行数据收集。

package com.blnp.net.rabbitmq.monitor;

/**
 * <p>集群节点信息统计项</p>
 *
 * @author lyb 2045165565@qq.com
 * @createDate 2023/10/30 11:24
 */
public class ClusterNode {

    /**
     *  磁盘空闲
     **/
    private long diskFree;
    private long diskFreeLimit;
    /**
     *  句柄使用数
     **/
    private long fdUsed;
    private long fdTotal;
    /**
     *  Socket 使用数
     **/
    private long socketsUsed;
    private long socketsTotal;
    /**
     *  内存使用值
     **/
    private long memoryUsed;
    private long memoryLimit;
    /**
     *  Erlang 进程使用数
     **/
    private long procUsed;
    private long procTotal ;
    @Override
    public String toString() {
        return " {disk free= " + diskFree + ", " +
                "disk free limit=" + diskFreeLimit + ", " +
                "fd used=" + fdUsed + ", " +
                "fd-total= " + fdTotal + ", " +
                "sockets used= " + socketsUsed + ", " +
                "sockets total=" + socketsTotal + ", " +
                "mem_used=" + memoryUsed + ", " +
                "mem_limit= " + memoryLimit + ", " +
                "proc used= " + procUsed + ", " +
                "proc total=" + procTotal + " } ";
    }

    public long getDiskFree() {
        return diskFree;
    }

    public void setDiskFree(long diskFree) {
        this.diskFree = diskFree;
    }

    public long getDiskFreeLimit() {
        return diskFreeLimit;
    }

    public void setDiskFreeLimit(long diskFreeLimit) {
        this.diskFreeLimit = diskFreeLimit;
    }

    public long getFdUsed() {
        return fdUsed;
    }

    public void setFdUsed(long fdUsed) {
        this.fdUsed = fdUsed;
    }

    public long getFdTotal() {
        return fdTotal;
    }

    public void setFdTotal(long fdTotal) {
        this.fdTotal = fdTotal;
    }

    public long getSocketsUsed() {
        return socketsUsed;
    }

    public void setSocketsUsed(long socketsUsed) {
        this.socketsUsed = socketsUsed;
    }

    public long getSocketsTotal() {
        return socketsTotal;
    }

    public void setSocketsTotal(long socketsTotal) {
        this.socketsTotal = socketsTotal;
    }

    public long getMemoryUsed() {
        return memoryUsed;
    }

    public void setMemoryUsed(long memoryUsed) {
        this.memoryUsed = memoryUsed;
    }

    public long getMemoryLimit() {
        return memoryLimit;
    }

    public void setMemoryLimit(long memoryLimit) {
        this.memoryLimit = memoryLimit;
    }

    public long getProcUsed() {
        return procUsed;
    }

    public void setProcUsed(long procUsed) {
        this.procUsed = procUsed;
    }

    public long getProcTotal() {
        return procTotal;
    }

    public void setProcTotal(long procTotal) {
        this.procTotal = procTotal;
    }
}

        在真正读取 /api/nodes 接口获取数据之前,我们还需要做一些准备工作,比如使用org.apache.commons.httpclient.HttpClient 对 HTTP GET 方法进行封装,方便后续程序直接调用。

package com.blnp.net.rabbitmq.monitor;


import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.core.JsonParser;
import org.apache.http.HttpStatus;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import sun.net.www.http.HttpClient;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;

/**
 * <p></p>
 *
 * @author lyb 2045165565@qq.com
 * @createDate 2023/10/30 11:31
 */
public class HttpUtils {

    public static String httpGet(String url, String username, String password) throws Exception {
        CredentialsProvider provider = new BasicCredentialsProvider();
        provider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(username, password));

        HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
        AuthScope scope = new AuthScope(AuthScope.ANY_HOST, AuthScope.ANY_PORT, AuthScope.ANY_REALM);
        UsernamePasswordCredentials credentials = new UsernamePasswordCredentials(username,password);
        provider.setCredentials(scope, credentials);
        httpClientBuilder.setDefaultCredentialsProvider(provider);
        CloseableHttpClient client = httpClientBuilder.build();

        HttpGet httpGet = new HttpGet(url);
        CloseableHttpResponse httpResponse = client.execute(httpGet);
        int state = httpResponse.getStatusLine().getStatusCode();

        String result = "";

        //等于200
        if (state == HttpStatus.SC_OK) {
            InputStream inputStream = httpResponse.getEntity().getContent();
            InputStreamReader isr = new InputStreamReader(inputStream);
            char[] buffer = new char[1024];
            StringBuilder sb = new StringBuilder();
            int length = 0;
            while ((length = isr.read(buffer)) > 0) {
                sb.append(buffer, 0, length);
            }
            result = sb.toString();
            System.out.println("返回的内容:" + result);
            JSONArray contentJson = JSONObject.parseArray(result);
            System.out.println("contentJson = " + contentJson.toJSONString());
        } else {
            System.out.println("请求返回的状态值:" + state);
        }

        return result;
    }

    public static List<ClusterNode> getClusterData(String ip, int port, String username, String password) {
        List<ClusterNode> list = new ArrayList<ClusterNode>();
        String url = "http://" + ip + ":" + port + "/api/nodes";
        System.out.println(url);

        try {
            String urlData = HttpUtils.httpGet(url, username, password);
            parseClusters(urlData, list) ;
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(list);
        return list;
    }

    private static void parseClusters(String urlData , List<ClusterNode> list) {
        JSONArray jsonArray = JSONObject.parseArray(urlData);

        for(int i=0; i<jsonArray.size(); i++) {
            JSONObject jsonObjectTemp = jsonArray.getJSONObject(i);

            ClusterNode cluster = new ClusterNode() ;
            cluster.setDiskFree(jsonObjectTemp.getLongValue("disk_free"));
            cluster.setDiskFreeLimit(jsonObjectTemp.getLongValue("dlsk_free_limit"));
            cluster.setFdUsed(jsonObjectTemp.getLongValue ("fd_used"));
            cluster.setFdTotal(jsonObjectTemp.getLongValue("fd_total"));
            cluster.setSocketsUsed (jsonObjectTemp.getLongValue("sockets_used"));
            cluster.setSocketsTotal(jsonObjectTemp.getLongValue("sockets_total"));
            cluster.setMemoryUsed (jsonObjectTemp.getLongValue("mem_used"));
            cluster.setMemoryLimit(jsonObjectTemp.getLongValue("mem_limit"));
            cluster.setProcUsed(jsonObjectTemp.getLongValue("proc_used"));
            cluster.setProcTotal(jsonObjectTemp.getLongValue("proc_total"));
            list.add(cluster) ;
        }
    }

    public static void main(String[] args) throws Exception {
        System.out.println("请求结果 = " + getClusterData("192.168.56.106",15672,"admin","admin@123"));
    }
}

使用的maven依赖:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>elasticsearch-demo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.bbossgroups.plugins</groupId>
            <artifactId>bboss-datatran-jdbc</artifactId>
            <version>7.0.8</version>
        </dependency>

        <dependency>
            <groupId>com.bbossgroups.plugins</groupId>
            <artifactId>bboss-elasticsearch-spring-boot-starter</artifactId>
            <version>7.0.8</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>com.rabbitmq</groupId>
            <artifactId>amqp-client</artifactId>
            <version>4.2.1</version>
        </dependency>

        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.5.13</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.79</version>
        </dependency>
    </dependencies>
</project>

        数据来集完之后并没有结束,如下图简单囊括了从数据采集到用户使用的过程。首先采集程序通过定时调用HTTP API 接口获取JSON 数据,然后进行JSON 解析之后再进行持久化处理。对于这种基于时间序列的数据非常适合使用OpenTSDB(基于Hbase 的分布式的,可伸缩的时间序列数据库。主要用途就是做监控系统,比如收集大规模集群(包括网络设备、操作系统、应用程序)的监控数据并进行存储、查询)来进行存储。监控管理系统可以根据用户的检索条件来从OpenTSDB 中获取相应的数据并展示到页面之中。监控管理系统本身还可以具备报表、权限管理等功能,同时也可以实时读取所采集的数据,对其进行分析处理,对于异常的数据需要及时报告给相应的人员。

从数据采集到用户使用的过程

        对于交换器而言的数据采集可以调用 /api/exchanges/vhost/name 接口, 比如需要调用虚拟主机为默认的 "/" 交换器名称为 exchange 的数据, 只需要使用HTTP GET 方法获取 http://xxx.xxx.xxx. xxx :15672/apνexchanges/%2F/exchange 的数据即可。注意, 这里需要将 "/" 进行HTML 转义成" %2F",否则会出错。对应的数据内容可以参考下方:

//单节点Rabbitmq服务返回:
{
	"durable": true,
	"incoming": [],
	"outgoing": [],
	"vhost": "/",
	"internal": false,
	"auto_delete": false,
	"name": "exchange.priority",
	"arguments": {},
	"type": "fanout",
	"user_who_performed_action": "admin"
}


//Rabbitmq集群服务返回:
{
	"durable": true,
	"incoming": [],
	"outgoing": [],
	"vhost": "/",
	"internal": false,
	"auto_delete": false,
	"name": "dem_exchange",
	"arguments": {},
	"message_stats": {
		"publish_in_details": {
			"rate": 0.0 //数据流入的速率
		},
		"publish_out_details": {
			"rate": 0.0 //数据流出的速率
		},
		"publish_in": 1, //数据流入的总量(条)
		"publish_out": 3 //数据流出的总量(条)
	},
	"type": "direct",
	"user_who_performed_action": "myblnp1"
}

        对于1 个交换器绑定3 个队列的情况,向交换器发送1 条消息,那么流入就是 1 条,而流出就是3 条。在应用的时候根据实际情况挑选数据流入速率或者数据流出速率作为发送数量,以及挑选数据流入的量还是数据流出的量作为发送量。

1.5.2、客户端监控

        除了HTTP API 接口可以提供监控数据, Java 版客户端(3. 6.x 版本开始)中Channel 接口中也提供了两个方法来获取数据。方法定义如下:

long messageCount(String queue) throws IOException;

long consumerCount(String queue) throws IOException;

        messageCount (String queue) 用来查询队列中的消息个数,可以为监控消息堆积的情况提供数据。consumerCount(String queue) 用来查询队列中的消费者个数, 可以为监控消费者的情况提供数据。

        除了这两个方法,也可以通过连接的状态进行监控。Java 客户端中Connection 接口提供了addBlockedListener(BlockedListener listener) 方法(用来监昕连接阻塞信息)和addShutdownListener (ShutdownListener listener) 方法(用来监昕连接关闭信息)。相关示例如以下代码所示:

package com.blnp.net.rabbitmq.monitor;

import com.rabbitmq.client.*;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * <p></p>
 *
 * @author lyb 2045165565@qq.com
 * @createDate 2023/10/30 14:26
 */
public class ConnectionMonitor {

    public static void main(String[] args) throws IOException, TimeoutException {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.56.106");
        factory.setPort(5672);
        factory.setUsername("admin");
        factory.setPassword("admin@123");

        //创建mq连接
        Connection connection = factory.newConnection();
        //创建信道
        Channel channel = connection.createChannel();

        connection.addShutdownListener(new ShutdownListener() {
            @Override
            public void shutdownCompleted(ShutdownSignalException cause) {
                //处理并记录连接关闭事项
            }
        }) ;

        connection.addBlockedListener(new BlockedListener() {
            @Override
            public void handleBlocked(String s) throws IOException {
                //处理并记录连接阻塞事项
            }

            @Override
            public void handleUnblocked() throws IOException {
                //处理并记录连接阻塞取消事项
            }
        });

        long msgCount = channel.messageCount( "queue.dlx");
        long consumerCount = channel.consumerCount( "queue.dlx") ;
        //记录msgCount 和consumerCount
        System.out.println("consumerCount = " + consumerCount);
        System.out.println("msgCount = " + msgCount);
    }
}

        用户客户端还可以自行定义一些数据进行埋点,比如客户端成功发送的消息个数和发送失败的消息个数, 进一步可以计算发送消息的成功率等。

package com.blnp.net.rabbitmq.monitor;

import com.rabbitmq.client.*;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeoutException;

/**
 * <p>自定义埋点数据</p>
 *
 * @author lyb 2045165565@qq.com
 * @createDate 2023/10/30 14:39
 */
public class CustomBurialPoint {

    /**
     *  记录发送成功的次数
     **/
    public static volatile int successCount = 0;
    /**
     *  记录发送失败的次数
     **/
    public static volatile int failureCount = 0;

    public static void main(String[] args) {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.56.106");
        factory.setPort(5672);
        factory.setUsername("admin");
        factory.setPassword("admin@123");

        try {
            //创建mq连接
            Connection connection = factory.newConnection();
            //创建信道
            Channel channel = connection.createChannel();

            //开启确认发送机制
            channel.confirmSelect();

            channel.addReturnListener(new ReturnListener() {
                @Override
                public void handleReturn(int replyCode, String replyText, String exchange, String routingKey,
                                         AMQP.BasicProperties basicProperties, byte[] body) throws IOException {
                    failureCount++;
                }
            });

            channel.basicPublish("","",MessageProperties.PERSISTENT_TEXT_PLAIN,"msg".getBytes(StandardCharsets.UTF_8));

            if (channel.waitForConfirms()) {
                successCount++;
            }else {
                failureCount++;
            }
        }catch (Exception e) {
            failureCount++;
        }

    }
}

        上面的代码中只是简单地对successCount 和failureCount 进行累加操作,这里推荐引入metrics 工具(比如com.codahale.metrics.* ) 来进行埋点,这样既方便又高效。同样的方式也可以统计消费者消费成功的条数和消费失败的条数,还可以统计速率。

1.5.3、检测RabbitMQ服务是否健康

        不管是通过HTTP API 接口还是客户端,获取的数据都是以作监控视图之用,不过这一切都基于RabbitMQ 服务运行完好的情况下。虽然可以通过某些其他工具或方法来检测RabbitMQ进程是否在运行(如ps aux I grep rabbitmq) ,或者5672 端口是否开启(如telnet xxx.xxx.xxx.xxx 5672) ,但是这样依旧不能真正地评判RabbitMQ 是否还具备服务外部请求的能力。这里就需要使用AMQP 协议来构建一个类似于TCP 协议中的Ping 的检测程序。当这个测试程序与RabbitMQ 服务无法建立TCP 协议层面的连接,或者无法构建AMQP 协议层面的连接,再或者构建连接超时时,则可判定RabbitMQ 服务处于异常状态而无法正常为外部应用提供相应的服务。示例程序如:

package com.blnp.net.rabbitmq.monitor;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

import java.io.IOException;
import java.io.InputStream;
import java.util.Optional;
import java.util.Properties;
import java.util.concurrent.TimeoutException;

/**
 * <p>检测RabbitMQ服务是否健康</p>
 *
 * @author lyb 2045165565@qq.com
 * @createDate 2023/10/30 15:11
 */
public class AmqpPing {
    private static String host = "192.168.56.119";
    private static int port = 5672;
    private static String vhost = "/" ;
    private static String username = "admin";
    private static String password = "admin@123";

    static {
        Properties properties = new Properties () ;
        try {
            InputStream resourceAsStream = AmqpPing.class.getClassLoader().getResourceAsStream("rmq_cfg.properties");
            if (Optional.ofNullable(resourceAsStream).isPresent()) {
                properties.load(AmqpPing.class.getClassLoader().getResourceAsStream( "rmq_cfg.properties")) ;
                if (Optional.ofNullable(properties).isPresent()) {
                    host = properties.getProperty( "host");
                    port = Integer.valueOf(properties.getProperty( "port"));
                    vhost = properties . getProperty("vhost");
                    username = properties . getProperty("username");
                    password = properties . getProperty("password");
                }
            }
        }catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static PING_STATUS checkAmqpPing() {
        PING_STATUS pingStatus = PING_STATUS.OK;
        ConnectionFactory connectionFactory = new ConnectionFactory() ;
        connectionFactory.setHost (host) ;
        connectionFactory.setPort(port) ;
        connectionFactory.setVirtualHost(vhost);
        connectionFactory.setUsername(username) ;
        connectionFactory.setPassword(password);
        Connection connection = null;
        Channel channel = null ;

        try {
            connection = connectionFactory.newConnection ();
            channel = connection.createChannel();
        }catch (IOException | TimeoutException e ) {
            e.printStackTrace() ;
            pingStatus = PING_STATUS.EXCEPTION;
        }finally {
            if (connection != null) {
                try {
                    connection.close() ;
                }catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return pingStatus;
    }

    public static void main(String[] args) {
        System.out.println("checkAmqpPing() = " + checkAmqpPing());
    }
}

enum PING_STATUS {
    /**
     *  服务正常
     **/
    OK,
    /**
     *  服务异常
     **/
    EXCEPTION;
}

        示例中涉及 rmq_cfg.properties 配置文件,这个文件用来灵活地配置与RabbitMQ 服务的连接所需的连接信息,包括IP 地址、端口号、vhost、用户名和密码等。如果没有配置相应的项则可以采用默认的值。

        监控应用时,可以定时调用AmqpPing.checkAMQPPing() 方法来获取检测信息,方法返回值是一个枚举类型,示例中只具备两个值: PING_STATUS.OK 和PING_STATUS.EXCEPTION,分别代表RabbitMQ 服务正常和异常的情况,这里可以根据实际应用情况来细分返回值的粒度。

        AmqpPing 这个类能够检测RabbitMQ 是否能够接收新的请求和构造AMQP 信道,但是要检测RabbitMQ 服务是否健康还需要进一步的措施。值得庆幸的是RabbitMQ Management 插件提供了/api/aliveness-test/vhost 的 HTTP API 形式的接口,这个接口通过3 个步骤来验证RabbitMQ 服务的健康性:

  1. 创建一个以 "aliveness-test" 为名称的队列来接收测试消息。
  2. 用队列名称, 即 "aliveness-test" 作为消息的路由键,将消息发往默认交换器。
  3. 到达队列时就消费该消息,否则就报错。

        这个HTTP API 接口背后的检测程序也称之为 aliveness-test,其运行在Erlang 虚拟机内部,因此它不会受到网络问题的影响。如果在虚拟机外部,则网络问题可能会阻止外部客户端连接到RabbitMQ 的5672 端口。aliveness-test 程序不会删除创建的队列,对于频繁调用这个接口的情况,它可以避免数以千计的队列元数据事务对 Mnesia 数据库造成巨大的压力。如果RabbitMQ服务完好,调用 /api/aliveness-test/vhost 接口会返回{"status": "ok"} , HTTP 状态码为200。

package com.blnp.net.rabbitmq.monitor;

import org.apache.http.HttpStatus;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

/**
 * <p></p>
 *
 * @author lyb 2045165565@qq.com
 * @createDate 2023/10/30 15:29
 */
public class AlivenessTest {

    public static ALIVE_STATUS checkAliveness(String url,String username,String passwd) throws IOException {
        ALIVE_STATUS aliveStatus = ALIVE_STATUS.OK;

        CredentialsProvider provider = new BasicCredentialsProvider();
        provider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(username, passwd));

        HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
        AuthScope scope = new AuthScope(AuthScope.ANY_HOST, AuthScope.ANY_PORT, AuthScope.ANY_REALM);
        UsernamePasswordCredentials credentials = new UsernamePasswordCredentials(username,passwd);
        provider.setCredentials(scope, credentials);
        httpClientBuilder.setDefaultCredentialsProvider(provider);
        CloseableHttpClient client = httpClientBuilder.build();

        HttpGet httpGet = new HttpGet(url);
        CloseableHttpResponse httpResponse = client.execute(httpGet);
        int state = httpResponse.getStatusLine().getStatusCode();
        //等于200
        if (state == HttpStatus.SC_OK) {
            InputStream inputStream = httpResponse.getEntity().getContent();
            InputStreamReader isr = new InputStreamReader(inputStream);
            char[] buffer = new char[1024];
            StringBuilder sb = new StringBuilder();
            int length = 0;
            while ((length = isr.read(buffer)) > 0) {
                sb.append(buffer, 0, length);
            }
            System.out.println("sb = " + sb.toString());
            if (!"{\"status\":\"ok\"}".equals(sb.toString())) {
                aliveStatus = ALIVE_STATUS.EXCEPTION;
            }
        } else {
            aliveStatus = ALIVE_STATUS.EXCEPTION;
        }
        return aliveStatus;
    }

    public static void main(String[] args) throws IOException {
        System.out.println("checkAliveness() = " + checkAliveness("http://192.168.56.119:15672/api/aliveness-test/%2F","admin","admin@123"));
    }
}

enum ALIVE_STATUS {
    /**
     *  服务正常
     **/
    OK,
    /**
     *  服务异常
     **/
    EXCEPTION;
}

        这里的 aliveness-test 程序配合前面的AmqpPing 程序一起使用可以从内部和外部这两个方面来全面地监控 RabbitMQ 服务。此外还有另外两个接口 /api/healthchecks/node和 /api/healthchecks/nodes/node , 这两个HTTP API 接口分别表示对当前节点或指定节点进行基本的健康检查,包括RabbitMQ 应用、信道、队列是否运行正常,是否有告警产生等。使用方式可以参考 /api/aliveness-test/vhost , 在此不再赘述。

2、跨越集群的界限

        RabbitMQ 可以通过3 种方式实现分布式部署:集群、Federation 和Shovel。这3 种方式不是互斥的,可以根据需要选择其中的一种或者以几种方式的组合来达到分布式部署的目的。Federation 和Shovel 可以为RabbitMQ 的分布式部署提供更高的灵活性,但同时也提高了部署的复杂性。

2.1、Federation

        Federation 插件的设计目标是使RabbitMQ 在不同的Broker 节点之间进行消息传递而无须建立集群,该功能在很多场景下都非常有用:

  • Federation 插件能够在不同管理域(可能设置了不同的用户和vhost ,也可能运行在不同版本的RabbitMQ 和Erlang 上〉中的Broker 或者集群之间传递消息。
  • Federation 插件基于AMQP 0-9-1 协议在不同的Broker 之间进行通信,并设计成能够容忍不稳定的网络连接情况。
  • 一个Broker 节点中可以同时存在联邦交换器(或队列)或者本地交换器(或队列),只需要对特定的交换器(或队列)创建Federation 连接 (Federation link ) 。
  • Federation 不需要在N 个Broker 节点之间创建O(N^2)个连接(尽管这是最简单的使用方式) ,这也就意味着Federation 在使用时更容易扩展。

        Federation 插件可以让多个交换器或者多个队列进行联邦。一个联邦交换器 (federated exchange) 或者一个联邦队列 (federated queue) 接收上游 (upstream) 的消息,这里的上游是指位于其他Broker 上的交换器或者队列。联邦交换器能够将原本发送给上游交换器 (upstream exchange) 的消息路由到本地的某个队列中;联邦队列则允许一个本地消费者接收到来自上游队列 (upstream queue) 的消息。

2.1.1、联邦交换器

        假设图中broker1 部署在北京, broker2 部署在上海,而broker3 部署在广州,彼此之间相距甚远,网络延迟是一个不得不面对的问题。

联邦交换器

        有一个在广州的业务ClientA需要连接broker3 ,并向其中的交换器exchangeA 发送消息,此时的网络延迟很小, ClientA可以迅速将消息发送至exchangeA 中,就算在开启了publisher confirrn 机制或者事务机制的情况下,也可以迅速收到确认信息。此时又有一个在北京的业务ClientB 需要向exchangeA 发送消息,那么ClientB 与broker3 之间有很大的网络延迟, ClientB将发送消息至exchangeA 会经历一定的延迟,尤其是在开启了publisher confrrrn 机制或者事务机制的情况下, ClientB 会等待很长的延迟时间来接收broker3 的确认信息,进而必然造成这条发送线程的性能降低,甚至造成一定程度上的阻塞。

        那么要怎么优化业务ClientB呢?将业务ClientB 部署到广州的机房中可以解决这个问题,但是如果ClientB调用的另一些服务都部署在北京,那么又会引发新的时延问题,总不见得将所有业务全部部署在一个机房,那么容灾又何以实现?这里使用 Federation 插件就可以很好地解决这个问题。

        如图下图所示,在broker3 中为交换器exchangeA (broker3 中的队列queueA 通过"rkA"与exchangeA 进行了绑定)与广州的broker1 之间建立一条单向的Federation link。此时Federation插件会在broker1 上会建立一个同名的交换器exchangeA (这个名称可以配置,默认同名),同时建立一个内部的交换器"exchangeA→ broker3 B ",并通过路由键"rkA"将这两个交换器绑定起来。这个交换器 " exchangeA• broker3 B" 名字中的" broker3 "是集群名,可以通过rabbitmqctl set_cluster_name {new name} 命令进行修改。与此同时Federation 插件还会在brokerl 上建立一个队列"federation: exchangeA• broker3 B 飞井与交换器 "exchangeA• broker3 B" 进行绑定。Federation 插件会在队列"federation: exchangeA• broker3 B" 与broker3中的交换器exchangeA 之间建立一条AMQP 连接来实时地消费队列"federation: exchangeA•broker3 B" 中的数据。这些操作都是内部的,对外部业务客户端来说这条Federation link 建立在broker1 的exchangeA 和broker3 的exchangeA 之间。

建立 Federation link

         回到前面的问题,部署在北京的业务ClientB 可以连接broker1 并向exchangeA 发送消息,这样ClientB可以迅速发送完消息并收到确认信息, 而之后消息会通过Federation link 转发到broker3 的交换器exchangeA 中。最终消息会存入与exchangeA 绑定的队列queueA 中,消费者最终可以消费队列queueA 中的消息。经过Federation link 转发的消息会带有特殊的headers 属性标记。例如向broker1 中的交换器exchangeA 发送一条内容为"federation test payload. "的持久化消息,之后可以在broker3 中的队列queueA 中消费到这条消息,详细如下图所示。

        Federation 不仅便利于消息生产方,同样也便利于消息消费方。假设某生产者将消息存入broker1 中的某个队列queueB , 在广州的业务ClientC 想要消费queueB 的消息,消息的流转及确认必然要忍受较大的网络延迟,内部编码逻辑也会因这一因素变得更加复杂,这样不利于ClientC 的发展。不如将这个消息转发的过程以及内部复杂的编程逻辑交给Federation 去完成,而业务方在编码时不必再考虑网络延迟的问题。Federation 使得生产者和消费者可以异地部署而又让这两方感受不到过多的差异。

        图中broker1 的队列 "federation: exchangeA -> broker3 B " 是一个相对普通的队列,可以直接通过客户端进行消费。假设此时还有一个客户端ClientD 通过 Basic.Consume 来消费队列"federation: exchangeA• broker3 B" 的消息,那么发往broker1 中exchangeA 的消息会有一部分(一半〉被ClientD消费掉,而另一半会发往broker3 的exchangeA。所以如果业务应用有要求所有发往broker1 中exchangeA 的消息都要转发至broker3 的exchangeA 中,此时就要注意队列" federation : exchangeA• broker3 B" 不能有其他的消费者;而对于"异地均摊消费"这种特殊需求,队列"federation: exchangeA• broker3 B" 这种天生特性提供了支持。对于broker1的交换器exchangeA 而言,它是一个普通的交换器,可以创建一个新的队列绑定它,对它的用法没有什么特殊之处。

        如图8-4 所示, 一个federated exchange 同样可以成为另一个交换器的upstream exchange。同样如图8-5 所示,两方的交换器可以互为federated exchange 和upstream exchange 。其中参数"max_hops=l" 表示一条消息最多被转发的次数为1。

图8-4 federation exchange 成为另一个交换器的 upstream exchange
图8-5 两方的交换器互为 federation echange 和 upstream exchange

        需要特别注意的是,对于默认的交换器(每个vhost 下都会默认创建一个名为""的交换器)和内部交换器而言,不能对其使用 Federation 的功能。对于联邦交换器而言,还有更复杂的拓扑逻辑部署方式。比如图8-6 中"fan-out " 的多叉树形式,或者图8-7 中"三足鼎立"的情形。

图8-6 “fan-out” 的多叉树
图8-7 三足鼎立
图8-8 环形的拓扑部署

2.1.2、联邦队列

        除了联邦交换器, RabbitMQ 还可以支持联邦队列(federated queue) 。联邦队列可以在多个Broker 节点(或者集群〉之间为单个队列提供均衡负载的功能。一个联邦队列可以连接一个或者多个上游队列(upstream queue) ,并从这些上游队列中获取消息以满足本地消费者消费消息的需求。

        图8-9 演示了位于两个Broker 中的几个联邦队列(灰色〉和非联邦队列(白色) 。队列queue1和queue2 原本在broker2 中,由于某种需求将其配置为federated queue 并将broker1作为upsteam。
Federation 插件会在broker1 上创建同名的队列queue1 和queue2,与broker2 中的队列queue1 和
queue2 分别建立两条单向独立的Federation link 。当有消费者ClinetA连接broker2 并通过Basic.Consume 消费队列queue1 (或queue2) 中的消息时,如果队列queue1 (或queue2) 中本身有若干消息堆积,那么ClientA直接消费这些消息,此时broker2 中的queue1 (或queue2 )并不会拉取broker1中的queue1 (或queue2 ) 的消息:如果队列queue1 (或queue2 ) 中没有消息堆积或者消息被消费完了,那么它会通过Federation link 拉取在broker1 中的上游队列queue1 (或queue2) 中的消息(如果有消息),然后存储到本地,之后再被消费者ClientA进行消费。

图8-9 联邦队列

 消费者既可以消费broker2 中的队列,又可以消费broker1中的队列, Federation 的这种分布式队列的部署可以提升单个队列的容量。如果在broker1 一端部署的消费者来不及消费队列queue1 中的消息,那么broker2 一端部署的消费者可以为其分担消费,也可以达到某种意义上的负载均衡。

        和federated exchange 不同, 一条消息可以在联邦队列间转发无限次。如图8-10 中两个队列queue 互为联邦队列。

图8-10 互为联邦队列

         队列中的消息除了被消费,还会转向有多余消费能力的一方,如果这种"多余的消费能力"在broker1 和broker2 中来回切换,那么消费也会在broker1 和broker2 中的队列queue 中来回转发。

        可以在其中一个队列上发送一条消息 "msg" , 然后再分别创建两个消费者ClientB 和ClientC分别连接broker1 和broker2 , 并消费队列queue 中的消息,但是并不需要确认消息(消费完消息不需要调用Basic. Ack) 。来回开启/关闭ClientB 和ClientC 可以发现消息" msg"会在broker1和broker2 之间串来串去。

        图8 -11 中的broker2 的队列queue 没有消息堆积或者消息被消费完之后并不能通过Basic.Get 来获取broker1 中队列queue 的消息。因为Basic.Get 是一个异步的方法,如果要从broker1 中队列queue 拉取消息,必须要阻塞等待通过Federation link 拉取消息存入broker2 中的队列queue 之后再消费消息,所以对于federated queue 而言只能使用Basic.Consume 进行消费

        federated queue 并不具备传递性。考虑图8 -11 的情形,队列queue2 作为federated queue 与队列queue1 进行联邦,而队列queue2 又作为队列queue3 的upstramqueue ,但是这样队列queue1与queue3 之间并没有产生任何联邦的关系。如果队列queue1 中有消息堆积, 消费者连接broker3消费queue3 中的消息,无论queue3 处于何种状态,这些消费者都消费不到queue1 中的消息,除非queue2 有消费者。

图8-11 联邦队列的传递性

注意要点:理论上可以将一个federated queue 与一个federated exchange 绑定起来,不过这样会导致一些不可预测的结果,如果对结果评估不足,建议慎用这种搭配方式.

2.1.3、Federation 的使用

为了能够使用Federation 功能, 需要配置以下2 个内容:

  1. 需要配置一个或多个 upstream ,每个upstream 均定义了到其他节点的Federation link。这个配置可以通过设置运行时的参数( Runtime Parameter ) 来完成,也可以通过federation management 插件来完成。
  2. 需要定义匹配交换器或者队列的一种/多种策略( Policy ) 。

        Federation 插件默认在RabbitMQ 发布包中,执行rabbitmq-plugins enable rabbitmq_federation 命令可以开启Federation 功能,示例如下:

[rabbit@myblnp1 ~]$ rabbitmq-plugins enable rabbitmq_federation
Enabling plugins on node rabbit@myblnp1:
rabbitmq_federation
The following plugins have been configured:
  rabbitmq_federation
  rabbitmq_management
  rabbitmq_management_agent
  rabbitmq_web_dispatch
Applying plugin configuration to rabbit@myblnp1...
The following plugins have been enabled:
  rabbitmq_federation

started 1 plugins.
[rabbit@myblnp1 ~]$ 

特别说明:当需要在集群中使用Federation 功能的时候,集群中所有的节点都应该开启Federation 插件

        根据前面的讲解可知, Federation 内部基于AMQP 协议拉取数据,所以在开启rabbitmq_federation 插件的时候,默认会开启 amqp_client 插件。同时,如果要开启Federation 的管理插件,需要执行rabbitmq-plugins enable rabbitmq_federation_management 命令, 示例如下:

[rabbit@myblnp1 ~]$ rabbitmq-plugins enable rabbitmq_federation_management
Enabling plugins on node rabbit@myblnp1:
rabbitmq_federation_management
The following plugins have been configured:
  rabbitmq_federation
  rabbitmq_federation_management
  rabbitmq_management
  rabbitmq_management_agent
  rabbitmq_web_dispatch
Applying plugin configuration to rabbit@myblnp1...
The following plugins have been enabled:
  rabbitmq_federation_management

started 1 plugins.
[rabbit@myblnp1 ~]$ 

        开启 rabbitmq_federation_management 插件之后,在RabbitMQ 的管理界面中 "Admin" 的右侧会多出"Federation Status" 和"Federation Upstreams" 两个Tab 页,如图:

        rabbitmq_federation_management 插件依附于rabbitmq_management 插件,所以开启rabbitmq_federation_management 插件的同时默认也会开启rabbitmq_management 插件。

        有关Federation upstream 的信息全部都保存在RabbitMQ 的Mnesia 数据库中,包括用户信息、权限信息、队列信息等。在Federation 中存在3 种级别的配置。

  1. Upstreams: 每个upstream 用于定义与其他Broker 建立连接的信息。
  2. Upstream sets: 每个upstream set 用于对一系列使用Federation 功能的upstream 进行分组。
  3. Policies: 每一个Policy 会选定出一组交换器,或者队列,亦或者两者皆有而进行限定,进而作用于一个单独的 upsteam 或者upstream set 之上。

        实际上,在简单使用场景下,基本上可以忽略 upstream set 的存在,因为存在一种名为" all"并且隐式定义的 upstream set ,所有的 upstream 都会添加到这个set 之中。Upstreams 和Upstream sets 都属于运行时参数,就像交换器和队列一样,每个vhost 都持有不同的参数和策略的集合。

Federation 相关的运行时参数和策略都可以通过下面3 种方式进行设置:

  1. 通过rabbitmqctl 工具。
  2. 通过RabbitMQ Management 插件提供的HTTP API 接口
  3. 通过 rabbitmq_federation_management 插件提供的Web 管理界面的方式(最方便且通用)。不过基于Web 管理界面的方式不能提供全部功能,比如无法针对 upstream set 进行管理。

        下面就详细讲解如何正确地使用 Federation 插件,首先以 broker1 ( IP 地址:192.168.56.119)和broker2 (IP 地址: 192 .1 68.56.120)的关系来讲述如何建立federated exchange 。


第一步:

        需要在broker1 和broker2 中开启rabbitmq_federation 插件,最好同时开启rabbitmq_federation_management 插件。

Broker 1
Broker 2

第二步:

        在broker2 中定义一个upstream。

##第一种方式:通过rabbitmqctl 工具的方式,详细如下:
[rabbit@myblnp2 ~]$ rabbitmqctl set_parameter federation-upstream f1 '{"uri":"amqp://root:root123@192.168.56.119:5672","ack-mode":"on-confirm"}'
Setting runtime parameter "f1" for component "federation-upstream" to "{"uri":"amqp://root:root123@192.168.56.119:5672","ack-mode":"on-confirm"}" in vhost "/" ...
[rabbit@myblnp2 ~]$ 



##第二种方式:通过调用HTTP API 接口的方式,详细如下:
[rabbit@myblnp2 ~]$ curl -i -u root:root123 -XPUT -d '{"value":{"uri":"amqp://root:root123@192.168.56.119:5672","ack-mode":"on-confirm"}}' http://192.168.56.120:15672/api/parameters/federation-upstream/%2F/f2
HTTP/1.1 201 Created
content-length: 0
content-security-policy: script-src 'self' 'unsafe-eval' 'unsafe-inline'; object-src 'self'
date: Tue, 31 Oct 2023 06:38:37 GMT
server: Cowboy
vary: accept, accept-encoding, origin

[rabbit@myblnp2 ~]$ 



##第三种方式:通过在Web 管理界面中添加的方式,在"Admin" → "Pederation Upstreams" -> "Add a new upstream" 中创建

通用的参数如下所示:

  • Name: 定义这个upstream 的名称。必填项。
  • URI (uri): 定义upstream 的AMQP 连接。必填项。本示例中可以填写为:amqp://root:root123@192.168.56.119:5672
  • Prefetch count (prefetch_count): 定义Federation 内部缓存的消息条数,即在收到上游消息之后且在发送到下游之前缓存的消息条数。
  • Reconnect delay (reconnect-delay): Federation link 由于某种原因断开之后,需要等待多少秒开始重新建立连接。
  • Acknowledgement Mode (ack-mode): 定义Federation link 的消息确认方式。其有3 种: on-confirm 、on-publish 、no-acko 默认为on-confirm ,表示在接收到下游的确认消息(等待下游的Basic.Ack) 之后再向上游发送消息确认,这个选项可以确保网络失败或者Broker 宕机时不会丢失消息,但也是处理速度最慢的选项。如果设置为on-publish ,则表示消息发送到下游后(并需要等待下游的Basic.Ack)再向上游发送消息确认,这个选项可以确保在网络失败的情况下不会丢失消息,但不能
  • 确保Broker 岩机时不会丢失消息。no - ack 表示无须进行消息确认,这个选项处理速度最快,但也最容易丢失消息。
  • Trust User-ID (trust-user-id): 设定Federation 是否使用"Validated User-ID" 这个功能。如果设置为false 或者没有设置,那么Federation 会忽略消息的user_id 这个属性;如果设置为true ,则Federation 只会转发user_id 为上游任意有效的用户的消息。

        所谓的 "Validated User-ID" 功能是指发送消息时验证消息的user_id 的属性,在前面章节中讲到channel.basicPublish 方法中有个参数是BasicProperties ,这个BasicProperties 类中有个属性为userld。可以通过如下的方法设置消息的user_id 属性为 "root":

AMQP.BasicProperties properties = new AMQP.BasicProperties();
properties.setUserId("root");
channel.basicPublish("amq.fanout", "", properties, "test user id".getBytes());

        如果在连接Broker 时所用的用户名为"root",当发送"test user id"这条消息时设置的user_id的属性为"guest",那么这条消息会发送失败, 具体报错为406 PRECONDITION FAILED - user id property set to 'guest' but authenticated user was 'root' ,只有当user_id 设置为"root" 时这条消息才会发送成功。

只适合Federation Exchange的参数:

  • Exchange (exchange): 指定upstream exchange 的名称,默认情况下和federated exchange 同名,即图8-2 中的exchangeA 。
  • Max hops (max-hops): 指定消息被丢弃前在Federation link 中最大的跳转次数。默认为1 。注意即使设置max-hops 参数为大于1 的值,同一条消息也不会在同一个Broker中出现2 次,但是有可能会在多个节点中被复制。
  • Expires (expires): 指定Federation link 断开之后, federated queue 所对应的upstream queue( 即图8-2 中的队列"federation: exchangeA• broker3 B") 的超时时间,默认为"none" ,表示为不删除,单位为ms 。这个参数相当于设置普通队列的x-expires 参数。设置这个值可以避免Federation link 断开之后,生产者一直在向broker1中的exchangeA 发送消息,这些消息又不能被转发到broker3 中而被消费掉,进而造成broker1中有大量的消息堆积。
  • Message TTL (message-ttl): 为federated queue 所对应的 upstream queue (即图8-2 中的队列 "federation: exchangeA• broked B ")设置,相当于普通队列的 x-message-ttl 参数。默认为"none" 表示消息没有超时时间。
  • HA policy (ha-policy): 为federated queue 所对应的upstream queue (即图8-2 中的队列 "federation: exchangeA• broker3 B" ) 设置,相当于普通队列的x-ha-policy参数,默认为"none飞表示队列没有任何HA 。

只适合Federation Queue的参数:

  • Queue (queue) :执行upstream queue 的名称,默认情况下和federated queue 同名,可以参考图8-10 中的queue 。
  • Consurm tag(consurm-tag):从上游消费时使用的消费者标签。可选的。

第三步:

        定义一个Policy 用于匹配交换器exchangeA,并使用第二步中所创建的upstream 。

##第一种方式:通过rabbitmqctl 工具的方式,如下(定义所有以"exchange" 开头的交换器作为federated exchange):
[rabbit@myblnp2 ~]$ rabbitmqctl set_policy --apply-to exchanges p1 "^exchange" '{"federation-upstream":"f1"}'
Setting policy "p1" for pattern "^exchange" to "{"federation-upstream":"f1"}" with priority "0" for vhost "/" ...
[rabbit@myblnp2 ~]$ 



##第二种方式:是通过HTTP API 接口的方式
[rabbit@myblnp2 ~]$ curl -i -u root:root123 -XPUT -d '{"pattern":"^f2_","definition":{"federation-upstream":"f2"},"apply-to":"exchanges"}' http://192.168.56.120:15672/api/policies/%2F/p2
HTTP/1.1 201 Created
content-length: 0
content-security-policy: script-src 'self' 'unsafe-eval' 'unsafe-inline'; object-src 'self'
date: Tue, 31 Oct 2023 07:11:16 GMT
server: Cowboy
vary: accept, accept-encoding, origin

[rabbit@myblnp2 ~]$ 



##第三种方式:在Web 管理界面中添加的方式,在"Admin " → "Policies" → "Add/update a policy" 中创建
web 方式添加

 Federation Upstream:

Policies:

Federation Status:

         这样就创建了一个Federation link ,可以在Web 管理界面中" Admin" → "Federation Status" ->  "Running Links" 查看到相应的链接。还可以通过rabbitmqctl eval 'rabbit_federation_status:status().'  命令来查看相应的Federation link。示例如下:

[rabbit@myblnp1 ~]$ rabbitmqctl eval 'rabbit_federation_status:status().'
[[{exchange,<<"exchange_de">>},
  {upstream_exchange,<<"exchange_de">>},
  {type,exchange},
  {vhost,<<"/">>},
  {upstream,<<"f1">>},
  {id,<<"b3047226">>},
  {status,running},
  {local_connection,<<"<rabbit@myblnp1.1698731305.5662.0>">>},
  {uri,<<"amqp://192.168.56.119:5672">>},
  {timestamp,{{2023,10,31},{15,7,37}}}]]
[rabbit@myblnp1 ~]$ 

特别说明:Federation Link 当匹配有对应策略后,在 Federation status 里才会有具体数据

        对于federated queue 的建立,首先同样也是定义一个upstream。之后定义Policy 的时候略微有变化,比如使用rabbitmqctl 工具的情况(定义所有以"queue" 开头的队列作为federated queue ) :

rabbitmqctl set_policy --apply-to queues p2 "^queue" '{"federation-upstream":"f1"}'

        通常情况下,针对每个upstream 都会有一条Federation link , 该Federation link 对应到一个交换器上。例如, 3 个交换器与2 个upstream 分别建立Federation link 的情况下,会有6条连接。

2.2、Shovel

        与Federation 具备的数据转发功能类似,Shovel 能够可靠、持续地从一个Broker 中的队列(作为源端,即source )拉取数据并转发至另一个Broker 中的交换器(作为目的端,即destination )。作为源端的队列和作为目的端的交换器可以同时位于同一个Broker 上,也可以位于不同的Broker 上。Shovel 可以翻译为"铲子",是一种比较形象的比喻,这个" 铲子"可以将消息从一方"挖到"另一方。Shovel 的行为就像优秀的客户端应用程序能够负责连接源和目的地、负责消息的读写及负责连接失败问题的处理。Shovel 的主要优势在于:

  • 松耦合:Shovel 可以移动位于不同管理域中的Broker (或者集群)上的消息,这些Broker (或者集群〉可以包含不同的用户和 vhost ,也可以使用不同的RabbitMQ 和Erlang 版本。
  • 支持广域网:Shovel 插件同样基于AMQP 协议在Broker 之间进行通信, 被设计成可以容忍时断时续的连通情形, 并且能够保证消息的可靠性。
  • 高度定制:当Shovel 成功连接后,可以对其进行配置以执行相关的AMQP 命令。

2.2.1、Shovel 的原理

        图8-15 展示的是Shovel 的结构示意图。这里一共有两个Broker:broker1(IP 地址:192.168.56.119) 和broker2 (lp 地址: 192.168.56.120)。 broker1 中有交换器 dexchange1 和队列queue1 ,且这两者通过路由键"rk1 "进行绑定; broker2 中有交换器 dexchange2 和队列queue2 ,且这两者通过路由键"rk2 "进行绑定。在队列 queue1 和交换器exchange2 之间配置一个Shovel link , 当一条内容为 "shovel test payload" 的消息从客户端发送至交换器exchange1 的时候,这条消息会经过图8-15中的数据流转最后存储在队列queue2 中。如果在配置Shovel link 时设置了
add-forward-headers 参数为true ,则在消费到队列queue2 中这条消息的时候会有特殊的 headers 属性标记,详细内容可参考图8-16。

图8-15 Shovel 的结构
图8-16 消息的内容

        通常情况下,使用Shovel 时配置队列作为源端,交换器作为目的端,就如图8-15 一样。同样可以将队列配置为目的端,如图8-17 所示。虽然看起来队列queue1 是通过Shovel link 直接将消息转发至queue2 的, 其实中间也是经由broker2 的交换器转发, 只不过这个交换器是默认的交换器而己。

图-17 将队列配置为目的端

        如图8-18 所示,配置交换器为源端也是可行的。虽然看起来交换器 exchange1 是通过Shovel link 直接将消息转发至exchange2 上的,实际上在broker1 中会新建一个队列(名称由RabbitMQ自定义,比如图8-18 中的"amq.gen-ZwolUsoUchY6a7xaPyrZZH") 并绑定exchange1,消息从交换器exchange1 过来先存储在这个队列中,然后Shovel 再从这个队列中拉取消息进而转发至交换器exchange2 。

图8-18 配置交换器为源端

        前面所阐述的broker1 和broker2 中的exchange1、queue1、exchange2 及queue2 都可以在
Shovel 成功连接源端或者目的端Broker 之后再第一次创建(执行一系列相应的AMQP 配置声明时),它们并不一定需要在Shovel link 建立之前创建。Shovel 可以为源端或者目的端配置多个Broker 的地址,这样可以使得源端或者目的端的Broker 失效后能够尝试重连到其他Broker之上(随机挑选) 。可以设置reconnect delay 参数以避免由于重连行为导致的网络泛洪,或者可以在重连失败后直接停止连接。针对源端和目的端的所有配置声明会在重连成功之后被重新发迭。

2.2.2、Shovel 的使用

        Shovel 插件默认也在RabbitMQ 的发布包中,执行rabbitmq-plugins enable rabbitmq_shovel 命令可以开启Shovel 功能, 示例如下:

[rabbit@myblnp1 ~]$ rabbitmq-plugins enable rabbitmq_shovel
Enabling plugins on node rabbit@myblnp1:
rabbitmq_shovel
The following plugins have been configured:
  rabbitmq_federation
  rabbitmq_federation_management
  rabbitmq_management
  rabbitmq_management_agent
  rabbitmq_shovel
  rabbitmq_web_dispatch
Applying plugin configuration to rabbit@myblnp1...
The following plugins have been enabled:
  rabbitmq_shovel

started 1 plugins.
[rabbit@myblnp1 ~]$ 

        由前面的讲解可知, Shovel 内部也是基于AMQP 协议转发数据的, 所以在开启rabbitmq_shovel 插件的时候, 默认也会开启amqp_client 插件。同时,如果要开启Shovel的管理插件, 需要执行rabbitmq-plugins enable rabbitmq_shovel_management 命令, 示例如下:

[rabbit@myblnp1 ~]$ rabbitmq-plugins enable rabbitmq_shovel_management
Enabling plugins on node rabbit@myblnp1:
rabbitmq_shovel_management
The following plugins have been configured:
  rabbitmq_federation
  rabbitmq_federation_management
  rabbitmq_management
  rabbitmq_management_agent
  rabbitmq_shovel
  rabbitmq_shovel_management
  rabbitmq_web_dispatch
Applying plugin configuration to rabbit@myblnp1...
The following plugins have been enabled:
  rabbitmq_shovel_management

started 1 plugins.
[rabbit@myblnp1 ~]$ 

        开启 rabbitmq_shovel_management 插件之后, 在RabbitMQ 的管理界面中" Admin "的右侧会多出"Shovel Status" 和" Shovel Management " 两个Tab 页,如上图所示。rabbitmq_shovel_management 插件依附于rabbitmq management 插件, 所以开启rabbitmq_shovel_management 插件的同时默认也会开启rabbitmq_management 插件。

        Shovel 既可以部署在源端,也可以部署在目的端。有两种方式可以部署Shovel: 静态方式(static)和动态方式(dynamic)。静态方式是指在rabbitmq.config(3.8.16版本是在:advanced.config.example) 配置文件中设置,而动态方式是指通过Runtime Parameter 设置。

静态方式:

        在rabbitmq.config 配置文件中针对Shove1 插件的配置信息是一种Erlang 项式,由单条Shove1 条目构成 (shovels 部分的下一层) :

{rabbitmq_shovel , [ {shovels , [ {shovel_name, [ . .. ]} , ... ]} ]}

        每一条Shove1 条目定义了源端与目的端的转发关系,其名称 (shove1 name ) 必须是独一无二的。每一条Shove1 的定义都像下面这样:

 {defaults, [{prefetch_count,     0},
             {ack_mode,           on_confirm},
             {publish_fields,     []},
             {publish_properties, [{delivery_mode, 2}]},
             …………
             {reconnect_delay,    2.5}]}

完整配置示例如下所示:

 {rabbitmq_shovel,
  [{shovels,
    [%% A named shovel worker.
     %% {my_first_shovel,
     %%  [

     %% List the source broker(s) from which to consume.
     %%
     %%   {sources,
     %%    [%% URI(s) and pre-declarations for all source broker(s).
     %%     {brokers, ["amqp://user:password@host.domain/my_vhost"]},
     %%     {declarations, []}
     %%    ]},

     %% List the destination broker(s) to publish to.
     %%   {destinations,
     %%    [%% A singular version of the 'brokers' element.
     %%     {broker, "amqp://"},
     %%     {declarations, []}
     %%    ]},

     %% Name of the queue to shovel messages from.
     %%
     %% {queue, <<"your-queue-name-goes-here">>},

     %% Optional prefetch count.
     %%
     %% {prefetch_count, 10},

     %% when to acknowledge messages:
     %% - no_ack: never (auto)
     %% - on_publish: after each message is republished
     %% - on_confirm: when the destination broker confirms receipt
     %%
     %% {ack_mode, on_confirm},

     %% Overwrite fields of the outbound basic.publish.
     %%
     %% {publish_fields, [{exchange,    <<"my_exchange">>},
     %%                   {routing_key, <<"from_shovel">>}]},

     %% Static list of basic.properties to set on re-publication.
     %%
     %% {publish_properties, [{delivery_mode, 2}]},

     %% The number of seconds to wait before attempting to
     %% reconnect in the event of a connection failure.
     %%
     %% {reconnect_delay, 2.5}

     %% ]} %% End of my_first_shovel
    ]}
   %% Rather than specifying some values per-shovel, you can specify
   %% them for all shovels here.
   %%
   %% {defaults, [{prefetch_count,     0},
   %%             {ack_mode,           on_confirm},
   %%             {publish_fields,     []},
   %%             {publish_properties, [{delivery_mode, 2}]},
   %%             {reconnect_delay,    2.5}]}
  ]},

        其中broker 项配置的是URI , 定义了用于连接Shovel 两端的服务器地址、用户名、密码、vhost 和端口号等。如果sources 或者destinations 是RabbitMQ 集群,那么就使用brokers ,并在其后用多个URI 字符串以" []"的形式包裹起来,比如 {brokers, ["amqp://root:root123@192.168.56.119:5672", "amqp://root:root123@192.168.56.120:5672" ]} ,这样的定义能够使得Shovel 在主节点故障时转移到另一个集群节点上。

        declarations 这一项是可选的, declaration list 指定了可以使用的AMQP 命令的列表,声明了队列、交换器和绑定关系。比如代码示例中sources 的declarations 这一项声明了队列queue1 ('queue.declare')、 交换器 exchange1 ('exchange. declare' )及其之间的绑定关系( 'queue.bind' )。注意其中所有的字符串井不是简单地用引号标注,而是同时用双尖括号包裹,比如<<"queue1">> 。这里的双尖括号是要让ErLang 程序不要将其视为简单的字符串,而是binary 类型的字符串。如果没有双尖括号包裹,那么Shovel 在启动的时候就会出错。与queue1 一起的还有一个durable 参数,它不需要像其他参数一样需要包裹在大括号内,这是因为像durable 这种类型的参数不需要赋值,它要么存在,要么不存在,只有在参数需要赋值的时候才需要加上大括号。

        与sources 和destinations 同级的queue 表示源端服务器上的队列名称。可以将queue 设置为"<<>>",表示匿名队列(队列名称由RabbitMQ 自动生成, 参考图8-18 中broker1的队列) 。

        prefetch_count 参数表示Shovel 内部缓存的消息条数,可以参考Federation 的相关参数。ShoveL 的内部缓存是源端服务器和目的端服务器之间的中间缓存部分。

ack_mode 表示在完成转发消息时的确认模式,和Federation 的ack mode 一样也有三种取值:

  • no ack 表示无须任何消息确认行为;
  • on publish 表示Shovel 会把每一条消息发送到目的端之后再向源端发送消息确认;
  • on confirm 表示Shovel 会使用publisher confmn 机制,在收到目的端的消息确认之后再向源端发送消息确认。

ShoveL 的ack mode 默认也是on confirm ,并且官方强烈建议使用该值。如果选择使用其他值,整体性能虽然会有略微提升,但是发生各种失效问题的情况时,消息的可靠性得不到保障。

        publish_properties 是指消息发往目的端时需要特别设置的属性列表。默认情况下,被转发的消息的各个属性是被保留的,但是如果在publish_properties 中对属性进行了设置则可以覆盖原先的属性值。publish properties 的属性列表包括content_type 、content_encoding 、headers 、delivery_mode 、priority 、correlation_id 、reply_to 、expiration 、message_id 、timestamp 、type 、user id 、app_id 和 cluster_id 。

        add_forward_headers 如果设置为true ,则会在转发的消息内添加x-shovelled 的header 属性

        publish_fields 定义了消息需要发往目的端服务器上的交换器以及标记在消息上的路由键。如果交换器和路由键没有定义,则Shovel 会从原始消息上复制这些被忽略的设置。

        reconnect_delay 指定在Shovel link 失效的情况下,重新建立连接前需要等待的时间,单位为秒。如果设置为0 ,则不会进行重连动作,即Shovel 会在首次连接失效时停止工作。reconnect_delay 默认为5 秒。

动态方式:

        与Federation upstream 类似, Shovel 动态部署方式的配置信息会被保存到RabbitMQ 的Mnesia 数据库中,包括权限信息、用户信息和队列信息等内容。每一个Shovel link 都由一个相应的Parameter 定义,这个Parameter 同样可以通过rabbitmqctl 工具、RabbitMQ Management 插件的HTTP API 接口或者rabbitmq_shovel_management 提供的Web 管理界面的方式设置。

##第一种方式:通过rabbitmqctl 工具的方式
[rabbit@myblnp2 ~]$ rabbitmqctl set_parameter shovel hidden_shovel \
> '{"src-uri":"amqp://root:root123@192.168.56.119:5672","src-queue":"queue1","dest-uri":"amqp://root:root123@192.168.56.121:5672","src-exchange-key":"rk1","prefetch-count":64,"reconnect-delay":5,"publish-properties":[],"add-forward-headers":true,"ack-mode":"on-confirm"}'
Setting runtime parameter "hidden_shovel" for component "shovel" to "{"src-uri":"amqp://root:root123@192.168.56.119:5672","src-queue":"queue1","dest-uri":"amqp://root:root123@192.168.56.121:5672","src-exchange-key":"rk1","prefetch-count":64,"reconnect-delay":5,"publish-properties":[],"add-forward-headers":true,"ack-mode":"on-confirm"}" in vhost "/" ...
[rabbit@myblnp2 ~]$ 


##第二种方式:通过HTTP API方式
[rabbit@myblnp2 ~]$ curl -i -u root:root123 -XPUT -d '{"value":{"src-uri":"amqp://root:root123@192.168.56.119:5672","src-queue":"queue1","dest-uri":"amqp://root:root123@192.168.56.121:5672","src-exchange-key":"rk1","prefetch-count":64,"reconnect-delay":5,"publish-properties":[],"add-forward-headers":true,"ack-mode":"on-confirm"}}' http://192.168.56.120:15672/api/parameters/shovel/%2F/hidden_shovel
HTTP/1.1 204 No Content
content-security-policy: script-src 'self' 'unsafe-eval' 'unsafe-inline'; object-src 'self'
date: Tue, 31 Oct 2023 09:38:02 GMT
server: Cowboy
vary: accept, accept-encoding, origin

[rabbit@myblnp2 ~]$ 

第三种是通过Web 管理界面中添加的方式,在"Admin" → "Shovel Management"  -->  "Add
a new shovel" 中创建:

        在创建了一个Shovel link 之后, 可以在Web 管理界面中"Admin " → "Shovel Status" 中查看到相应的信息, 也可以通过rabbitmqctl eval 'rabbit_shovel_status:status().' 命令直接查询Shovel 的状态信息, 该命令会调用rabbitmq shovel 插件模块中的status 方法,该方法将返回一个Erlang 列表, 其中每一个元素对应一个己配直好的Shovel。示例如下:

[rabbit@myblnp2 ~]$ rabbitmqctl eval 'rabbit_shovel_status:status().'
[{{<<"/">>,<<"hidden_shovel">>},
  dynamic,
  {running,[{src_uri,<<"amqp://192.168.56.119:5672">>},
            {src_protocol,<<"amqp091">>},
            {dest_protocol,<<"amqp091">>},
            {dest_uri,<<"amqp://192.168.56.121:5672">>},
            {src_queue,<<"queue1">>}]},
  {{2023,10,31},{17,27,21}}}]
[rabbit@myblnp2 ~]$ 

        列表中的每一个元素都以一个四元组的形式构成: {Name , Type , Status , Timestamp} 。具体含义如下:

  • Name 表示Shovel 的名称。
  • Type 表示类型,有2 种取值一-static 和dynamic 。
  • Status 表示目前Shovel 的状态。当Shovel 处于启动、连接和创建资源时状态为 starting; 当Shovel 正常运行时是 running;当Shovel 终止时是terminated 。
  • Timestamp 表示该Shovel 进入当前状态的时间戳,具体格式是{{YYYY,MM,DD } I {HH,MM,SS}}。

2.2.3、案例:消息堆积的治理

        消息堆积是在使用消息中间件过程中遇到的最正常不过的事情。消息堆积是一把双刃剑,适量的堆积可以有削峰、缓存之用,但是如果堆积过于严重,那么就可能影响到其他队列的使用,导致整体服务质量的下降。对于一台普通的服务器来说,在一个队列中堆积1 万至10 万条消息,丝毫不会影响什么。但是如果这个队列中堆积超过1 千万乃至一亿条消息时,可能会引起一些严重的问题,比如引起内存或者磁盘告警而造成所有Connection 阻塞。

        消息堆积严重时,可以选择清空队列,或者采用空消费程序丢弃掉部分消息。不过对于重要的数据而言,丢弃消息的方案并无用武之地。另一种方案是增加下游消费者的消费能力,这个思路可以通过后期优化代码逻辑或者增加消费者的实例数来实现。但是后期的代码优化在面临紧急情况时总归是"远水解不了近渴",并且有些业务场景也井非可以简单地通过增加消费实例而得以增强消费能力。

        在一筹莫展之时,不如试一下Shovel 。当某个队列中的消息堆积严重时,比如超过某个设定的阑值,就可以通过Shovel 将队列中的消息移交给另一个集群

如图8-21 所示,这里有如下几种情形。

情形1 :当检测到当前运行集群cluster1 中的队列queue1 中有严重消息堆积,比如通过 /api/queues/vhost/name 接口获取到队列的消息个数(messages) 超过2 千万或者消息占用大小(messages bytes) 超过10GB 时,就启用shovel1 将队列queue1中的消息转发至备份集群cluster2 中的队列queue2 。

情形2: 紧随情形1,当检测到队列queue1 中的消息个数低于 1 百万或者消息占用大小低于1GB 时就停止shovel1 ,然后让原本队列queue1 中的消费者慢慢处理剩余的堆积。

情形3: 当检测到队列queue1 中的消息个数低于10 万或者消息占用大小低于100 MB时,就开启shove12 将队列queue2 中暂存的消息返还给队列queue1 。

情形4: 紧随情形3 ,当检测到队列queue1 中的消息个数超过 1 百万或者消息占用大小高于1GB 时就将shove12 停掉。

图8-21 消息堆积的治理

2.3、小结

        上一章节一直在讲述的一种部署方式,也是最为通用的一种方式。集群将多个Broker 节点连接起来组成逻辑上独立的单个Broker。集群内部借助Erlang 进行消息传输,所以集群中的每个节点的Erlang cookie 务必要保持一致。同时,集群内部的网络必须是可靠的,RabbitMQ 和Erlang 的版本也必须一致。虚拟主机、交换器、用户、权限等都会自动备份到集群中的各个节点。队列可能部署单个节点或被镜像到多个节点中。连接到任意节点的客户端能够看到集群中所有的队列,即使该队列不在所连接的节点之上。通常使用集群的部署方式来提高可靠性和吞吐量,不过集群只能部署在局域网内。

        Federation ,可以翻译为"联邦"。 Federation 可以通过AMQP 协议(可配置SSL)让原本发送到某个Broker (或集群〉中的交换器(或队列)上的消息能够转发到另一个Broker (或集群)中的交换器(或队列)上,两方的交换器(或队列〉看起来是以一种"联邦"的形式在运作。当然必须要确保这些"联邦"的交换器或者队列都具备合适的用户和权限。

        联邦交换器(federated exchange) 通过单向点对点的连接(Federation link) 形式进行通信。默认情况下,消息只会由Federation 连接转发一次,可以允许有复杂的路由拓扑来提高转发次数。在Federation 连接上,消息可能不会被转发,如果消息到达了联邦交换器之后路由不到合适的队列,那么它也不会被再次转发到原来的地方(这里指上游交换器,即(upstream exchange) 。可以通过Federation 连接广域网中的各个RabbitMQ 服务器来生产和消费消息。联邦队列(federated queue) 也是通过单向点对点连接进行通信的,消息可以根据具体的配置消费者的状态在联邦队列中游离任意次数。

        通过Shovel 来连接各个RabbitMQ Broker ,概念上与Federation 的情形类似,不过Shovel工作在更低一层。鉴于Federation 从一个交换器中转发消息到另一个交换器(如果必要可以确认消息是否被转发), Shovel 只是简单地从某个Broker 上的队列中消费消息,然后转发消息到另一个Broker 上的交换器而已。Shovel 也可以在单独的一台服务器上去转发消息,比如将一个队列中的数据移动到另一个队列中。如果想获得比Federation 更多的控制,可以在广域网中使用Shovel 连接各个RabbitMQ Broker 来生产或消费消息。

Federation / Shovel集群
各个Broker 节点之间逻缉分离逻辑上是个Broker 节点
各个Broker 节点之间可以运行不同版本的Erlang 和RabbitMQ各个Broker 节点之间必须运行相同版本的Erlang 和RabbitMQ
各个Broker 节点之间可以在广域网中相连,当然必须要授予适当的用户和权限各个Broker 节点之间必须在可信赖的局域网中相连, 通过 Erlang 内部节点传输消息,但节点间需要有相同的Erlang cookie
各个Broker 节点之间能以任何拓扑逻辑部署,连接可以是单向的或者双向的所有Broker 节点都双向连续所有其他节点
从CAP 理论中选择可用性和分区耐受性,即AP从CAP 理论中选择一致性和可用性, CA
一个Broker 中的交换器可以是Federation 生成的或者是本地的集群中所有Broker 节点中的交换器都是一样的,要么全有要么全无
客户端所能看到它所连接的Broker 节点上的队列客户端连接到集群中的任何Broker 节点都可以看到所有的队列

3、流控 & 告警

3.1、存储机制

        不管是持久化的消息还是非持久化的消息都可以被写入到磁盘。持久化的消息在到达队列时就被写入到磁盘,并且如果可以,持久化的消息也会在内存中保存一份备份,这样可以提高一定的性能,当内存吃紧的时候会从内存中清除。非持久化的消息一般只保存在内存中,在内存吃紧的时候会被换入到磁盘中,以节省内存空间。这两种类型的消息的落盘处理都在RabbitMQ 的"持久层"中完成。

        持久层是一个逻辑上的概念,实际包含两个部分: 队列索引 (rabbit_queue_index) 和消息存储(rabbit_msg_store)。 rabbit_queue_index 负责维护队列中落盘消息的信息,包括消息的存储地点、是否己被交付给消费者、是否己被消费者ack 等。每个队列都有与之对应的一个rabbit_queue_index。rabbit_msg_store 以键值对的形式存储消息,它被所有队列共享,在每个节点中有且只有一个。从技术层面上来说, rabbit_msg_store 具体还可以分为 msg_store_persistent 和 msg_store_transient, msg_store_persistent 负责持久化消息的持久化,重启后消息不会丢失; msg_store_transient 负责非持久化消息的持久化,重启后消息会丢失。通常情况下,习惯性地将msg_store_persistent 和msg_store_transient 看成 rabbit_msg_store 这样一个整体。

        消息(包括消息体、属性和headers) 可以直接存储在 rabbit_queue_index 中,也可以被保存在rabbit_msg_store 中。默认在 $RABBITMQ_HOME/var/lib/mnesia/rabbit@$HOSTNAME/ 路径下包含queues 、msg_store_persistent 、msg_store_ transient 这3 个文件夹(下面信息中加粗的部分),其分别存储对应的信息。

[root@myblnp rabbit@myblnp]# find / -name "*rabbit*" | grep rabbit@myblnp
/var/lib/rabbitmq/mnesia/rabbit@myblnp
/var/lib/rabbitmq/mnesia/rabbit@myblnp/quorum/rabbit@myblnp
/var/lib/rabbitmq/mnesia/rabbit@myblnp/rabbit_user.DCD
/var/lib/rabbitmq/mnesia/rabbit@myblnp/rabbit_user_permission.DCD
/var/lib/rabbitmq/mnesia/rabbit@myblnp/rabbit_topic_permission.DCD
/var/lib/rabbitmq/mnesia/rabbit@myblnp/rabbit_vhost.DCD
/var/lib/rabbitmq/mnesia/rabbit@myblnp/rabbit_durable_route.DCD
/var/lib/rabbitmq/mnesia/rabbit@myblnp/rabbit_durable_exchange.DCD
/var/lib/rabbitmq/mnesia/rabbit@myblnp/rabbit_runtime_parameters.DCD
/var/lib/rabbitmq/mnesia/rabbit@myblnp/rabbit_durable_queue.DCD
/var/lib/rabbitmq/mnesia/rabbit@myblnp/rabbit_serial
/var/lib/rabbitmq/mnesia/rabbit@myblnp/rabbit_durable_queue.DCL
/var/lib/rabbitmq/mnesia/rabbit@myblnp-feature_flags
/var/lib/rabbitmq/mnesia/rabbit@myblnp.pid
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_federation-3.8.13
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_federation-3.8.13/ebin/rabbit_federation_app.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_federation-3.8.13/ebin/rabbit_federation_db.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_federation-3.8.13/ebin/rabbit_federation_event.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_federation-3.8.13/ebin/rabbit_federation_exchange.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_federation-3.8.13/ebin/rabbit_federation_exchange_link.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_federation-3.8.13/ebin/rabbit_federation_exchange_link_sup_sup.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_federation-3.8.13/ebin/rabbit_federation_link_sup.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_federation-3.8.13/ebin/rabbit_federation_link_util.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_federation-3.8.13/ebin/rabbit_federation_parameters.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_federation-3.8.13/ebin/rabbit_federation_queue.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_federation-3.8.13/ebin/rabbit_federation_queue_link.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_federation-3.8.13/ebin/rabbit_federation_queue_link_sup_sup.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_federation-3.8.13/ebin/rabbit_federation_status.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_federation-3.8.13/ebin/rabbit_federation_sup.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_federation-3.8.13/ebin/rabbit_federation_upstream.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_federation-3.8.13/ebin/rabbit_federation_upstream_exchange.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_federation-3.8.13/ebin/rabbit_federation_util.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_federation-3.8.13/ebin/rabbitmq_federation.app
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_federation-3.8.13/include/rabbit_federation.hrl
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_app.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_cors.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_csp.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_db.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_db_cache.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_db_cache_sup.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_dispatcher.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_extension.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_headers.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_hsts.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_load_definitions.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_reset_handler.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_stats.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_sup.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_sup_sup.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_util.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_aliveness_test.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_auth.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_auth_attempts.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_binding.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_bindings.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_channel.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_channels.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_channels_vhost.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_cluster_name.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_connection.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_connection_channels.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_connections.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_connections_vhost.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_consumers.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_definitions.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_exchange.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_exchange_publish.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_exchanges.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_extensions.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_feature_flag_enable.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_feature_flags.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_global_parameter.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_global_parameters.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_health_check_alarms.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_health_check_certificate_expiration.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_health_check_local_alarms.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_health_check_node_is_mirror_sync_critical.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_health_check_node_is_quorum_critical.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_health_check_port_listener.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_health_check_protocol_listener.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_health_check_virtual_hosts.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_healthchecks.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_limit.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_limits.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_login.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_node.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_node_memory.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_node_memory_ets.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_nodes.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_operator_policies.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_operator_policy.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_overview.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_parameter.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_parameters.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_permission.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_permissions.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_permissions_user.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_permissions_vhost.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_policies.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_policy.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_queue.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_queue_actions.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_queue_get.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_queue_purge.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_queues.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_rebalance_queues.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_redirect.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_reset.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_static.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_topic_permission.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_topic_permissions.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_topic_permissions_user.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_topic_permissions_vhost.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_user.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_user_limit.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_user_limits.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_users.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_users_bulk_delete.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_vhost.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_vhost_restart.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_vhosts.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbit_mgmt_wm_whoami.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/ebin/rabbitmq_management.app
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/include/rabbit_mgmt.hrl
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/priv/schema/rabbitmq_management.schema
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/priv/www/cli/rabbitmqadmin
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/priv/www/img/rabbitmqlogo-master-copy.svg
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management-3.8.13/priv/www/img/rabbitmqlogo.svg
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/amqp_client-3.8.13/ebin/rabbit_routing_util.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/amqp_client-3.8.13/include/rabbit_routing_prefixes.hrl
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_web_dispatch-3.8.13
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_web_dispatch-3.8.13/ebin/rabbit_cowboy_middleware.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_web_dispatch-3.8.13/ebin/rabbit_cowboy_redirect.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_web_dispatch-3.8.13/ebin/rabbit_cowboy_stream_h.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_web_dispatch-3.8.13/ebin/rabbit_web_dispatch.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_web_dispatch-3.8.13/ebin/rabbit_web_dispatch_app.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_web_dispatch-3.8.13/ebin/rabbit_web_dispatch_listing_handler.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_web_dispatch-3.8.13/ebin/rabbit_web_dispatch_registry.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_web_dispatch-3.8.13/ebin/rabbit_web_dispatch_sup.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_web_dispatch-3.8.13/ebin/rabbit_web_dispatch_util.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_web_dispatch-3.8.13/ebin/rabbitmq_web_dispatch.app
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management_agent-3.8.13
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management_agent-3.8.13/ebin/rabbit_mgmt_agent_app.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management_agent-3.8.13/ebin/rabbit_mgmt_agent_config.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management_agent-3.8.13/ebin/rabbit_mgmt_agent_sup.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management_agent-3.8.13/ebin/rabbit_mgmt_agent_sup_sup.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management_agent-3.8.13/ebin/rabbit_mgmt_data.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management_agent-3.8.13/ebin/rabbit_mgmt_data_compat.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management_agent-3.8.13/ebin/rabbit_mgmt_db_handler.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management_agent-3.8.13/ebin/rabbit_mgmt_external_stats.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management_agent-3.8.13/ebin/rabbit_mgmt_ff.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management_agent-3.8.13/ebin/rabbit_mgmt_format.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management_agent-3.8.13/ebin/rabbit_mgmt_gc.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management_agent-3.8.13/ebin/rabbit_mgmt_metrics_collector.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management_agent-3.8.13/ebin/rabbit_mgmt_metrics_gc.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management_agent-3.8.13/ebin/rabbit_mgmt_storage.beam
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management_agent-3.8.13/ebin/rabbitmq_management_agent.app
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management_agent-3.8.13/include/rabbit_mgmt_metrics.hrl
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management_agent-3.8.13/include/rabbit_mgmt_records.hrl
/var/lib/rabbitmq/mnesia/rabbit@myblnp-plugins-expand/rabbitmq_management_agent-3.8.13/priv/schema/rabbitmq_management_agent.schema
/var/log/rabbitmq/rabbit@myblnp.log-20221109.gz
/var/log/rabbitmq/rabbit@myblnp_upgrade.log-20221109.gz
/var/log/rabbitmq/rabbit@myblnp.log-20230317.gz
/var/log/rabbitmq/rabbit@myblnp_upgrade.log-20230317.gz
/var/log/rabbitmq/rabbit@myblnp.log-20230831.gz
/var/log/rabbitmq/rabbit@myblnp_upgrade.log-20230831.gz
/var/log/rabbitmq/rabbit@myblnp.log-20230908.gz
/var/log/rabbitmq/rabbit@myblnp_upgrade.log-20230908.gz
/var/log/rabbitmq/rabbit@myblnp.log-20230915.gz
/var/log/rabbitmq/rabbit@myblnp_upgrade.log-20230915.gz
/var/log/rabbitmq/rabbit@myblnp.log-20230917.gz
/var/log/rabbitmq/rabbit@myblnp.log-20231017.gz
/var/log/rabbitmq/rabbit@myblnp_upgrade.log-20231017.gz
/var/log/rabbitmq/rabbit@myblnp.log
/var/log/rabbitmq/rabbit@myblnp.log-20231026.gz
/var/log/rabbitmq/rabbit@myblnp_upgrade.log
/var/log/rabbitmq/rabbit@myblnp_upgrade.log-20231026.gz
[root@myblnp rabbit@myblnp]# 
[root@myblnp rabbit@myblnp]# pwd
/var/lib/rabbitmq/mnesia/rabbit@myblnp
[root@myblnp rabbit@myblnp]# 
[root@myblnp rabbit@myblnp]# 
[root@myblnp rabbit@myblnp]# ll
总用量 92
-rw-r-----. 1 rabbitmq rabbitmq    35 11月  1 09:38 cluster_nodes.config
-rw-r-----. 1 rabbitmq rabbitmq   158 11月  1 09:41 DECISION_TAB.LOG
-rw-r-----. 1 rabbitmq rabbitmq    94 11月  1 09:41 LATEST.LOG
drwxr-x---. 3 rabbitmq rabbitmq    20 11月  1 2022 msg_stores
-rw-r-----. 1 rabbitmq rabbitmq    17 11月  1 09:38 nodes_running_at_shutdown
drwxr-x---. 3 rabbitmq rabbitmq    27 11月  1 2022 quorum
-rw-r-----. 1 rabbitmq rabbitmq  3623 10月 20 18:14 rabbit_durable_exchange.DCD
-rw-r-----. 1 rabbitmq rabbitmq  1664 11月  1 09:38 rabbit_durable_queue.DCD
-rw-r-----. 1 rabbitmq rabbitmq  2107 11月  1 09:41 rabbit_durable_queue.DCL
-rw-r-----. 1 rabbitmq rabbitmq   802 9月  12 16:57 rabbit_durable_route.DCD
-rw-r-----. 1 rabbitmq rabbitmq   448 10月 17 10:59 rabbit_runtime_parameters.DCD
-rw-r-----. 1 rabbitmq rabbitmq     4 11月  1 09:38 rabbit_serial
-rw-r-----. 1 rabbitmq rabbitmq     8 11月  1 2022 rabbit_topic_permission.DCD
-rw-r-----. 1 rabbitmq rabbitmq   505 10月 20 18:14 rabbit_user.DCD
-rw-r-----. 1 rabbitmq rabbitmq   384 9月  12 10:25 rabbit_user_permission.DCD
-rw-r-----. 1 rabbitmq rabbitmq   233 9月  11 17:17 rabbit_vhost.DCD
-rw-r-----. 1 rabbitmq rabbitmq 30935 11月  1 2022 schema.DAT
-rw-r-----. 1 rabbitmq rabbitmq   342 11月  1 2022 schema_version
[root@myblnp rabbit@myblnp]#

这里如果不知道或者没找到存储目录的话,可以通过上述方式来查找对应的目录位置,因为安装方式或者配置的问题。该数据目录的所在位置都会有所不同。

        最佳的配备是较小的消息存储在 rabbit_queue_index 中而较大的消息存储在 rabbit_msg_store 中。这个消息大小的界定可以通过 queue_index_embed_msgs_below 来配置, 默认大小为4096 ,单位为B 。注意这里的消息大小是指消息体、属性及headers 整体的大小。当一个消息小于设定的大小闹值时就可以存储在 rabbit_queue_index 中,这样可以得到性能上的优化。

        rabbit_queue_index 中以顺序 (文件名从0 开始累加) 的段文件来进行存储,后缀为
" . idx " ,每个段文件中包含固定的 SEGMENT_ENTRY_COUNT 条记录,SEGMENT_ENTRY_COUNT 默认值为16384 。每个 rabbit_queue_index 从磁盘中读取消息的时候至少要在内存中维护一个段文件,所以设置 queue_index_embed_msgs_below 值的时候要格外谨慎, 一点点增大也可能会引起内存爆炸式的增长

        经过 rabbit_msg_store 处理的所有消息都会以追加的方式写入到文件中,当一个文件的大小超过指定的限制 (file_size_limit) 后, 关闭这个文件再创建一个新的文件以供新的消息写入。文件名(文件后缀是". rdq") 从0 开始进行累加, 因此文件名最小的文件也是最老的文件。在进行消息的存储时, RabbitMQ 会在 ETS (Erlang Term Storage) 表中记录消息在文件中的位置映射 (Index)和文件的相关信息 (FileSummary)。

        在读取消息的时候,先根据消息的ID (msg_id) 找到对应存储的文件,如果文件存在并且未被锁住,则直接打开文件,从指定位置读取消息的内容。如果文件不存在或者被锁住了,则发送请求由rabbit_msg_store 进行处理。

        消息的删除只是从ETS 表删除指定消息的相关信息, 同时更新消息对应的存储文件的相关信息。执行消息删除操作时,井不立即对在文件中的消息进行删除,也就是说消息依然在文件中,仅仅是标记为垃圾数据而己。当一个文件中都是垃圾数据时可以将这个文件删除。当检测到前后两个文件中的有效数据可以合并在一个文件中,井且所有的垃圾数据的大小和所有文件(至少有3 个文件存在的情况下〉的数据大小的比值超过设置的阑值 GARBAGE_FRACTION (默认值为0.5) 时才会触发垃圾回收将两个文件合井。

        执行合并的两个文件一定是逻辑上相邻的两个文件。如图9-1 所示,执行合并时首先锁定这两个文件,井先对前面文件中的有效数据进行整理,再将后面文件的有效数据写入到前面的文件,同时更新消息在ETS 表中的记录,最后删除后面的文件。

图9-1 垃圾回收

3.1.1、队列的结构

        通常队列由 rabbit_amqqueue_process 和 backing_queue 这两部分组成,rabbit_amqqueue_process 负责协议相关的消息处理,即接收生产者发布的消息、向消费者交付消息、处理消息的确认(包括生产端的confirm 和消费端的ack) 等。backing_queue是消息存储的具体形式和引擎,并向 rabbit_amqqueue_process 提供相关的接口以供调用。

        如果消息投递的目的队列是空的,并且有消费者订阅了这个队列,那么该消息会直接发送给消费者,不会经过队列这一步。而当消息无法直接投递给消费者时,需要暂时将消息存入队列,以便重新投递。消息存入队列后,不是固定不变的,它会随着系统的负载在队列中不断地流动,消息的状态会不断发生变化。RabbitMQ 中的队列消息可能会处于以下4 种状态。

  • alpha: 消息内容(包括消息体、属性和headers) 和消息索引都存储在内存中。
  • beta: 消息内容保存在磁盘中,消息索引保存在内存中。
  • gamma: 消息内容保存在磁盘中,消息索引在磁盘和内存中都有。
  • delta: 消息内容和索引都在磁盘中。

        对于持久化的消息,消息内容和消息索引都必须先保存在磁盘上,才会处于上述状态中的一种。而gamma 状态的消息是只有持久化的消息才会有的状态。

        RabbitMQ 在运行时会根据统计的消息传送速度定期计算一个当前内存中能够保存的最大消息数量(target_ram_count) ,如果alpha 状态的消息数量大于此值时,就会引起消息的状态转换,多余的消息可能会转换到beta 状态、gamma 状态或者delta 状态。区分这4 种状态的主要作用是满足不同的内存和CPU 需求。alpha 状态最耗内存,但很少消耗CPU。delta状态基本不消耗内存,但是需要消耗更多的CPU 和磁盘 I/O 操作。delta 状态需要执行两次 I/O 操作才能读取到消息, 一次是读消息索引(从rabbit_queue_index 中), 一次是读消息内容(从rabbit_msg_store 中); beta 和gamma 状态都只需要一次 I/O 操作就可以读取到消息(从rabbit_msg_store 中)。

        对于普通的没有设置优先级和镜像的队列来说, backing_queue 的默认实现是rabbit_variable_queue ,其内部通过5 个子队列Q1、Q2 , Delta、Q3 和Q4 来体现消息的各个状态。整个队列包括 rabbit_amqqueue_process 和 backing_queue 的各个子队列,队列的结构可以参考图9-2 。其中Q1 、Q4 只包含alpha 状态的消息, Q2 和Q3 包含beta 和gamma 状态的消息, Delta 只包含delta 状态的消息。一般情况下,消息按照Q1 → Q2→ Delta→ Q3 → Q4 这样的顺序步骤进行流动,但并不是每一条消息都一定会经历所有的状态,这个取决于当前系统的负载状况。从Q1 至Q4 基本经历内存到磁盘,再由磁盘到内存这样的一个过程,如此可以在队列负载很高的情况下,能够通过将一部分消息由磁盘保存来节省内存空间,而在负载降低的时候,这部分消息又渐渐回到内存被消费者获取,使得整个队列具有很好的弹性。

图9-2 队列结构

        消费者获取消息也会引起消息的状态转换。当消费者获取消息时,首先会从Q4 中获取消息,如果获取成功则返回。如果Q4 为空,则尝试从Q3 中获取消息, 系统首先会判断Q3 是否为空,如果为空则返回队列为空,即此时队列中无消息。如果Q3 不为空,则取出Q3 中的消息,进而再判断此时Q3 和Delta 中的长度,如果都为空,则可以认为Q2 , Delta 、Q3 , Q4 全部为空,此时将Q1 中的消息直接转移至Q4 ,下次直接从Q4 中获取消息。如果Q3 为空, Delta 不为空,则将Delta 的消息转移至Q3 中,下次可以直接从Q3 中获取消息。在将消息从Delta 转移到Q3 的过程中, 是按照索引分段读取的,首先读取某一段,然后判断读取的消息的个数与Delta 中消息的个数是否相等,如果相等,则可以判定此时Delta 中己无消息,则直接将Q2 和刚读取到的消息一并放入到Q3 中:如果不相等,仅将此次读取到的消息转移到Q3。

        这里就有两处疑问,第一个疑问是:为什么Q3 为空则可以认定整个队列为空?试想一下,如果Q3 为空, Delta 不为空,那么在Q3 取出最后一条消息的时候, Delta 上的消息就会被转移到Q3 , 这样与Q3 为空矛盾:如果Delta 为空且Q2 不为空,则在Q3 取出最后一条消息时会将Q2 的消息并入到Q3 中,这样也与Q3 为空矛盾: 在Q3 取出最后一条消息之后,如果Q2 、Delta 、Q3 都为空,且Q1 不为空时,则Q1 的消息会被转移到Q4 , 这与Q4 为空矛盾。其实这一番论述也解释了另一个问题: 为什么Q3 和Delta 都为空时,则可以认为Q2 , Delta 、Q3 , Q4 全部为空。

        通常在负载正常时,如果消息被消费的速度不小于接收新消息的速度, 对于不需要保证可靠不丢失的消息来说,极有可能只会处于alpha 状态。对于durable 属性设置为true 的消息,它一定会进入gamma 状态,并且在开启publisher confmn 机制时, 只有到了gamma 状态时才会确认该消息己被接收,若消息消费速度足够快、内存也充足, 这些消息也不会继续走到下一个状态。

        在系统负载较高时,己接收到的消息若不能很快被消费掉,这些消息就会进入到很深的队列中去,这样会增加处理每个消息的平均开销。因为要花更多的时间和资源处理"堆积"的消息,如此用来处理新流入的消息的能力就会降低,使得后流入的消息又被积压到很深的队列中继续增大处理每个消息的平均开销,继而情况变得越来越恶化,使得系统的处理能力大大降低。应对这一问题一般有3 种措施:

  1. 增加prefetch_count 的值,即一次发送多条消息给消费者, 加快消息被消费的速度
  2. 采用multiple ack ,降低处理ack 带来的开销
  3. 流量控制

3.1.2、惰性队列

        RabbitMQ 从3 . 6 .0 版本开始引入了惰性队列( Lazy Queue) 的概念。惰性队列会尽可能地将消息存入磁盘中,而在消费者消费到相应的消息时才会被加载到内存中,它的一个重要的设计目标是能够支持更长的队列, 即支持更多的消息存储。当消费者由于各种各样的原因(比如消费者下线、岩机, 或者由于维护而关闭等〉致使长时间内不能消费消息而造成堆积时, 惰性队列就很有必要了。

        默认情况下,当生产者将消息发送到RabbitMQ 的时候, 队列中的消息会尽可能地存储在内存之中,这样可以更加快速地将消息发送给消费者。即使是持久化的消息,在被写入磁盘的同时也会在内存中驻留一份备份。当RabbitMQ 需要释放内存的时候,会将内存中的消息换页至磁盘中,这个操作会耗费较长的时间,也会阻塞队列的操作,进而无法接收新的消息。虽然RabbitMQ 的开发者们一直在升级相关的算法,但是效果始终不太理想,尤其是在消息量特别大的时候。

        惰性队列会将接收到的消息直接存入文件系统中,而不管是持久化的或者是非持久化的,这样可以减少了内存的消耗,但是会增加 I/O 的使用,如果消息是持久化的,那么这样的 I/O 操作不可避免,惰性队列和持久化的消息可谓是"最佳拍档"。注意如果惰性队列中存储的是非持久化的消息,内存的使用率会一直很稳定,但是重启之后消息一样会丢失

        队列具备两种模式: default 和lazy 。默认的为default 模式,在3.6.0 之前的版本无须做任何变更。lazy 模式即为惰性队列的模式,可以通过调用 channel.queueDeclare 方法的时候在参数中设置,也可以通过Policy 的方式设置,如果一个队列同时使用这两种方式设置,那么Policy 的方式具备更高的优先级。如果要通过声明的方式改变己有队列的模式,那么只能先删除队列,然后再重新声明一个新的。

        在队列声明的时候可以通过 x-queue-mode 参数来设置队列的模式,取值为default 和 lazy。下面示例演示了一个惰性队列的声明细节:

Map<String, Object> args = new HashMap<String, Object>();
args.put("x-queue-mode", "lazy");
channel.queueDeclare("myqueue", false, false, false, args);

对应的Policy的设置方式:

[rabit@myblnp ~]$ sudo rabbitmqctl set_policy lazy "^myqueue$" '{"queue-mode":"lazy"}' --apply-to queues
[sudo] rabit 的密码:
Setting policy "lazy" for pattern "^myqueue$" to "{"queue-mode":"lazy"}" with priority "0" for vhost "/" ...
[rabit@myblnp ~]$ 

对应的WEB设置方式:

        惰性队列和普通队列相比,只有很小的内存开销。这里很难对每种情况给出一个具体的数
值,但是我们可以类比一下:发送 1 千万条消息,每条消息的大小为 1 KB,并且此时没有任何的消费者,那么普通队列会消耗1.2GB 的内存,而惰性队列只消耗1.5MB 的内存。

        据官方测试数据显示,对于普通队列,如果要发送 1 千万条消息,需要耗费801 秒,平均发送速度约为13000 条/秒。如果使用惰性队列,那么发送同样多的消息时,耗时是421 秒,平均发送速度约为24000 条/秒。出现性能偏差的原因是普通队列会由于内存不足而不得不将消息换页至磁盘。如果有消费者消费时,惰性队列会耗费将近40 MB 的空间来发送消息,对于一个消费者的情况,平均的消费速度约为14000 条/秒。

        如果要将普通队列转变为惰性队列,那么我们需要忍受同样的性能损耗, 首先需要将缓存中的消息换页至磁盘中,然后才能接收新的消息。反之,当将一个惰性队列转变为普通队列的时候,和恢复一个队列执行同样的操作,会将磁盘中的消息批量地导入到内存中。

3.2、内存与磁盘告警

        当内存使用超过配置的阑值或者磁盘剩余空间低于配置的阑值时, RabbitMQ 都会暂时阻塞(block) 客户端的连接(Connection) 并停止接收从客户端发来的消息,以此避免服务崩溃。与此同时,客户端与服务端的心跳检测也会失效。可以通过 rabbitmqctl list_connections 命令或者Web 管理界面来查看它的状态,如图9-3 所示。

图9-3 Connection 状态
root@lidechang-test:/# rabbitmqctl list_connections
Listing connections ...
user    peer_host       peer_port       state
alarm   10.55.21.141    13624   running
alarm   10.55.21.149    55530   running
alarm   10.55.21.149    55524   running
property        10.2.2.254      51186   running
property        10.2.2.254      51190   running
root@lidechang-test:/#

        被阻塞的Connection 的状态要么是blocking ,要么是blocked。前者对应于并不试图发送消息的Connection ,比如消费者关联的Connection ,这种状态下的Connection 可以继续运行。而后者对应于一直有消息发送的Connection ,这种状态下的Connection 会被停止发送消息。注意在一个集群中,如果一个Broker 节点的内存或者磁盘受限,都会引起整个集群中所有的Connection 被阻塞

        理想的情况是当发生阻塞时可以在阻止生产者的同时而又不影响消费者的运行。但是在AMQP 协议中, 一个信道(Channel) 上可以同时承载生产者和消费者, 同一个Connection 中也可以同时承载若干个生产者的信道和消费者的信道,这样就会使阻塞逻辑错乱, 虽然大多数情况下并不会发生任何问题,但还是建议生产和消费的逻辑可以分摊到独立的Connection 之上而不发生任何交集。客户端程序可以通过添加 BlockedListener 来监昕相应连接的阻塞信息

3.2.1、内存告警

        RabbitMQ 服务器会在启动或者执行 rabbitmqctl set_vm_memory_high_watermark fractio口命令时计算系统内存的大小。默认情况下 vm_memory_high_watermark 的值为0.4,即内存阑值为0.4, 表示当RabbitMQ 使用的内存超过40%时,就会产生内存告警并阻塞所有生产者的连接。一旦告警被解除(有消息被消费或者从内存转储到磁盘等情况的发生), 一切都会恢复正常。

        默认情况下将RabbitMQ 所使用内存的阔值设置为40% , 这并不意味着此时RabbitMQ 不能使用超过40% 的内存,这仅仅只是限制了RabbitMQ 的消息生产者。在最坏的情况下, Erlang的垃圾回收机制会导致两倍的内存消耗,也就是80%的使用占比。

        内存阔值可以通过rabbitmq.config 配置文件来配置,下面示例中设置了默认的内存闽值为0 .4:

## Resource Limits & Flow Control
## ==============================
##
## Related doc guide: https://rabbitmq.com/memory.html.

## Memory-based Flow Control threshold.
##
vm_memory_high_watermark.relative = 0.4

与此配置对应的 rabbitmqctl 系列的命令为:

##查看当前服务的内存阈值
[root@myblnp ~]# rabbitmqctl environment | grep vm_memory
      {vm_memory_calculation_strategy,rss},
      {vm_memory_high_watermark,0.4},
      {vm_memory_high_watermark_paging_ratio,0.5},
[root@myblnp ~]# 
[root@myblnp ~]# 

##设置更新内存阈值
[root@myblnp ~]# rabbitmqctl set vm_memory_high_watermark 0.5
[root@myblnp ~]# 
[root@myblnp ~]# 

##vm_memory_high_watermark正常取值范围在0.4到0.66,不建议取值超过0.7.也可以设置绝对值内存,如:

##{vm_memory_high_watermark,{absolute,”1024MiB”}},命令如下:

##rabbitmqctl set vm_memory_high_watermark {absolute,”1024MiB”}

        fraction 对应上面配置中的0 .4,表示占用内存的百分比,取值为大于等于0 的浮点数。设置对应的百分比值之后, RabbitMQ 中会打印服务日志。当在内存为7872MB 的节点中设置内存阑值为0 .4时, 会有如下信息:

=INFO REPORT==== 4-Sep- 2017: : 20 : 30 :09 ===
Memory 1imit set to 3148MB of 7872MB total.

        此时又将fraction 设置为0. 1,同时发出了内存告警,相应的服务日志会打印如下信息:

=INFO REPORT==== 4- Sep- 2017: : 20 :29:55 ===
Memory limit set to 787MB of 7872MB total .

=INFO REPORT==== 4-Sep-2017 : : 20 : 29 : 55 ===
vm_memory_high_watermark set . Memory used:1163673112 allowed : 825482444

=WARNING REPORT==== 4-Sep-2017 : :20 : 29 : 55 ===
memory resource 1imit a1arm set on node rabbit@node1.

**********************************************************
*** Pub1ishers wi11 be blocked until this alarm clears ***
**********************************************************

之后又设置fraction 为0.4以消除内存告警,相应的服务日志会打印如下信息:

=INFO REPORT==== 4-Sep-2017 : : 20 : 30 : 01 ===
vm memory high watermark clear. Memory used:693482232 allowed : 825482444

=WARNING REPORT==== 4-Sep-2017 : :20 : 30 : 01 ===
memory resource 1imit alarm cleared on node rabbit@node1

=WARNING REPORT==== 4-Sep-2017 : : 20 : 30 : 01 ===
memory resource limit alarm cleared across the cluster

        如果设置fraction 为0 ,所有的生产者都会被停止发送消息。这个功能可以适用于需要禁止集群中所有消息发布的情况。正常情况下建议 vm_memory_high_watermark 取值在0.4到0.66 之间, 不建议取值超过0.7 。

        假设机器的内存为4GB , 那么就代表RabbitMQ 服务的内存阔值的绝对值为 4GB x 0 .4= 1.6GB 。如果是32 位的Windows 操作系统,那么可用内存被限制为2GB ,也就意味着RabbitMQ 服务的内存阑值的绝对值为820MB 左右。除了通过百分比的形式, RabbitMQ 也可以采用绝对值的形式来设置内存阑值, 默认单位为B 。下面示例设置了内存阔值的绝对值为 1024MB ( 1024x 1024x 1024B= 1073741824B ) :

## Alternatively, we can set a limit (in bytes) of RAM used by the node.
##
# vm_memory_high_watermark.absolute = 1073741824

## Or you can set absolute value using memory units (with RabbitMQ 3.6.0+).
## Absolute watermark will be ignored if relative is defined!
##
# vm_memory_high_watermark.absolute = 2GB

        不管是这个命令还是 rabbitmqctl_set_vm_memory_high_watermark {fraction}命令,在服务器重启之后所设置的阐值都会失效,而通过配置文件的方式设置的阔值则不会在重启之后失效,但是修改后的配置需要在重启之后才能生效。

        在某个Broker 节点触及内存并阻塞生产者之前,它会尝试将队列中的消息换页到磁盘以释放内存空间。持久化和非持久化的消息都会被转储到磁盘中,其中持久化的消息本身就在磁盘中有一份副本,这里会将持久化的消息从内存中清除掉。

        默认情况下,在内存到达内存阐值的50%时会进行换页动作。也就是说,在默认的内存阑值为0 .4 的情况下,当内存超过0 .4x 0 . 5 =0.2 时会进行换页动作。可以通过在配置文件中配置vm_memory_high_watermark_paging_ratio 项来修改此值。下面示例中将换页比率从默认的0 . 5 修改为0.75:

## Fraction of the high watermark limit at which queues start to
## page message out to disc in order to free up memory.
## For example, when vm_memory_high_watermark is set to 0.4 and this value is set to 0.5,
## paging can begin as early as when 20% of total available RAM is used by the node.
##
## Values greater than 1.0 can be dangerous and should be used carefully.
##
## One alternative to this is to use durable queues and publish messages
## as persistent (delivery mode = 2). With this combination queues will
## move messages to disk much more rapidly.
##
## Another alternative is to configure queues to page all messages (both
## persistent and transient) to disk as quickly
## as possible, see https://rabbitmq.com/lazy-queues.html.
##
vm_memory_high_watermark_paging_ratio = 0.75

        上面的配置会在RabbitMQ 内存使用率达到 30%时进行换页动作,并在40%时阻塞生产者。可以将vm_memory_high_watermark_paging_ratio 值设置为大于1 的浮点数,这种配置相当于禁用了换页功能。注意这里RabbitMQ 中井没有类似 rabbitmqctl vm_memory_high_watermark_paging_ratio {xxx} 的命令。

        如果RabbitMQ 无法识别所在的操作系统,那么在启动的时候会在日志文件中追加一些信息,并将内存的值假定为1GB 。相应的日志信息参考如下:

=WARNING REPORT==== 5-Sep-2017 : :17:23 : 44 ===
Unknown total memory size for your OS {unix, magic_homebrew_os) . Assuming memory
size is 1024MB.

        对应 vm_memory_high_watermark 为0 .4的情形来说, RabbitMQ 的内存阑值就约为410MB 。如果操作系统本身的内存大小为8GB ,可以将 vm_memory_high_watermark 设置为3 ,这样内存阁值就提高到了3GB 。

3.2.2、磁盘告警

        当剩余磁盘空间低于确定的闽值时, RabbitMQ 同样会阻塞生产者,这样可以避免因非持久化的消息持续换页而耗尽磁盘空间导致服务崩溃。默认情况下,磁盘阑值为50MB,这意味着当磁盘剩余空间低于5 0MB 时会阻塞生产者井停止内存中消息的换页动作。这个阑值的设置可以减小但不能完全消除因磁盘耗尽而导致崩渍的可能性,比如在两次磁盘空间检测期间内,磁盘空间从大于50MB 被耗尽到OMB 。一个相对谨慎的做法是将磁盘阔值设置为与操作系统所显示的内存大小一致。

在Broker 节点启动的时候会默认开启磁盘检测的进程,相对应的服务日志为:

=INFO REPORT==== 7-Sep-2017::20 : 03 : 00 ===
Disk free 1imit set to 50MB

对于不识别的操作系统而言,磁盘检测功能会失效,对应的服务日志为:

=WARNING REPORT==== 7-Sep-2017: :1 5 :45: 29 ===
Disabling disk free space monitoring

        RabbitMQ 会定期检测磁盘剩余空间,检测的频率与上一次执行检测到的磁盘剩余空间大小有关。正常情况下,每10 秒执行一次检测,随着磁盘剩余空间与磁盘阑值的接近,检测频率会有所增加。当要到达磁盘阑值时,检测频率为每秒10 次,这样有可能会增加系统的负载。

        可以通过在配置文件中配直 disk_free_limit 项来设置磁盘阈值。下面示例中将磁盘阔值设置为1GB 左右:

## Set disk free limit (in bytes). Once free disk space reaches this
## lower bound, a disk alarm will be set - see the documentation
## listed above for more details.
##
## Absolute watermark will be ignored if relative is defined!
# disk_free_limit.absolute = 50000

## Or you can set it using memory units (same as in vm_memory_high_watermark)
## with RabbitMQ 3.6.0+.
# disk_free_limit.absolute = 500KB
# disk_free_limit.absolute = 50mb
# disk_free_limit.absolute = 5GB

## Alternatively, we can set a limit relative to total available RAM.
##
## Values lower than 1.0 can be dangerous and should be used carefully.
# disk_free_limit.relative = 2.0

        这里也可以使用单位设置,单位的选择可以参照内存阑值的设置(KB, KiB ,MB, MiB, GB , GiB)。示例可参考上述示例。

        与绝对值和相对值这两种配置对应的 rabbitmqctl 系列的命令为: rabbitmqctl set_disk_free_limit {disk limit}  和 rabbitmqctl set_disk_free_limit_mem_relative {fraction} ,和内存阔值的设置命令一样, Broker 重启之后将会失效。同样,通过配置文件的方式设置的阑值则不会在重启之后失效,但是修改后的配置需要在重启之后才能生效。正常情况下,建议 disk_free_limit_mem_relative 的取值为1.0 和2.0 之间。

3.3、流控

        RabbitMQ 可以对内存和磁盘使用量设置阈值,当达到阈值后,生产者将被阻塞 (block) ,直到对应项恢复正常。除了这两个阈值,从2.8.0 版本开始, RabbitMQ 还引入了流控 (Flow Control)机制来确保稳定性。流控机制是用来避免消息的发送速率过快而导致服务器难以支撑的情形。内存和磁盘告警相当于全局的流控 (Global Flow Control) , 一旦触发会阻塞集群中所有的Connection ,而本节的流控是针对单个Connection 的,可以称之为Per-Connection Flow Control或者Intemal Flow Control。

3.3.1、流控的原理

        Erlang 进程之间并不共享内存 (binary类型的除外),而是通过消息传递来通信,每个进程都有自己的进程邮箱 (mailbox) 。默认情况下, Erlang 并没有对进程邮箱的大小进行限制,所以当有大量消息持续发往某个进程时,会导致该进程邮箱过大,最终内存溢出并崩溃。在RabbitMQ 中,如果生产者持续高速发送,而消费者消费速度较低时,如果没有流控,很快就会使内部进程邮箱的大小达到内存阀值。

        RabbitMQ 使用了一种基于信用证算法(credit-based algorithm) 的流控机制来限制发送消息的速率以解决前面所提出的问题。它通过监控各个进程的进程邮箱,当某个进程负载过高而来不及处理消息时,这个进程的进程邮箱就会开始堆积消息。当堆积到一定量时,就会阻塞而不接收上游的新消息。从而慢慢地,上游进程的进程邮箱也会开始堆积消息。当堆积到一定量时也会阻塞而停止接收上游的消息,最后就会使负责网络数据包接收的进程阻塞而暂停接收新的数据。

        就以图9-4 为例,进程A 接收消息井转发至进程B ,进程B 接收消息并转发至进程C 。每个进程中都有一对关于收发消息的credit 值。以进程B 为例, { {credit_from , C} , value} 表示能发送多少条消息给C ,每发送一条消息该值减1,当为0 时,进程B 不再往进程C 发送消息也不再接收进程A 的消息。{{credit_to , A} , value} 表示再接收多少条消息就向进
程A 发送增加credit 值的通知,进程A 接收到该通知后就增加{ {credit_from ,B}, value} 所对应的值,这样进程A 就能持续发送消息。当上游发送速率高于下游接收速率时, credit 值就会被逐渐耗光,这时进程就会被阻塞,阻塞的情况会一直传递到最上游。当上游进程收到来自下游进程的增加credit 值的通知时,若此时上游进程处于阻塞状态则解除阻塞,开始接收更上游进程的消息,一个一个传导最终能够解除最上游的阻塞状态。由此可知,基于信用证的流控机制最终将消息发送进程的发送速率限制在消息处理进程的处理能力范围之内。

图9-4 信用证算法

        一个连接(Connection) 触发流控时会处于"flow" 的状态,也就意味着这个Connection 的状态每秒在blocked 和unblocked 之间来回切换数次,这样可以将消息发送的速率控制在服务器能够支撑的范围之内。可以通过rabbitmqctl list_connections 命令或者Web 管理界面来查看Connection 的状态,如图所示。

        处于flow 状态的Connection 和处于running 状态的Connection 并没有什么不同,这个状态只是告诉系统管理员相应的发送速率受限了。而对于客户端而言,它看到的只是服务器的带宽要比正常情况下要小一些。

        流控机制不只是作用于Connection ,同样作用于信道(Channel)和队列。从Connection 到Channel ,再到队列,最后是消息持久化存储形成一个完整的流控链,对于处于整个流控链中的任意进程,只要该进程阻塞,上游的进程必定全部被阻塞。也就是说,如果某个进程达到性能瓶颈,必然会导致上游所有的进程被阻塞。所以我们可以利用流控机制的这个特点找出瓶颈之所在。处理消息的几个关键进程及其对应的顺序关系如图9-6 所示。

图9-6 流控链

其中的各个进程如下所述:

  • rabbit_reader: Connection 的处理进程,负责接收、解析AMQP 协议数据包等。
  • rabbit_channel: Channel 的处理进程, 负责处理AMQP 协议的各种方法、进行路由解析等。
  • rabbit_amqqueue_process: 队列的处理进程,负责实现队列的所有逻辑。
  • rabbit_msg_store: 负责实现消息的持久化

        当某个Connection 处于flow 状态,但这个Connection 中没有一个Channel 处于flow 状态时,这就意味这个Connection 中有一个或者多个Channel 出现了性能瓶颈。某些Channel 进程的运作(比如处理路由逻辑)会使得服务器CPU 的负载过高从而导致了此种情形。尤其是在发送大量较小的非持久化消息时,此种情形最易显现

        当某个Connection 处于flow 状态,并且这个Connection 中也有若干个Channel 处于flow 状态,但没有任何一个对应的队列处于flow 状态时,这就意味着有一个或者多个队列出现了性能瓶颈。这可能是由于将消息存入队列的过程中引起服务器CPU 负载过高,或者是将队列中的消息存入磁盘的过程中引起服务器 I/O 负载过高而引起的此种情形。尤其是在发送大量较小的持久化消息时,此种情形最易显现。

        当某个Connection 处于flow 状态,同时这个Connection 中也有若干个Channel 处于flow 状态,井且也有若干个对应的队列处于flow 状态时,这就意味着在消息持久化时出现了性能瓶颈。在将队列中的消息存入磁盘的过程中引起服务器1/0 负载过高而引起的此种情形。尤其是在发送大量较大的持久化消息时,此种情形最易显现。

3.4、镜像队列

        如果RabbitMQ 集群中只有一个Broker 节点,那么该节点的失效将导致整体服务的临时性不可用,并且也可能会导致消息的丢失。可以将所有消息都设置为持久化,并且对应队列的durable 属性也设置为true,但是这样仍然无法避免由于缓存导致的问题:因为消息在发送之后和被写入磁盘井执行刷盘动作之间存在一个短暂却会产生问题的时间窗。通过publisher confmn 机制能够确保客户端知道哪些消息己经存入磁盘,尽管如此, 一般不希望遇到因单点故障导致的服务不可用。

        如果RabbitMQ 集群是由多个Broker 节点组成的,那么从服务的整体可用性上来讲,该集群对于单点故障是有弹性的,但是同时也需要注意:尽管交换器和绑定关系能够在单点故障问题上幸免于难,但是队列和其上的存储的消息却不行,这是因为队列进程及其内容仅仅维持在单个节点之上,所以一个节点的失效表现为其对应的队列不可用。

        引入镜像队列(Mirror Queue) 的机制,可以将队列镜像到集群中的其他Broker 节点之上,如果集群中的一个节点失效了,队列能自动地切换到镜像中的另一个节点上以保证服务的可用性。在通常的用法中,针对每一个配置镜像的队列(以下简称镜像队列〉都包含一个主节点(master) 和若干个从节点(slave) ,相应的结构可以参考图9-11 。

图9-11 主从结构

        slave 会准确地按照master 执行命令的顺序进行动作,故slave 与master 上维护的状态应该是相同的。如果master 由于某种原因失效,那么"资历最老"的slave 会被提升为新的master 。根据slave 加入的时间排序,时间最长的slave 即为"资历最老"。发送到镜像队列的所有消息会被同时发往master 和所有的slave 上,如果此时master 挂掉了,消息还会在slave 上,这样slave提升为master 的时候消息也不会丢失。除发送消息(Basic.Publish) 外的所有动作都只会向master 发送,然后再由master 将命令执行的结果广播给各个slave 。

        如果消费者与slave 建立连接并进行订阅消费,其实质上都是从master 上获取消息,只不过看似是从slave 上消费而己。比如消费者与slave 建立了TCP 连接之后执行一个Basic.Get 的操作,那么首先是由slave 将Basic.Get 请求发往master ,再由master 准备好数据返回给slave ,最后由slave 投递给消费者。读者可能会有疑问,大多的读写压力都落到了master 上,那么这样是否负载会做不到有效的均衡?或者说是否可以像MySQL 一样能够实现master 写而slave 读呢?注意这里的master 和slave 是针对队列而言的,而队列可以均匀地散落在集群的各个Broker 节点中以达到负载均衡的目的,因为真正的负载还是针对实际的物理机器而言的,而不是内存中驻留的队列进程。

        在图9-12 中,集群中的每个Broker 节点都包含1 个队列的master 和2 个队列的slave ,Q1的负载大多都集中在broker1 上, Q2 的负载大多都集中在broker2 上, Q3 的负载大多都集中在broker3 上,只要确保队列的master 节点均匀散落在集群中的各个Broker 节点即可确保很大程度上的负载均衡(每个队列的流量会有不同,因此均匀散落各个队列的master 也无法确保绝对的负载均衡)。至于为什么不像MySQL 一样读写分离, RabbitMQ 从编程逻辑上来说完全可以实现,但是这样得不到更好的收益,即读写分离并不能进一步优化负载,却会增加编码实现的复杂度,增加出错的可能,显得得不偿失。

图9-12 集群结构

注意要点:RabbitMQ 的镜像队列同时支持publisher confirm 和事务两种机制.在事务机制中,只有当前事务在全部镜像中执行之后,客户端才会收到Tx.Commit-Ok 的消息。同样的,在publisher confirm 机制中, 生产者进行当前消息确认的前提是该消息被全部进行所接收了。

        不同于普通的非镜像队列(参考图9-2 ) ,镜像队列的backing_queue 比较特殊,其实现井非是rabbit_variable_queue ,它内部包裹了普通 backing_queue 进行本地消息消息持久化处理,在此基础上增加了将消息和ack 复制到所有镜像的功能。镜像队列的结构可以参考图9-13 , master 的backing_queue 采用的是 rabbit_mirror_queue_master ,而slave的 backing_queue 实现是rabbit_mirror_queue_slave。

图9-13 镜像队列结构

        所有对 rabbit_mirror_queue_master 的操作都会通过组播GM(Guaranteed Multicast)的方式同步到各个slave 中。GM 负责消息的广播, rabbit_mirror_queue_slave 负责回调处理,而master 上的回调处理是由coordinator 负责完成的。如前所述,除了 Basic.Publish ,所有的操作都是通过master 来完成的, master 对消息进行处理的同时将消息的处理通过GM 广播给所有的slave,slave 的GM 收到消息后,通过回调交由 rabbit_mirror_queue_slave 进行实际的处理。

        GM 模块实现的是一种可靠的组播通信协议,该协议能够保证组播消息的原子性,即保证组中活着的节点要么都收到消息要么都收不到,它的实现大致为:将所有的节点形成一个循环链表,每个节点都会监控位于自己左右两边的节点,当有节点新增时,相邻的节点保证当前广播的消息会复制到新的节点上: 当有节点失效时,相邻的节点会接管以保证本次广播的消息会复制到所有的点。在master 和slave 上的这些GM 形成一个组(gm_group) ,这个组的信息会记录在Mnesia 中。不同的镜像队列形成不同的组。操作命令从master 对应的GM 发出后,顺着链表传送到所有的节点。由于所有节点组成了一个循环链表, master 对应的GM 最终会收到自己发送的操作命令,这个时候master 就知道该操作命令都同步到了所有的 slave 上。

        新节点的加入过程可以参考图9-14 ,整个过程就像在链表中间插入一个节点。注意每当一个节点加入或者重新加入到这个镜像链路中时,之前队列保存的内容会被全部清空。

图9-14 新节点的加入过程

        当slave 挂掉之后,除了与slave 相连的客户端连接全部断开,没有其他影响。当master 挂掉之后,会有以下连锁反应:

  1. 与master 连接的客户端连接全部断开。
  2. 选举最老的slave 作为新的master,因为最老的slave 与旧的master 之间的同步状态应该是最好的。如果此时所有slave 处于未同步状态,则未同步的消息会丢失。
  3. 新的master 重新入队所有unack 的消息,因为新的slave 无法区分这些unack 的消息是否己经到达客户端,或者是ack 信息丢失在老的master 链路上,再或者是丢失在老的master 组播ack 消息到所有slave 的链路上,所以出于消息可靠性的考虑,重新入队所有unack 的消息,不过此时客户端可能会有重复消息。
  4. 如果客户端连接着slave ,并且Basic.Consume 消费时指定了x-cancel-on-ha-failover 参数,那么断开之时客户端会收到一个Consumer Cancellation Notification 的通知,消费者客户端中会回调Consumer 接口的handleCancel 方法。如果未指定 x-cancelon-ha-failover 参数,那么消费者将无法感知master 岩机。

x-cancel-on-ha-failover 参数的使用示例如下:

Channel channel = . .. ;
Consumer consumer = ..;
Map<String , Object> args = new HashMap<String , Object>();
args.put("x-cancel-on-ha-failover" , true);
channel.basicConsume("my-queue", false, args, consumer);

        镜像队列的配置主要是通过添加相应的Policy 来完成的,对于添加Policy 的细节可以参考签名章节。这里需要详解介绍的是rabbitmqctl set_policy [-p vhost]  [--priority priority]  [--apply-to apply- to]  {name) {pattern} {definition} 命令中的 definition 部分,对于镜像队列的配置来说, definition 中需要包含3 个部分: ha-mode 、ha-params 和ha-sync-mode 。

  • ha-mode : 指明镜像队列的模式,有效值为all 、exactly 、nodes ,默认为all 。all 表示在集群中所有的节点上进行镜像; exactly 表示在指定个数的节点上进行镜像,节点个数由 ha-params 指定;nodes 表示在指定节点上进行镜像,节点名称通过ha-params 指定,节点的名称通常类似于rabbit@hostname ,可以通过rabbitmqctl cluster_status 命令查看到。
  • ha-params : 不同的 ha-mode 配置中需要用到的参数。
  • ha-sync-mode : 队列中消息的同步方式,有效值为automatic 和manual 。

        举个例子,对队列名称以 "queue_" 开头的所有队列进行镜像,并在集群的两个节点上完成镜像, Policy 的设置命令为:

rabbitmqctl set_policy --priority 0 --apply-to queues mirror_queue "^queue_" '{"ha-mode":"exactly" , "ha-params ": 2, "ha-sync-mode ": "automatic"}'

        ha-mode 参数对排他( exclusive ) 队列并不生效,因为排他队列是连接独占的,当连接断开时队列会自动删除,所以实际上这个参数对排他队列没有任何意义。

        将新节点加入己存在的镜像队列时,默认情况下ha-sync-mode 取值为manual.镜像队列中的消息不会主动同步到新的slave 中,除非显式调用同步命令。当调用同步命令后,队列开始阻塞,无法对其进行其他操作,直到同步完成。当ha-sync-mode 设置为automatic 时,新加入的slave 会默认同步己知的镜像队列。由于同步过程的限制,所以不建议对生产环境中正在使用的队列进行操作。使用 rabbitrnqctl list_queues {name} slave_pids synchronised_slave_pids 命令可以查看哪些slaves 已经完成同步。通过手动方式同步一个队列的命令为 rabbitrnqctl sync_queue {name} ,同样也可以取消某个队列的同步操作: rabbitrnqctl cancel_sync_queue {name}。

        当所有slave 都出现未同步状态,并且ha-prornote-on -shutdown 设置为when-synced(默认)时,如果master 因为主动原因停掉,比如通过rabbitrnqctl stop 命令或者优雅关闭操作系统,那么slave 不会接管master,也就是此时镜像队列不可用:但是如果master 因为被动原因停掉,比如Erlang 虚拟机或者操作系统崩溃,那么slave 会接管master。这个配置项隐含的价值取向是保证消息可靠不丢失,同时放弃了可用性。如果ha-prornote-on-shutdown 设置为always ,那么不论master 因为何种原因停止, slave 都会接管master ,优先保证可用性,不过消息可能会丢失。

        镜像队列中最后一个停止的节点会是master,启动顺序必须是master 先启动。如果slave先启动,它会有30 秒的等待时间,等待master 的启动,然后加入到集群中。如果30 秒内master没有启动, slave 会自动停止。当所有节点因故(断电等)同时离线时,每个节点都认为自己不是最后一个停止的节点,要恢复镜像队列,可以尝试在30 秒内启动所有节点。

4、网络分区

4.1、什么是网络分区

        RabbitMQ 集群的网络分区的容错性并不是很高, 一般都是使用Federation 或者Shovel 来解决广域网中的使用问题。不过即使是在局域网环境下,网络分区也不可能完全避免,网络设备(比如中继设备、网卡)出现故障也会导致网络分区。当出现网络分区时,不同分区里的节点会认为不属于自身所在分区的节点都已经挂(down) 了,对于队列、交换器、绑定的操作仅对当前分区有效。在RabbitMQ 的默认配置下,即使网络恢复了也不会自动处理网络分区带来的问题。RabbitMQ 从3.1 版本开始会自动探测网络分区,并且提供了相应的配置来解决这个问题。

        当一个集群发生网络分区时,这个集群会分成两个部分或者更多,它们各自为政,互相都认为对方分区内的节点已经挂了,包括队列、交换器及绑定等元数据的创建和销毁都处于自身分区内,与其他分区无关。如果原集群中配置了镜像队列,而这个镜像队列又牵涉两个或者更多个网络分区中的节点时,每一个网络分区中都会出现一个master 节点,对于各个网络分区,此队列都是相互独立的。当然也会有一些其他未知的、怪异的事情发生。当网络恢复时,网络分区的状态还是会保持,除非你采取了一些措施去解决它。

        如果你没有经历过网络分区,就不算真正掌握RabbitMQ 。网络分区带来的影响大多是负面的,极端情况下不仅会造成数据丢失,还会影响服务的可用性。

        或许你不禁要问,既然网络分区会带来如此负面的影响,为什么RabbitMQ 还要引入网络分区的设计理念呢?其中一个原因就与它本身的数据一致性复制原理有关,如上一章所述,RabbitMQ 采用的镜像队列是一种环形的逻辑结构,如图10-1 所示。

        图10-1 中为某队列配置了4 个镜像,其中A 节点作为master 节点,其余B 、C 和D 节点作为slave 节点, 4 个镜像节点组成一个环形结构。假如需要确认( ack) 一条消息,先会在A节点即master 节点上执行确认命令,之后转向B 节点,然后是C 和D 节点,最后由D 将执行操作返回给A 节点,这样才真正确认了一条消息,之后才可以继续相应的处理。这种复制原理和 ZooKeeper 的Quorum 原理不同,它可以保证更强的一致性。在这种一致性数据模型下,如果出现网络波动或者网络故障等异常情况,那么整个数据链的性能就会大大降低。如果C 节点网络异常,那么整个A→B→ C→D→A 的数据链就会被阻塞,继而相关服务也会被阻塞,所以这里就需要寻|入网络分区来将异常的节点剥离出整个分区,以确保RabbitMQ 服务的可用性及可靠性。等待网络恢复之后,可以进行相应的处理来将此前的异常节点加入集群中。

图10-1 环形逻辑结构

        网络分区对于RabbitMQ 本身而言有利有弊,读者在遇到网络分区时不必过于惊慌。许多情况下,网络分区都是由单个节点的网络故障引起的,且通常会形成一个大分区和一个单节点的分区,如果之前又配置了镜像,那么可以在不影响服务可用性,不丢失消息的情况下从网络分区的情形下得以恢复。

4.2、如何判定网络分区

        RabbitMQ 集群节点内部通信端口默认为25672 ,两两节点之间都会有信息交互。如果某节点出现网络故障,或者是端口不通, 则会致使与此节点的交互出现中断, 这里就会有个超时判定机制, 继而判定网络分区。

        对于网络分区的判定是与 net_ticktirne 这个参数息息相关的,此参数默认值为60 秒。注意与heartbeat_tirne 的区别, heartbeat_tirne 是指客户端与RabbitMQ 服务之间通信的心跳时间,针对5672 端口而言。如果发生超时则会有 net_ticktimeout 的信息报出。在RabbitMQ 集群内部的每个节点之间会每隔四分之一的 net_ticktirne 计一次应答( tick ) 。如果有任何数据被写入节点中,则此节点被认为已经被应答( ticked ) 了。如果连续4 次, 某节点都没有被ticked , 则可以判定此节点己处于" down" 状态, 其余节点可以将此节点剥离出当前分区。

        将连续4 次的tick 时间记为T ,那么T 的取值范围为: O. 75 * net_ticktirne < T < 1.25 * net_ticktirne 。图10-2 可以形象地描绘出这个取值范围的缘由。

图10-2 取值范围的原由

        图10-2 中每个节点代表一次tick 判定的时间戳,在2 个临界值 0.75 * net_ticktirne 和 1. 25 * net_ticktirne 之间可以连续执行4 次的tick 判定。默认情况下,在 45s < T < 75s 之间会判定出net_ticktimeout 。

        RabbitMQ 不仅会将队列、交换器及绑定等信息存储在Mnesia 数据库中,而且许多围绕网络分区的一些细节也都和这个Mnesia 的行为相关。如果一个节点不能在T 时间连上另一个节点,那么Mnesia 通常认为这个节点己经挂了, 就算之后两个节点又重新恢复了内部通信, 但是这两
个节点都会认为对方已经挂了, Mnesia 此时认定了发生网络分区的情况。这些会被记录到RabbitMQ 的服务日志之中, 如下:

=ERROR REPORT==== 16-0ct-2017 : :18 : 20:55 ===
Mnesia( ' rabbit@node1 ' ) : ** ERROR ** mnesia event got
{inconsistent database , running partitioned network, ' rabbit@node2'}

        除了通过查看RabbitMQ 服务日志的方式,还有以下3 种方法可以查看是否出现网络分区。第一种,采用rabbitmqctl 工具来查看,即采用 rabbitmqctl cluster_status 命令。通过这条命令可以看到集群相关信息, 未发生网络分区时的情形举例如下:

         根据上面的信息可以知道 partitions 这一项没有相关记录,则说明没有产生网络分区。如果有相关记录,则说明产生了网络分区,例如:

特别说明:这里如果是通过模拟网络分区产生的话,当想查看集群节点的 partitions 选项是否有记录时,需要在出问题的节点上才能看到对应的记录信息。其余正常节点的该选项是没数据的。

第二种方式,也可以通过WEB管理界面查看到对应的反馈信息,如下图显示的警告信息。当出现该信息,说明发生了网络分区。

集群其余正常节点的web: 

第三种方式,是通过HTTP API的方式来检测是否发生了网络分区,比如通过 curl 命令来调取节点信息:

[rabbit@myblnp1 rabbitmq]$ curl -i -u root:root123 -H "content-type:application/json" -X GET http://192.168.56.120:15672/api/nodes | grep partitions

4.3、模拟网络分区

模拟网络分区示例,建议最好是在本地虚机上进行操作。因为很容易导致服务器上的服务不可用,尤其是对相关命令不熟悉的情况下。

相关文章推荐:

        正常情况下,很难观察到RabbitMQ 网络分区的发生。为了更好地理解网络分区,需要采取某些手段将其模拟出来,以便对其进行相应的分析处理,进而在实际应用环境中遇到类似情形,可以让你的处理游刃有余。往长远方面讲,也可以采取一些必要的手段去规避网络分区的发生,或者可以监控网络分区以及准备相关的处理预案。模拟网络分区的方式有多种, 主要分为以下3 大类:

  • iptables 封禁/解封 IP 地址或者端口号。
  • 关闭/开启网卡。
  • 挂起/恢复操作系统。

4.3.1、iptables 封禁/解封

        由于RabbitMQ 集群内部节点通信端口默认为25672 ,可以封禁这个端口来模拟出net_tick_timeout , 然后再开启此端口让集群判定网络分区的发生。举例说明, 整个RabbitMQ 集群由3 个节点组成,分别为node1、node2 和node3 。此时我们要模拟node2 节点被剥离出当前分区的情形,即模拟[node1 , node3]和[node2]两个分区。可以在node2 上执行如下命令以封禁25672 端口。如果在配置中修改过这个端口号,将下面的命令改成相应的端口号即可。

[rabbit@myblnp2 ~]$ sudo iptables -A INPUT -p tcp --dport 25672 -j DROP
[rabbit@myblnp2 ~]$ sudo iptables -A OUTPUT -p tcp --dport 25672 -j DROP

        同时需要监测各个节点的服务日志,当有如下相似信息出现时即为己经判定出:net_tick_timeout:

2023-11-03 14:28:52.590 [info] <0.774.0> Node rabbit@myblnp2 is down, deleting its listeners
2023-11-03 14:28:52.593 [info] <0.774.0> node rabbit@myblnp2 down: net_tick_timeout

        当然,如果不想这么麻烦地去监测各个节点的服务日志,那么也可以等待75 秒(45s<T< 75s) 之后以确保出现 net_tick_timeout。注意此时只判定出 net_tick_timeout , 要等node2 网络恢复之后,即解封25672 端口之后才会判定出现网络分区。解封命令如下:

[rabbit@myblnp2 ~]$ sudo iptables -D INPUT 1
[rabbit@myblnp2 ~]$ sudo iptables -D OUTPUT 1

特别提醒:执行该解封命令时,有可能导致ssh工具断开连接,并且当前节点的对外服务都无法访问(我这边操作是出现了这个情况),但是在节点内访问是正常的。初步怀疑导致的原因是 iptables 规则的问题,因为我这边将当前节点的 iptables 规则清空后(如果需要恢复默认的规则可重启服务器),对外服务又正常了。但是rabbitmq服务还是不行,会出现脑裂的问题。

        脑裂的问题是因为rabbitmq集群发生了网络分区问题,导致各节点都认为对方节点挂了,然后rabbitmq服务的网络分区处理策略默认是不处理的,因此哪怕你重启服务或者服务器,该节点都无法恢复正常。

        解决办法是将其余节点停止(rabbitmqctl stop_app)并在配置文件中配置处理策略(最好是所有节点都统一配置),具体配置如下所示:

#ignore:
##默认类型,不处理。要求你所在的网络环境非常可靠。例如,你的所有 node 都在同一个机架上,通过交换机互联,并且该交换机还是与外界通信的必经之路。

#pause_minority:
##rabbitmq节点感知集群中其他节点down掉时,会判断自己在集群中处于多数派还是少数派,也就是判断与自己形成集群的节点个数在整个集群中的比例是否超过一半。如果是多数派,则正常工作,如果是少数派,则会停止rabbit应用并不断检测直到自己成为多数派的一员后再次启动rabbit应用。注意:这种处理方式集群通常由奇数个节点组成。在CAP中,优先保证了CP。注意:pause_minority适用情形有限制,如3个节点集群,每次只down1个时,此模式适用。但如果网络都出问题,3节点会独立形成3个集群。

#autoheal:
##你的网络环境可能是不可靠的。你会更加关心服务的可持续性,而非数据完整性。你可以构建一个包含2个node的集群。当网络分区恢复后,rabbitmq各分区彼此进行协商,分区中客户端连接数最多的为胜者,其余的全部会进行重启,恢复到同步状态。
cluster_partition_handling=autoheal

        当节点都配置后,将节点启动(rabbitmqctl start_app)。发生网络分区的节点需要重启服务器,随后就恢复正常了。

        除此之外,还可以使用iptables 封禁 IP 地址的方法模拟网络分区。假设整个RabbitMQ集群的节点名称与其IP 地址对应如下:

  1. node1  --> 192.168.56.119
  2. node2 -->  192.168.56.120
  3. node3 -->  192.168.56.121

        如果要模拟出 [nodel , node3] 和 [node2] 两个分区的情形,可以在node2 节点上执行:

封禁命令:

[rabbit@myblnp2 ~]$ sudo iptables -I INPUT -s 192.168.56.119 -j DROP
[rabbit@myblnp2 ~]$ sudo iptables -I INPUT -s 192.168.56.121 -j DROP

解封命令:

[rabbit@myblnp2 ~]$ sudo iptables -D INPUT 1
[rabbit@myblnp2 ~]$ sudo iptables -D INPUT 1

或者也可以分别在 node1 和 node3 上执行以下命令:

#封禁命令
[rabbit@myblnp2 ~]$ sudo iptables -I INPUT -s 192.168.56.120 -j DROP
#解封命令
[rabbit@myblnp2 ~]$ sudo iptables -D INPUT 1

        如果集群的节点部署跨网段, 可以采取禁用整个网络段的方式模拟网络分区。假设 RabbitMQ 集群中3 个节点和其对应的E 关系如下:

  1. node1 --> 192.168.56.119
  2. node2 --> 192.168.57.120   //这里跨网段了
  3. node3 -->  192.168.56.121

模拟出 [node1 , node3] 和 [node2] 两个分区的情形, 可以在node2 节点上执行:

#封禁命令:
[rabbit@myblnp2 ~]$ sudo iptables -I INPUT -s 192.168.56.0/24 -j DROP
#解封命令:
[rabbit@myblnp2 ~]$ sudo iptables -D INPUT 1

4.3.2、关闭/开启网卡

        操作网卡的方式和 iptables 的方式有相似之处, 都是模拟网络故障来产生网络分区。首先需要使用 ifconfig 命令来查询出当前的网卡编号, 如下所示,一般情况下单台机器只有一个网卡(这里暂时不考虑多网卡的情形, 因为对于RabbitMQ 来说,多网卡的情况造成的网络分区异常复杂,这个在后面的内容中会有详细阐述。

[root@myblnp1 ~]# ifconfig
enp0s3: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 10.0.2.11  netmask 255.255.255.0  broadcast 10.0.2.255
        inet6 fe80::2668:caa:cf45:5163  prefixlen 64  scopeid 0x20<link>
        ether 08:00:27:a9:84:fb  txqueuelen 1000  (Ethernet)
        RX packets 127  bytes 16990 (16.5 KiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 136  bytes 12483 (12.1 KiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

enp0s8: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.56.119  netmask 255.255.255.0  broadcast 192.168.56.255
        inet6 fe80::ebd9:a38c:65cf:beb3  prefixlen 64  scopeid 0x20<link>
        ether 08:00:27:ef:2c:d4  txqueuelen 1000  (Ethernet)
        RX packets 51162  bytes 13825781 (13.1 MiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 53544  bytes 18812064 (17.9 MiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10<host>
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 1063  bytes 98400 (96.0 KiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 1063  bytes 98400 (96.0 KiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

[root@myblnp1 ~]# 

        同样假设node1 、node2 和node3 这三个节点组成RabbitMQ 集群, node2 的网卡编号为
enp0s8,此时要模拟网络分区 [node1, node3 ] 和 [node2] 的情形,需要在node2 上执行以下命令关闭网卡:

[rabbit@myblnp2 ~]$ ifdown enp0s8
用户不能控制这一设备。
[rabbit@myblnp2 ~]$ sudo ifdown enp0s8

Socket error Event: 32 Error: 10053.
Connection closing...Socket close.

Connection closed by foreign host.

Disconnected from remote host(myblnp2) at 10:40:56.

Type `help' to learn how to use Xshell prompt.
[C:\~]$ 

##使用ssh工具执行的话,当执行完成后连接会断开,当前服务器上的对外服务也将不能访问

        执行一段时间后 RabbitMQ 服务将会自动检测,当判定出 net_tick_timeout 之后,再开启网卡,如下输出日志:

2023-11-06 10:40:53.917 [error] <0.986.0> closing AMQP connection <0.986.0> (192.168.56.120:51296 -> 192.168.56.121:5672 - Shovel hidden_shovel):
missed heartbeats from client, timeout: 10s
2023-11-06 10:40:59.124 [info] <0.2713.0> accepting AMQP connection <0.2713.0> (192.168.56.1:65299 -> 192.168.56.121:5672)
2023-11-06 10:40:59.126 [info] <0.2713.0> Connection <0.2713.0> (192.168.56.1:65299 -> 192.168.56.121:5672) has a client-provided name: Shovel hidden_shovel
2023-11-06 10:40:59.130 [info] <0.2713.0> connection <0.2713.0> (192.168.56.1:65299 -> 192.168.56.121:5672 - Shovel hidden_shovel): user 'root' authenticated and granted access to vhost '/'
2023-11-06 10:41:36.720 [info] <0.2713.0> closing AMQP connection <0.2713.0> (192.168.56.1:65299 -> 192.168.56.121:5672 - Shovel hidden_shovel, vhost: '/', user: 'root')
2023-11-06 10:41:42.951 [info] <0.747.0> rabbit on node rabbit@myblnp2 down
2023-11-06 10:41:42.951 [error] <0.249.0> ** Node rabbit@myblnp2 not responding **
** Removing (timedout) connection **
2023-11-06 10:41:42.952 [warning] <0.936.0> Management delegate query returned errors:
[{<18509.677.0>,{exit,{nodedown,rabbit@myblnp2},[]}}]
2023-11-06 10:41:42.953 [warning] <0.937.0> Management delegate query returned errors:
[{<18509.677.0>,{exit,{nodedown,rabbit@myblnp2},[]}}]
2023-11-06 10:41:42.954 [error] <0.953.0> ** Generic server rabbit_shovel_status terminating 
** Last message in was check
** When Server state == {state,{erlang,#Ref<0.1479694798.1283457025.156303>}}
** Reason for termination ==
** {{{nodedown,rabbit@myblnp2},{gen_server,call,[<18509.776.0>,which_children,infinity]}},[{gen_server,call,3,[{file,"gen_server.erl"},{line,247}]},{mirrored_supervisor,child,2,[{file,"src/mirrored_supervisor.erl"},{line,252}]},{mirrored_supervisor,'-fold/3-lc$^0/1-0-',2,[{file,"src/mirrored_supervisor.erl"},{line,249}]},{mirrored_supervisor,'-fold/3-lc$^1/1-1-',3,[{file,"src/mirrored_supervisor.erl"},{line,249}]},{mirrored_supervisor,fold,3,[{file,"src/mirrored_supervisor.erl"},{line,248}]},{rabbit_shovel_dyn_worker_sup_sup,cleanup_specs,0,[{file,"src/rabbit_shovel_dyn_worker_sup_sup.erl"},{line,70}]},{rabbit_shovel_status,handle_info,2,[{file,"src/rabbit_shovel_status.erl"},{line,74}]},{gen_server,try_dispatch,4,[{file,"gen_server.erl"},{line,695}]}]}
2023-11-06 10:41:42.955 [error] <0.953.0> CRASH REPORT Process rabbit_shovel_status with 0 neighbours exited with reason: {{nodedown,rabbit@myblnp2},{gen_server,call,[<18509.776.0>,which_children,infinity]}} in gen_server:call/3 line 247
2023-11-06 10:41:42.956 [error] <0.952.0> Supervisor rabbit_shovel_sup had child rabbit_shovel_status started with rabbit_shovel_status:start_link() at <0.953.0> exit with reason {{nodedown,rabbit@myblnp2},{gen_server,call,[<18509.776.0>,which_children,infinity]}} in context child_terminated
2023-11-06 10:41:42.957 [error] <0.952.0> Supervisor rabbit_shovel_sup had child rabbit_shovel_status started with rabbit_shovel_status:start_link() at <0.953.0> exit with reason reached_max_restart_intensity in context shutdown
2023-11-06 10:41:42.970 [info] <0.44.0> Application rabbitmq_shovel exited with reason: shutdown
2023-11-06 10:41:42.979 [error] <0.383.0> Mnesia(rabbit@myblnp3): ** ERROR ** mnesia_event got {inconsistent_database, running_partitioned_network, rabbit@myblnp2}
2023-11-06 10:41:43.006 [info] <0.747.0> Keeping rabbit@myblnp2 listeners: the node is already back
2023-11-06 10:41:43.024 [info] <0.747.0> node rabbit@myblnp2 down: net_tick_timeout
2023-11-06 10:41:43.024 [info] <0.747.0> node rabbit@myblnp2 up
2023-11-06 10:41:43.026 [info] <0.747.0> Autoheal request sent to rabbit@myblnp1
2023-11-06 10:41:43.141 [warning] <0.936.0> Management delegate query returned errors:
[{<18509.677.0>,{exit,{nodedown,rabbit@myblnp2},[]}}]
2023-11-06 10:41:43.209 [info] <0.747.0> rabbit on node rabbit@myblnp2 down
2023-11-06 10:41:43.250 [info] <0.747.0> Keeping rabbit@myblnp2 listeners: the node is already back
2023-11-06 10:43:27.960 [error] <0.2746.0> ** Node rabbit@myblnp2 not responding **
** Removing (timedout) connection **
2023-11-06 10:43:27.960 [info] <0.747.0> node rabbit@myblnp2 down: net_tick_timeout
2023-11-06 10:43:39.002 [info] <0.3013.0> accepting AMQP connection <0.3013.0> (192.168.56.120:51464 -> 192.168.56.121:5672)
2023-11-06 10:43:39.004 [info] <0.3013.0> Connection <0.3013.0> (192.168.56.120:51464 -> 192.168.56.121:5672) has a client-provided name: Shovel hidden_shovel
2023-11-06 10:43:39.006 [info] <0.3013.0> connection <0.3013.0> (192.168.56.120:51464 -> 192.168.56.121:5672 - Shovel hidden_shovel): user 'root' authenticated and granted access to vhost '/'

        此外,在 node2 网卡关闭后的期间,rabbitmq 集群web服务将无法访问;当网卡恢复后(sudo ifup enp0s8)还能继续使用,与此同时 web 管控台也将反馈发生了网络分区错误,如下所示:

        解决方法很简单,和上一节的办法是一样的。如果此时都是已经配置了处理策略的情况下,只需要对 node2 节点重启,即可恢复正常:

[rabbit@myblnp2 ~]$ rabbitmqctl stop_app
Stopping rabbit application on node rabbit@myblnp2 ...
[rabbit@myblnp2 ~]$ rabbitmqctl start_app
Starting node rabbit@myblnp2 ...
[rabbit@myblnp2 ~]$ 

2023-11-06 10:54:56.156 [info] <0.4010.0> Closing all channels from connection '192.168.56.120:51464 -> 192.168.56.121:5672' because it has been closed
2023-11-06 10:55:04.662 [info] <0.747.0> node rabbit@myblnp2 up
2023-11-06 10:55:05.865 [info] <0.747.0> rabbit on node rabbit@myblnp2 up
2023-11-06 10:55:06.013 [info] <0.4055.0> accepting AMQP connection <0.4055.0> (192.168.56.120:51532 -> 192.168.56.121:5672)
2023-11-06 10:55:06.014 [info] <0.4055.0> Connection <0.4055.0> (192.168.56.120:51532 -> 192.168.56.121:5672) has a client-provided name: Shovel hidden_shovel
2023-11-06 10:55:06.016 [info] <0.4055.0> connection <0.4055.0> (192.168.56.120:51532 -> 192.168.56.121:5672 - Shovel hidden_shovel): user 'root' authenticated and granted access to vhost '/'

        这样就可以模拟出网络分区。当然也可以使用 service network stop 和 service network start 这两个命令来模拟网络分区,原理同 ifdown/ifup enp0s8 的方式。

4.3.3、挂起/恢复操作系统

        除了模拟网络故障的方式,操作系统的挂起和恢复操作也会导致集群内节点的网络分区。因为发生挂起的节点不会认为自身己经失败或者停止工作,但是集群内的其他节点会这么认为。如果集群中的一个节点运行在一台笔记本电脑上,然后你合上了笔记本电脑,那么这个节点就挂起了。或者一个更常见的现象,集群中的一个节点运行在某台虚拟机上,然后虚拟机的管理程序挂起了这个虚拟机节点,这样节点就被挂起了。在等待了(0.75 * net_ticktime, 1. 25 * net_ticktime) 这个区间大小的时间之后,判定出net_tick_timeout,再恢复挂起的节点即可以复现网络分区。

4.4、网络分区的影响

4.4.1、未配置镜像

        node1 、node2 和node3 这3 个节点组成一个RabbitMQ 集群,且在这三个节点中分别创建queue1 、queue2 和queue3 这三个队列,井且相应的交换器与绑定关系如下:

节点名称交换机路由key队列
node1exchangerk1queue1
node2exchangerk2queue2
node3exchangerk3queue3

        node1 那条信息表示:在node1 节点上创建了队列queue1 ,并通过路由键 rk1 与交换器exchange 进行了绑定。在网络分区发生之前,客户端分别连接node1 和node2 井分别向queue1 和queue2 发送消息,对应关系如下所示:

情形一:

客户端节点名称交换器路由key队列
client1node1exchangerk1queue1
client2node2exchangerk2queue2

        client1 那条信息表示: 客户端 client1 连接 node1 的 IP 地址,并通过路由键 rk1 向交换器exchange 发送消息。如果发送成功,消息可以存入队列 queue1 中。其对应的发送代码如下:

channel.basicPublish ("exchange" , "rk1", true,
    MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());

        采用 iptables 封禁/解封 25672 端口的方式模拟网络分区,使node1 和node2 存在于两个不同的分区之中,对于客户端 client1 和client2 而言,没有任何异常,消息正常发送也没有消息丢失。如果这里采用 关闭/开启网卡 的方式来模拟网络分区, 在关闭网卡的时候客户端的连接也会关闭,这样就检测不出在网络分区发生时对客户端的影响。

情形二:

        下面我们再转换一下思路,如果上面的 client1 连接node1 的IP. 并向 queue2 发送消息会发生何种情形。新的对应关系参考下表:

客户端节点名称交换器路由key队列
client1node1exchangerk2queue2
client2node2exchangerk1queue1

        这里同样采用 iptables 的方式模拟网络分区,使得node1 和node2 处于两个不同的分区。如果客户端在发送消息的时候将 mandatory 参数设置为true. 那么在网络分区之后可以通过抓包工具(如wireshark 等)看到有 Basic.Return 将发迭的消息返回过来。这里表示在发生网络分区之后。client1 不能将消息正确地送达到 queue2 中,同样 client2 不能将消息送达到 queue1 中。如果客户端中设置了 ReturnListener 来监听 Basic.Return 的信息,井附带有消息重传机制,那么在整个网络分区前后的过程中可以保证发送端的消息不丢失。

        在网络分区之前 queue1 进程存在于 node1 节点中。 queue2 的进程存在于node2 节点中。在网络分区之后,在node1 所在的分区并不会创建新的 queue2 进程,同样在node2 所在的分区也不会创建新的queue1 的进程。这样在网络分区发生之后,虽然可以通过 rabbitmqctl list_queues name 命令在node1 节点上查看到queue2. 但是在node1 上已经没有真实的queue2 进程的存在。

        client1 将消息发往交换器exchange 之后并不能路由到queue2 中,因此消息也就不能存储。如果客户端没有设置mandatory 参数并且没有通过ReturnListener 进行消息重试(或者其他措施)来保障消息可靠性,那么在发送端就会有消息丢失。

情形三:

        上面讨论的是消息发送端的情况, 下面来探讨网络分区对消费端的影响。在网络分区之前,分别有客户端连接 node1 和node2 并订阅消费其上队列中的消息,其对应关系参考下表:

客户端节点名称队列
client3node1queue1
client4node2queue2

        client3 那条信息表示:client3 连接node1 的lp 并订阅消费queue1 。模拟网络分区置node1 和node2 于不同的分区之中。在发生网络分区的前后,消费端client3 和client4 都能正常消费,无任何异常发生。

情形四:

        参考情形二 和情形三中的消费队列交换下,即client3连接node1 的 IP 消费queue2 ,其对应关系如:

客户端节点名称队列
client3node1queue2
client4node2queue1

        模拟网络分区,将node1 与node2 置于两个不同的分区。在发生网络分区前,消费一切正常。在网络分区发生之后,虽然客户端没有异常报错,且可以消费到相关数据,但是此时会有一些怪异的现象发生,比如对于己消费消息的ack 会失效。在从网络分区中恢复之后,数据不会丢失。

        如果分区之后,重启 client3 或者有个新的客户端 client5 连接node1 的IP 来消费queue2 ,则会有如下报错:

同样在node1 的服务日志中也有相关记录:

        综上所述,对于未配置镜像的集群,网络分区发生之后,队列也会伴随着宿主节点而分散在各自的分区之中。对于消息发送方而言,可以成功发送消息,但是会有路由失败的现象, 需要需要配合 mandatory 等机制保障消息的可靠性。对于消息消费方来说,有可能会有诡异、不可预知的现象发生,比如对于己消费消息的ack 会失效。如果网络分区发生之后,客户端与某分区重新建立通信链路,其分区中如果没有相应的队列进程,则会有异常报出。如果从网络分区中恢复之后, 数据不会丢失, 但是客户端会重复消费。

4.4.2、已配置镜像

        如果集群中配置了镜像队列,那么在发生网络分区时,情形比未配置镜像队列的情况复杂得多,尤其是发生多个网络分区的时候。这里先简单地从3 个节点分裂成2 个网络分区的情形展开讨论。如前一节所述,集群中有node1 、node2 和node3 这3 个节点,分别在这些节点上创建队列queue1、queue2 和queue3 , 并配置镜像队列。采用iptables 的方式将集群模拟分裂成 [node1, node3] 和 [node2] 这两个网络分区。

情形一:分区之前

队列masterslave
queue1node1node3
queue2node2node3
queue3node3node2

        在发生网络分区之后, [nodel , node3]分区中的队列有了新的部署。除了queue1 未发生改变,queue2 由于原宿主节点node2 己被剥离当前分区,那么node3 提升为master ,同时选择node1作为slave 。在queue3 重新选择node1 作为其新的slave 。

情形一:分区之后

[nodel , node3] 分区 [node2] 分区

队列masterslavemasterslave
queue1node1node3node1node3
queue2node3node1node2[]
queue3node3node1node2[]

        对于 [node2] 分区而言, queue2 和queue3 的分布比较容易理解,此分区中只有一个节点,所有 slave 这一列为空。但是对于queue1 而言,其部署还是和分区前如出一辙。不管是在网络分区前,还是在网络分区之后,再或者是又从网络分区中恢复, 对于queue1 而言生产和消费消息都不会受到任何的影响,就如未发生过网络分区一样。对于队列queue2 和queue3 的情形可以参考上面未配置镜像的相关细节,从网络分区中恢复(即恢复成之前的[node1,node2, node3]组成的完整分区)之后可能会有数据丢失。

情形二:分区之前

队列masterslave
queue1node1node2
queue2node2node3
queue3node3node1

情形二:分区之后

[nodel , node3] 分区  [node2] 分区

队列masterslavemasterslave
queue1node1node3node2[]
queue2node3node1node2[]
queue3node3node1node3node1

        前面讨论的镜像配置都是 ha-sync-mode=automatic 的情形,当有新的slave 出现时,此slave 会自动同步master 中的数据。注意在同步的过程中,集群的整个服务都不可用,客户端连接会被阻塞。如果master 中有大量的消息堆积,必然会造成slave 的同步时间增长,进一步影响了集群服务的可用性。如果配置ha-sync-mode=manual ,有新的slave 创建的同时不会去同步master 上旧的数据,如果此时master 节点又发生了异常,那么此部分数据将会丢失。同样 ha-promote-on-shutdown 这个参数的影响也需要考虑进来。

        网络分区的发生可能会引起消息的丢失,当然这点也有办法解决首先消息发送端要有能够处理 Basic.Return 的能力。其次,在监测到网络分区发生之后,需要迅速地挂起所有的生产者进程。之后连接分区中的每个节点消费分区中所有的队列数据。在消费完之后再处理网络分区。最后在从网络分区中恢复之后再恢复生产者的进程。整个过程可以最大程度上保证网络分区之后的消息的可靠性。同样也要注意的是,在整个过程中会伴有大量的消息重复,消费者客户端需要做好相应的罪等性处理。当然也可以采用集群迁移,将所有旧集群的资源都迁移到新集群来解决这个问题。

4.5、手动处理网络分区

        为了从网络分区中恢复,首先需要挑选一个信任分区,这个分区才有决定 Mnesia 内容的权限,发生在其他分区的改变将不会被记录到Mnesia 中而被直接丢弃。在挑选完信任分区之后,重启非信任分区中的节点,如果此时还有网络分区的告警,紧接着重启信任分区中的节点。这里有3 个要点需要详细阐述:

  1. 如何挑选信任分区?
  2. 如何重启节点?
  3. 重启的顺序有何考究?

        挑选信任分区一般可以按照这几个指标进行: 分区中要有disc 节点; 分区中的节点数最多;分区中的队列数最多;分区中的客户端连接数最多。优先级从前到后, 例如信任分区中要有disc节点; 如果有两个或者多个分区满足,则挑选节点数最多的分区作为信任分区;如果又有两个或者多个分区满足,那么挑选队列数最多的分区作为信任分区。依次类推, 如果有两个或者多个分区对于这些指标都均等,那么随机挑选一个分区也不失为一良策。

        RabbitMQ 中有两种重启方式: 第一种方式是使用 rabbitmqctl stop 命令关闭,然后再用rabbitmq-server -detached 命令启动; 第二种方式是使用rabbitmqctl stop_app 关闭, 然后使用rabbitmqctl start_app 命令启动。第一种方式需要同时重启Erlang 虚拟机和RabbitMQ 应用, 而第二种方式只是重启RabbitMQ 应用。两种方式都可以从网络分区中恢复,但是更加推荐使用第二种方式,包括下一节所讲述的自动处理网络分区的方式,其内部也是采用的第二种方式进行重启节点。

        RabbitMQ 的重启顺序也比较讲究,必须在以下两种重启顺序中择其一进行重启操作:

  1. 停止其他非信任分区中的所有节点, 然后再启动这些节点。如果此时还有网络分区的告警,则再重启信任分区中的节点以去除告警。
  2. 关闭整个集群中的节点,然后再启动每一个节点, 这里需要确保启动的第一个节点在信任的分区之中。

        在选择哪种重启顺序之前, 首先考虑一下队列" 漂移"的现象。所谓的队列"漂移"是在配置镜像队列的情况下才会发生的。假设一共集群中有node1 、node2 和node3 这3 个节点, 且配置全镜像(ha-mode=all ) , 具体如:

情形一:

队列masterslave
queue1node1node2,node3
queue2node2node3,node1
queue3node3node2,node1

        这里首先关闭node3 节点,那么queue3 中的某个slave 提升为master , 具体变化为情形二。

情形二:

队列masterslave
queue1node1node2
queue2node2node1
queue3node2node1

        然后在再关闭node2 节点,继续演变为情形三。

情形三:

队列masterslave
queue1node1[]
queue2node1[]
queue3node1[]

        此时,如果关闭node1 节点,然后再启动这3 个节点。或者不关闭node1 节点,而启动node2和node3 节点都只会增加slave 的个数,而不会改变master 的分布,最终如情形四所示。注意这里哪怕关闭了node 1.然后并非先启动node1 ,而是先启动node2 或者node3 ,对于master节点的分布都不会受影响

情形四:

队列masterslave(按节点启动顺序排列)
queue1node1node2,node3
queue2node1node2,node3
queue3node1node2,node3

        这里就可以看出,随着节点的重启,所有的队列的master 都" 漂移"到了node1 节点上,因为在RabbitMQ 中,除了发布消息,所有的操作都是在master 上完成的,如此大部分压力都集中到了node1 节点上,从而不能很好地实现负载均衡。

        基于情形二 , 考虑另外一种情形。如果在关闭节点node3 之后, 又重新启动节点node3,那么会有如情形五 的变化。

情形五:

队列masterslave
queue1node1node2,node3
queue2node2node1,node3
queue3node2node1,node3

        之后再重启(先关闭, 后启动) node2 节点,如情形六

情形六:

队列masterslave
queue1node1node3,node2
queue2node1node3,node2
queue3node1node3,node2

        继续重启node1 节点,如情形七

情形七:

队列masterslave
queue1node3node2,node1
queue2node3node2,node1
queue3node3node2,node1

        如此顺序演变,在配置镜像的集群中重启会有队列"漂移"的情况发生,造成负载不均衡。这里采用的是全镜像以作说明,读者可以自行推理对于2 个节点 ha-mode=exactly 且haparams=2 的镜像配置的演变过程。不管如何,都难以避免队列"漂移"的发生。

特别注意:

        一定要按照前面提及的两种方式择其一进行重启。如果选择挨个节点重启的方式,同样可以处理网络分区,但是这里会有一个严重的问题,即Mnesia 内容权限的归属问题。比如有两个分区 [node1, node2] 和 [node3 , node4] ,其中 [node1, node2] 为信任分区。此时若按照挨个重启的方式进行重启,比如先重启node3 ,在node3 节点启动之时无法判断其节点的 Mnesia 内容走向 [node 1, node2] 分区靠齐还是向node4 节点靠齐。至此,如果挨个一轮重启之后,最终集群中的 Mnesia 数据是 [node3 , node4] 这个非信任分区,就会造成无法估量的损失。挨个节点重启也有可能会引起二次网络分区的发生.

        如果原本配置了镜像队列,从发生网络分区到恢复的过程中队列可能会出现"漂移"的现象。可以重启之前先删除镜像队列的配置,这样能够在一定程度上阻止队列的"过分漂移",即阻止可能所有队列都"漂移"到一个节点上的情况。

删除镜像队列的配置可以采用rabbitmqctl 工具删除:

rabbitmqctl clear_policy [-p vhost] {mirror_queue_name}

可以通过Web 管理界面进行删除,也可以通过HTTPAPI 的方式进行删除:

curl -s -u {username:password} - X DELETE http://localhost:15672/api/policies/default/{mirror_queue_name}

        注意, 如果事先没有开启RabbitMQ Management 插件,那么只能使用rabbitmqctl 工具的方式。与此同时, 需要在每个分区上都执行删除镜像队列配置的操作,以确保每个分区中的镜像都被删除。具体的网络分区处理步骤如下所述。

  • 步骤1 : 挂起生产者和消费者进程。这样可以减少消息不必要的丢失,如果进程数过多,情形又比较紧急,也可跳过此步骤。
  • 步骤2 : 删除镜像队列的配置。
  • 步骤3: 挑选信任分区。
  • 步骤4: 关闭非信任分区中的节点。采用 rabbitmqctl stop_app 命令关闭。
  • 步骤5: 启动非信任分区中的节点。采用与步骤4 对应的rabbitmqctl start_app命令启动。
  • 步骤6: 检查网络分区是否恢复, 如果己经恢复则转步骤8 ; 如果还有网络分区的报警则转步骤7 。
  • 步骤7: 重启信任分区中的节点。
  • 步骤8: 添加镜像队列的配置。
  • 步骤9: 恢复生产者和消费者的进程。

4.6、自动处理网络分区

        RabbitMQ 提供了三种方法自动地处理网络分区: pause-mmonty 模式、pause-lιall-down 模式和autoheal 模式。默认是 ignore 模式,即不自动处理网络分区,所以在这种模式下,当网络分区的时候需要人工介入。在rabbitmq.config 配置文件中配置 cluster_partition_handling 参数即可实现相应的功能。默认的 ignore 模式的配置如下:

## Clustering
## =====================
##
# cluster_partition_handling = ignore

4.6.1、pause-minority 模式

        在 pause-minority 模式下,当发生网络分区时,集群中的节点在观察到某些节点 "down" 的时候,会自动检测其自身是否处于"少数派" (分区中的节点小于或者等于集群中一半的节点数), RabbitMQ 会自动关闭这些节点的运作。根据CAP 原理,这里保障了P ,即分区耐受性。这样确保了在发生网络分区的情况下,大多数节点(当然这些节点得在同一个分区中〉可以继续运行。"少数派"中的节点在分区开始时会关闭, 当分区结束时又会启动。这里关闭是指 RabbitMQ 应用的关闭,而Erlang 虚拟机并不关闭,类似于执行了rabbitmqctl stop_app命令。处于关闭的节点会每秒检测一次是否可连通到剩余集群中,如果可以则启动自身的应用。相当于执行 rabbitmqctl start_app 命令。

## Pauses all nodes on the minority side of a partition. The cluster
## MUST have an odd number of nodes (3, 5, etc)
cluster_partition_handling = pause_minority

        需要注意的是, RabbitMQ 也会关闭不是严格意义上的大多数,比如在一个集群中只有两个节点的时候并不适合采用pause-minority 的模式,因为其中任何一个节点失败而发生网络分区时,两个节点都会关闭。当网络恢复时, 有可能两个节点会自动启动恢复网络分区,也有可能仍保持关闭状态。然而如果集群中的节点数远大于2 个时, pause_minority 模式比 ignore 模式更加可靠,特别是网络分区通常是由单节点网络故障而脱离原有分区引起的。

        不过也需要考虑 2v2 、3v3 这种被分裂成对等节点数的分区的情况。所谓的2v2 这种对等分
区表示原有集群的组成为 [node1, node2, node3, node4] ,由于某种原因分裂成类似[node1, node2] 和 [node3 , node4] 这两个网络分区的情形。这种情况在跨机架部署时就有可能发生,当node1 和 node2 部署在机架A 上,而node3 和node4 部署在机架B 上, 那么有可能机架A 与机架B 之间网络的通断会造成对等分区的出现。

        在 4.3 节中只阐述了如何模拟网络分区, 并没有明确说明如何模拟对等的网络分区。可以在node1 和node2 上分别执行iptables 命令去封禁node3 和node4 的IP 。如果node1 、node2 和node3 、node4 处于不同的网段,那么也可以采用封禁网段的做法。更有甚者, 可以将node1 、
node2 部署到物理机A 上的两台虚拟机中, 然后将node3 、node4 部署到物理机B 上的两台虚拟机中,之后切断物理机A 与B 之间的通信即可。

        当对等分区出现时, 会关闭这些分区内的所有节点,对于前面的[node1,node2]和[node3,
node4] 的例子而言,这四个节点上的RabbitMQ 应用都会被关闭。只有等待网络恢复之后,才会自动启动所有的节点以求从网络分区中恢复。

4.6.2、pause-if-all-down 模式

        在pause-if-all-down 模式下, RabbitMQ 集群中的节点在和所配置的列表中的任何节点不能交互时才会关闭, 语法为{pause_if_all_down , [nodes1, ignore I autoheal]  ,其中[nodes] 为前面所说的列表,也可称之为受信节点列表。参考配置如下:

## pause_if_all_down strategy require additional configuration
cluster_partition_handling = pause_if_all_down

## Recover strategy. Can be either 'autoheal' or 'ignore'
cluster_partition_handling.pause_if_all_down.recover = ignore

## Node names to check
cluster_partition_handling.pause_if_all_down.nodes.1 = rabbit@localhost
cluster_partition_handling.pause_if_all_down.nodes.2 = hare@localhost

        如果一个节点与 rabbit@node1 节点无法通信时,则会关闭自身的RabbitMQ 应用。如果是rabbit@node1 本身发生了故障造成网络不可用,而其他节点都是正常的情况下, 这种规则会让所有的节点中RabbitMQ 应用都关闭,待rabbit@node 1 中的网络恢复之后,各个节点再启动自身应用以从网络分区中恢复。

        注意到 pause-if-all-down 模式下有 ignore 和autoheal 两种不同的配置。考虑前面 pause-minority 模式中提及的一种情形, node1 和node2 部署在机架A 上,而node3 和node4 部署在机架B 上。此时配置 {cluster_partition_handling , {pause_if_all_down, ['rabbit@nodel' , 'rabbit@node3 '] , ignore}} ,那么当机架A 和机架B 的通信出现异常时,由于node1 和node2 保持着通信, node3 和node4 保持着通信,这4 个节点都不会自行关闭,但是会形成两个分区,所以这样不能实现自动处理的功能。所以如果将配置中的 ignore 替换成 autoheal 就可以处理此种情形。

4.6.3、autoheal 模式

        在autoheal 模式下, 当认为发生网络分区时, RabbitMQ 会自动决定一个获胜(winning)的分区,然后重启不在这个分区中的节点来从网络分区中恢复。一个获胜的分区是指客户端连接最多的分区,如果产生一个平局,即有两个或者多个分区的客户端连接数一样多,那么节点数最多的一个分区就是获胜分区。如果此时节点数也一样多,将以节点名称的字典序来挑选获胜分区。

cluster_partition_handling = autoheal

        对于pause-minority 模式,关闭节点的状态是在网络故障时,也就是判定出net_tick_timeout之时,会关闭"少数派"分区中的节点,等待网络恢复之后,即判定出网络分区之后,启动关闭的节点来从网络分区中恢复。autoheal 模式在判定出net tick timeout 之时不做动作,要等到网络恢复之时,在判定出网络分区之后才会有相应的动作,即重启非获胜分区中的节点。

注意要点:在autoheal 模式下,如果集群中有节点处于非运行状态,那么当发生网络分区的时候,将不会有任何自动处理的动作

4.6.4、模式的选择

        有一点必须要清楚, 允许RabbitMQ 能够自动处理网络分区并不一定会有正面的成效,也有可能会带来更多的问题。网络分区会导致RabbitMQ 集群产生众多的问题,需要对遇到的问题做出一定的选择。就像本章开篇所说的,如果置RabbitMQ 于一个不可靠的网络环境下,需要使用Federation 或者Shovel。就算从网络分区中恢复了之后,也要谨防发生二次网络分区。

        每种模式都有自身的优缺点,没有哪种模式是万无一失的,希望根据实际情形做出相应的选择,下面简要概论以下4 个模式。

  • ignore 模式:发生网络分区时,不做任何动作,需要人工介入。
  • pause-minority 模式:对于对等分区的处理不够优雅,可能会关闭所有的节点。一般情况下,可应用于非跨机架、奇数节点数的集群中。
  • pause-if-all-down 模式:对于受信节点的选择尤为考究,尤其是在集群中所有节点硬件配置相同的情况下。此种模式可以处理对等分区的情形。
  • autoheal 模式:可以处于各个情形下的网络分区。但是如果集群中有节点处于非运行状态,则此种模式会失效。

5、高级扩展

5.1、消息追踪

5.1.1、Firehose

        在RabbitMQ 中可以使用Firehose 功能来实现消息追踪, Firehose 可以记录每一次发送或者消费消息的记录,方便RabbitMQ 的使用者进行调试、排错等。

        Firehose 的原理是将生产者投递给RabbitMQ 的消息,或者RabbitMQ 投递给消费者的消息按照指定的格式发送到默认的交换器上。这个默认的交换器的名称为 amq.rabbitmq.trace ,它是一个topic 类型的交换器。发送到这个交换器上的消息的路由键为 publish.{exchangename}  和deliver.{queuename} 。其中 exchangename 和 queuename 为交换器和队列的名称,分别对应生产者投递到交换器的消息和消费者从队列中获取的消息。

        开启 Firehose 命令:rabbitmqctl trace_on [-p vhost] 。其中[-p vhost] 是可选参数,用来指定虚拟主机vhost。对应的关闭命令为rabbitmqctl trace_off [-p vhost] 。Firehose 默认情况下处于关闭状态,并且 Firehose 的状态也是非持久化的,会在RabbitMQ 服务重启的时候还原成默认的状态。Firehose 开启之后多少会影响RabbitMQ 整体服务的性能,因为它会引起额外的消息生成、路由和存储。

        下面我们举例说明Firehose 的用法。需要做一下准备工作,确保Firehose 处于开启状态,创建7 个队列: queue 、queue.another 、queue1 、queue2 、queue3 、queue4 和queue5 。之后再创建2 个交换器 exchange 和 exchange.another,分别通过路由键 rk 和rk. another 与queue 和queue.another 进行绑定。最后将amq.rabbitmq.trace 这个关键的交换器与queuel 、queue2 、queue3 、queue4 和queue5 绑定,详细示意图可以参考:

结构示意图

        分别用客户端向exchange 和exchange.another 中发送一条消息"trace test payload." ,然后再用客户端消费队列queue 和queue.another 中的消息。

package com.blnp.net.rabbitmq.base;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.MessageProperties;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * <p></p>
 *
 * @author lyb 2045165565@qq.com
 * @createDate 2023/8/31 11:37
 */
public class RabbitProducer {

    private static final String EXCHANGE_NAME = "exchange.another";
    private static final String ROUTING_KEY = "rk.another";
    private static final String QUEUE_NAME = "queue_demo";
    private static final String IP_ADDRESS = "192.168.56.119";
    /**
     *  RabbitMQ 服务端默认端口号为5672
     **/
    private static final int PORT = 5672;

    public static void main(String[] args) throws IOException, TimeoutException {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost(IP_ADDRESS);
        factory.setPort(PORT);
        factory.setUsername("admin");
        factory.setPassword("admin@123");

        //创建mq连接
        Connection connection = factory.newConnection();
        //创建信道
        Channel channel = connection.createChannel();
        // 创建一个type="direct" 、持久化的、非自动删除的交换器
        channel.exchangeDeclare(EXCHANGE_NAME, "direct" , true , false , null);
        //创建一个持久化、非排他的、非自动删除的队列
//        channel. queueDeclare(QUEUE_NAME , true , false , false , null) ;
        //将交换器与队列通过路由键绑定
//        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME,ROUTING_KEY);
        //发送一条持久化的消息:
        String message = "trace test payload.";
        channel.basicPublish(EXCHANGE_NAME,ROUTING_KEY, MessageProperties.TEXT_PLAIN,message.getBytes());

        //关闭资源
        channel.close();
        connection.close();
    }
}

消息发送完并且未消费前的消息如下所示:

消息消费完成后:

package com.blnp.net.rabbitmq.base;

import com.alibaba.fastjson.JSONObject;
import com.rabbitmq.client.*;

import java.io.IOException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

/**
 * <p></p>
 *
 * @author lyb 2045165565@qq.com
 * @createDate 2023/8/31 11:52
 */
public class RabbitConsumer {

    private static final String QUEUE_NAME = "queue.another";
    private static final String IP_ADDRESS = "192.168.56.119";
    /**
     *  RabbitMQ 服务端默认端口号为5672
     **/
    private static final int PORT = 5672;

    public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
        Address[] addresses = {
                new Address(IP_ADDRESS, PORT)
        };

        ConnectionFactory factory = new ConnectionFactory();
        factory.setUsername("admin");
        factory.setPassword("admin@123");

        //创建mq连接
        Connection connection = factory.newConnection(addresses);
        //创建信道
        final Channel channel = connection.createChannel();
        // 设置客户端最多接收未被ack 的消息的个数
        channel.basicQos(64);

        //注意这里采用的是继承DefaultConsurner 的方式来实现消费, 有过RabbitMQ 使用经验
        //的读者也许会喜欢采用QueueingConsurner 的方式来实现消费, 但是并不推荐,使用
        //QueueingConsurner 会有一些隐患。同时, 在RabbitMQ Java 客户端4.0.0 版本开始将
        //QueueingConsurner 标记为@Deprecated , 在后面的大版本中会删除这个类,
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("[" + envelope.getDeliveryTag() + "]收到消息为 = " + new String(body)+"\n详细参数为:"+ JSONObject.toJSONString(properties));
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        };

        //消费指定队列的数据
        channel.basicConsume(QUEUE_NAME,consumer);
        //等待回调函数执行完毕之后, 关闭资源
        TimeUnit.SECONDS.sleep(5);
        channel.close();
        connection.close();
    }
}

由于我这边rabbitmq服务配置了联邦交换机策略的原因,所以实际消息数会翻一倍。

 

        此时queuel 中有2 条消息, queue2 中有2 条消息, queue3 中有4 条消息,而queue4 和queue5 中只有1 条消息。在向exchange 发送 1 条消息后, amq.rabbitmq.trace 分别向queue1 、queue3 和queue4 发送 1 条内部封装的消息。同样,在向exchange.another 中发送1 条消息之后,对应的队列queue1 和queue3 中会多l 条消息。消费队列queue 的时候, queue2 、queue3 和queue5中会多 1 条消息:消费队列queue.another 的时候, queue2 和queue3 会多1 条消息。"publish.#"匹配发送到所有交换器的消息, "deliver.#"匹配消费所有队列的消息,而"#"则包含了"publish.#"和"deliver.#"。

        在Firehose 开启状态下, 当有客户端发送或者消费消息时, Firehose 会自动封装相应的消息体,并添加详细的headers 属性。对于前面的将"trace test pay load. "这条消息发送到交换器exchange 来说, Firehore 会将其封装成如图所示的内容:

在消费 queue 时,会将消息封装成如图所示的内容:

未开启 Firehose 的消息封装格式如下所示:

        headers 中的 exchange_narne 表示发送此条消息的交换器; routing_keys 表示与exchange_narne 对应的路由键列表; properties 表示消息本身的属性,比如delivery_mode 设置为2 表示消息需要持久化处理。

5.1.2、rabbitmq_tracing 插件

        rabbitrnq_tracing 插件相当于Firehose 的GUI 版本, 它同样能跟踪RabbitMQ 中消息的流入流出情况。rabbitrnq_tracing 插件同样会对流入流出的消息进行封装, 然后将封装后的消息日志存入相应的trace 文件之中。可以使用rabbitrnq - plugins enable rabbitrnq_tracing 命令来启rabbitrnq
tracing 插件:

[rabbit@myblnp1 rabbitmq]$ rabbitmq-plugins enable rabbitmq_tracing
Enabling plugins on node rabbit@myblnp1:
rabbitmq_tracing
The following plugins have been configured:
  rabbitmq_federation
  rabbitmq_federation_management
  rabbitmq_management
  rabbitmq_management_agent
  rabbitmq_shovel
  rabbitmq_shovel_management
  rabbitmq_tracing
  rabbitmq_web_dispatch
Applying plugin configuration to rabbit@myblnp1...
The following plugins have been enabled:
  rabbitmq_tracing

started 2 plugins.
[rabbit@myblnp1 rabbitmq]$ 

        对应的关闭插件的命令是 rabbitrnq-plugins disable rabbitrnq_tracing 。

        在Web 管理界面 "Admin" 右侧原本只有" Users"、" Virtul Hosts " 和" Policies" 这个三
Tab 项, 在添加rabbitrnq_tracing 插件之后,会多出" Tracing " 这一项内容,如下所示:

        在添加完trace 之后,会根据匹配的规则将相应的消息日志输出到对应的trace 文件之中,文件的默认路径为 $HOME/var/tmp/rabbitmq-tracingo 可以在页面中直接点击"Trace log files"下面的列表直接查看对应的日志文件。如下图所示,添加了两个 trace 任务:

再添加完相应的trace 任务之后,会发现多了两个队列,如图所示:

        就以第一个队列 amq.gen-E9E_4gS55tArq7H7uV0eKw 而言,其所绑定的交换器就是amq.rabbitmq.trace

        由此可以看出整个rabbitmq_tracing 插件和Firehose 在实现上如出一辙,只不过rabbitmq_tracing 插件比 Firehose 多了一层GUI 的包装,更容易使用和管理。这里在补充下 trace 相关参数中Name 、Format 、Max payload bytes 、Patter口的具体含义。

  • Name ,顾名思义,就是为即将创建的trace 任务取个名称。
  • Format 表示输出的消息日志格式,有Text 和JSON 两种, Text 格式的日志方便人类阅读,JSON 的格式方便程序解析。

Text 格式的消息日志参考如下:

================================================================================
2023-11-06 9:43:44:000: Message published

Node:         rabbit@myblnp1
Connection:   192.168.56.1:53920 -> 192.168.56.119:5672
Virtual host: /
User:         admin
Channel:      1
Exchange:     exchange.another
Routing keys: [<<"rk.another">>]
Routed queues: [<<"federation: exchange.another -> rabbit@myblnp1">>,
                <<"queue.another">>]
Properties:   [{<<"priority">>,signedint,0},
               {<<"delivery_mode">>,signedint,1},
               {<<"content_type">>,longstr,<<"text/plain">>}]
Payload: 
trace test

================================================================================
2023-11-06 9:43:44:000: Message received

Node:         rabbit@myblnp1
Connection:   192.168.56.119:59438 -> 192.168.56.119:5672
Virtual host: /
User:         root
Channel:      1
Exchange:     exchange.another
Routing keys: [<<"rk.another">>]
Queue:        federation: exchange.another -> rabbit@myblnp1
Properties:   [{<<"priority">>,signedint,0},
               {<<"delivery_mode">>,signedint,1},
               {<<"content_type">>,longstr,<<"text/plain">>}]
Payload: 
trace test

================================================================================
2023-11-06 9:43:44:001: Message published

Node:         rabbit@myblnp1
Connection:   <rabbit@myblnp1.1699237032.24938.0>
Virtual host: /
User:         none
Channel:      1
Exchange:     exchange.another
Routing keys: [<<"rk.another">>]
Routed queues: [<<"queue.another">>]
Properties:   [{<<"priority">>,signedint,0},
               {<<"delivery_mode">>,signedint,1},
               {<<"headers">>,table,
                [{<<"x-received-from">>,array,
                  [{table,[{<<"uri">>,longstr,
                            <<"amqp://192.168.56.119:5672">>},
                           {<<"exchange">>,longstr,<<"exchange.another">>},
                           {<<"redelivered">>,bool,false},
                           {<<"cluster-name">>,longstr,<<"rabbit@myblnp1">>},
                           {<<"vhost">>,longstr,<<"/">>}]}]}]},
               {<<"content_type">>,longstr,<<"text/plain">>}]
Payload: 
trace test

JSON 格式的消息日志参考如下:

(一条消息记录为一行JSON)

{
	"channel": 1,
	"connection": "192.168.56.1:53920 -> 192.168.56.119:5672",
	"exchange": "exchange.another",
	"node": "rabbit@myblnp1",
	"payload": "dHJhY2UgdGVzdA==",
	"properties": {
		"content_type": "text/plain",
		"delivery_mode": 1,
		"priority": 0
	},
	"queue": "none",
	"routed_queues": [
		"federation: exchange.another -> rabbit@myblnp1",
		"queue.another"
	],
	"routing_keys": [
		"rk.another"
	],
	"timestamp": "2023-11-06 9:43:44:000",
	"type": "published",
	"user": "admin",
	"vhost": "/"
}
{
	"channel": 1,
	"connection": "192.168.56.119:59438 -> 192.168.56.119:5672",
	"exchange": "exchange.another",
	"node": "rabbit@myblnp1",
	"payload": "dHJhY2UgdGVzdA==",
	"properties": {
		"content_type": "text/plain",
		"delivery_mode": 1,
		"priority": 0
	},
	"queue": "federation: exchange.another -> rabbit@myblnp1",
	"routed_queues": "none",
	"routing_keys": [
		"rk.another"
	],
	"timestamp": "2023-11-06 9:43:44:000",
	"type": "received",
	"user": "root",
	"vhost": "/"
}
{
	"channel": 1,
	"connection": "<rabbit@myblnp1.1699237032.24938.0>",
	"exchange": "exchange.another",
	"node": "rabbit@myblnp1",
	"payload": "dHJhY2UgdGVzdA==",
	"properties": {
		"content_type": "text/plain",
		"delivery_mode": 1,
		"headers": {
			"x-received-from": [
				{
					"cluster-name": "rabbit@myblnp1",
					"exchange": "exchange.another",
					"redelivered": false,
					"uri": "amqp://192.168.56.119:5672",
					"vhost": "/"
				}
			]
		},
		"priority": 0
	},
	"queue": "none",
	"routed_queues": [
		"queue.another"
	],
	"routing_keys": [
		"rk.another"
	],
	"timestamp": "2023-11-06 9:43:44:001",
	"type": "published",
	"user": "none",
	"vhost": "/"
}

        JSON 格式的 payload (消息体) 默认会采用Base64 进行编码,如上面的" trace test payload."
会被编码成 " dHJhY2UgdGVzdA=="。

  • Max payload bytes 表示每条消息的最大限制,单位为B 。比如设置了此值为10 ,那么当有超过10B 的消息经过RabbitMQ 流转时, 在记录到trace 文件时会被截断。如上Text 日志格式中" trace test payload." 会被截断成"trace test " 。
  • Pattern 用来设置匹配的模式, 和Firehose 的类似。如"#"匹配所有消息流入流出的情况,即当有客户端生产消息或者消费消息的时候, 会把相应的消息日志都记录下来; " publish.#"匹配所有消息流入的情况; "deliver.#"匹配所有消息流出的情况。

5.1.3、案例:可靠性检测

        当生产者将消息发送到交换器时,实际上是由生产者所连接的信道将消息上的路由键同交换器的绑定列表比较,之后再路由消息到相应的队列进程中。那么在信道比对完绑定列表之后,将消息路由到队列并且保存的过程中,是否会由于RabbitMQ 的内部缺陷而引起偶然性的消息丢失?如果你对此也有同样的疑问,就可以使用RabbitMQ 的消息追踪机制来验证这一情况。大致思路: 一个交换器通过同一个路由键绑定多个队列,生产者客户端采用同一个路由键发送消息到这个交换器中,检测其所绑定的队列中是否有消息丢失。

1、准备工作

  1. 在RabbitMQ 集群开启rabbitmq_tracing插件。
  2. 创建 1 个交换器exchange 和3 个队列: queue1 、queue2 、queue3 ,都用同一个路由键"rk" 进行绑定。
  3. 创建3 个trace: trace 1 、trace2 、trace3 分别,采用"#.queue1"、"#.queue2"、"#.queue3"
    的 Pattern 来追踪从队列queue1 、queue2 、queue3 中消费的消息。
  4. 创建 1 个trace: trace_publish 采用 publish.exchange 的Pattern 来追踪流入交换器exchange 的消息。

2、验证过程

(完整的测试代码,请查看后文)

        第一步,开启 1 个生产者线程,然后持续发送消息至交换器exchange 。消息的格式为"当前时间戳+自增计数",如" 1506067447530-726",这样在检索到相应数据丢失时可以快速在trace日志中找到大致的地方。注意设置mandatory 参数,防止消息路由不到对应的队列而造成对消息丢失的误判。在消息发送之前需要将消息以 [msg , QUEUE_NUM] 的形式存入一个全局的 msgMap 中,用来在消费端做数据验证。这里的QUEUE_NUM 为3 ,对应创建的3 个队列。

ProducerThread(消息生产线程示例代码)

private static HashMap<String, Integer> msgMap = new HashMap<String, Integer> ();
    /**
     *  记录程序日志
     **/
    private static BlockingQueue<String> log2disk = new LinkedBlockingQueue<String>() ;
    /**
     *  交换机绑定队列数
     **/
    private static Integer QUEUE_NUM = 3;
    private static final String IP_ADDRESS = "192.168.56.119";
    private static final int PORT = 5672;

    /**
     * 用途:消息生产线程
     * @author liaoyibin
     * @date 10:27 2023/11/7
     * @params
     **/
    public static class ProducerThread implements Runnable {
        private Connection connection ;

        public ProducerThread(Connection connection) {
            this.connection = connection;
        }

        @Override
        public void run() {
            try {
                Channel channel = connection.createChannel();
                //添加消费监听
                channel.addReturnListener(new ReturnListener() {
                    @Override
                    public void handleReturn(int replyCode, String replyText, String exchange, String routingKey,
                                             AMQP.BasicProperties basicProperties, byte[] body) throws IOException {
                        //返回消息
                        String errorInfo = "Basic.Return:" + new String(body) + "\n";
                        try {
                            log2disk.put(errorInfo);
//                            log.info(errorInfo);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println("errorInfo = " + errorInfo);
                    }
                });

                int count = 0;

                while (true) {
                    String message = System.currentTimeMillis() + "-" + count++;
                    synchronized (msgMap) {
                        msgMap.put(message,QUEUE_NUM);
                    }
                    channel.basicPublish("exchange","rk",true, MessageProperties.PERSISTENT_TEXT_PLAIN,message.getBytes());

                    try {
                        TimeUnit.MILLISECONDS.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

        注意代码里的存储消息的动作一定要在发送消息之前,如果在上述代码清单中调换顺序,生产者线程在发送完消息之后,并抢占到msgMap 的对象锁之前, 消费者就有可能消费到相应的数据, 此时msgMap 中并没有相应的消息,这样会误报错误。

//错误示例:
channel.basicPublish("exchange","rk",true, MessageProperties.PERSISTENT_TEXT_PLAIN,message.getBytes());

synchronized (msgMap) {
    msgMap.put(message,QUEUE_NUM);
 }

        第二步,开启3 个消费者线程分别消费队列queue1 、queue2 、queue3 的消息,从存储的msgMap 中寻找是否有相应的消息。如果有,则将消息对应的value 计数减1, 如果value 计数为0 ,则从Map 中删除此条消息: 如果没有找到这条消息则报错。

ConsumerThread(消息消费者线程示例代码)

public static class ConsumerThread implements Runnable {
        private Connection connection;
        private String queue;

        public ConsumerThread(Connection connection, String queue) {
            this.connection = connection;
            this.queue = queue;
        }

        @Override
        public void run() {
            try {
                final Channel channel = connection.createChannel();
                channel.basicQos(64);
                channel.basicConsume(this.queue,new DefaultConsumer(channel) {
                    @Override
                    public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                        String msg = new String(body);
                        synchronized (msgMap) {
                            if (msgMap.containsKey(msg)) {
                                Integer count = msgMap.get(msg);
                                count--;
                                if (count > 0) {
                                    msgMap.put(msg,count);
                                }else {
                                    msgMap.remove(msg);
                                }
                            }else {
                                String errorInfo = "unknown msg:" + msg + "\n";
                                try {
                                    log2disk.put(errorInfo);
                                    System.out.println("unknown errorInfo = " + errorInfo);
                                } catch (InterruptedException e) {
                                    e.printStackTrace();
                                }
                            }
                        }
                        channel.basicAck(envelope.getDeliveryTag(),false);
                    }
                });

            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

        第三步,开启一个检测进程,每隔10 分钟检测msgMap 中的数据。由前面的描述可知msgMap 中的键就是消息,而消息中有时间戳的信息,那么可以将这个时间戳与当前的时间戳进行对比,如果发现差值超过10 分钟,这说明可能有消息丢失。这个结论的前提是队列中基本没有堆积,并且前面的生产和消费代码同时运行时可以保证消费消息的速度不会低于生产消息的速度。

DetectionThread(消息一致性检查程序)

public static class DetectionThread implements Runnable {

        @Override
        public void run() {
            while (true) {
                try {
                    TimeUnit.MINUTES.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (msgMap) {
                    if (msgMap.size() > 0) {
                        long now = System.currentTimeMillis();
                        for(Map.Entry<String, Integer> entry : msgMap.entrySet()) {
                            String msg = entry.getKey();
                            if ((now - parseTime(msg)) >= 10*60*1000) {
                                String findLossInfo = "We find loss msg:" + msg + ",now the time is:" + now +
                                        ",and this msg still has " + entry.getValue() + " queue missed \n";
                                try {
                                    log2disk.put(findLossInfo);
                                    System.out.println("findLossInfo = " + findLossInfo);
                                    msgMap.remove(msg);
                                } catch (InterruptedException e) {
                                    e.printStackTrace();
                                }
                            }
                        }
                    }
                }
            }
        }

        public static Long parseTime(String msg) {
            int index = msg.indexOf("-");
            String timeStr = msg.substring(0, index);
            return Long.parseLong(timeStr);
        }
    }

        如果检测到msgMap 中有消息超过10 分钟没有被处理,此时还不能证明有数据丢失, 这里就需要用到了trace 。如果看到 [msg,count] 这条数据中有以下情况:

  • 考虑count=3 的情况。需要检索 trace 文件 trace_publish .log 来进一步验证。如果trace_publish.log 中没有搜索到相应的消息则说明消息未发生到交换器exchange 中;如果trace_publish . log 中检索到相应的消息,那么可以进一步检索 trace1.log、trace2 .log 和trace3 .log 来进行验证,如果这3 个trace 文件中不是全部都有此条消息,则验证了本节开头所述的消息丢失现象。
  • 考虑 0<count<3 的情况。需要检索 trace1.log、trace2.log 和 trace3.log 来进一步验证,如果这3 个 trace 文件中不是全部都有此条消息, 则验证了本节开篇所述的消息丢失问题。
  • 考虑count=Q 的情况。说明检测程序异常,可以忽略。

完整代码

package com.blnp.net.rabbitmq.base;

import com.rabbitmq.client.*;
import lombok.extern.slf4j.Slf4j;

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.*;

/**
 * <p></p>
 *
 * @author lyb 2045165565@qq.com
 * @createDate 2023/11/7 9:57
 */
@Slf4j
public class DetectionRabbitmq {
    private static HashMap<String, Integer> msgMap = new HashMap<String, Integer> ();
    /**
     *  记录程序日志
     **/
    private static BlockingQueue<String> log2disk = new LinkedBlockingQueue<String>() ;
    /**
     *  交换机绑定队列数
     **/
    private static Integer QUEUE_NUM = 3;
    private static final String IP_ADDRESS = "192.168.56.119";
    private static final int PORT = 5672;

    /**
     * 用途:消息生产线程
     * @author liaoyibin
     * @date 10:27 2023/11/7
     * @params
     **/
    public static class ProducerThread implements Runnable {
        private Connection connection ;

        public ProducerThread(Connection connection) {
            this.connection = connection;
        }

        @Override
        public void run() {
            try {
                Channel channel = connection.createChannel();
                //添加消费监听
                channel.addReturnListener(new ReturnListener() {
                    @Override
                    public void handleReturn(int replyCode, String replyText, String exchange, String routingKey,
                                             AMQP.BasicProperties basicProperties, byte[] body) throws IOException {
                        //返回消息
                        String errorInfo = "Basic.Return:" + new String(body) + "\n";
                        try {
                            log2disk.put(errorInfo);
//                            log.info(errorInfo);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println("errorInfo = " + errorInfo);
                    }
                });

                int count = 0;

                while (true) {
                    String message = System.currentTimeMillis() + "-" + count++;
                    synchronized (msgMap) {
                        msgMap.put(message,QUEUE_NUM);
                    }
                    channel.basicPublish("exchange","rk",true, MessageProperties.PERSISTENT_TEXT_PLAIN,message.getBytes());

                    try {
                        TimeUnit.MILLISECONDS.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 用途:消息消费线程
     * @author liaoyibin
     * @date 10:27 2023/11/7
     * @params
     **/
    public static class ConsumerThread implements Runnable {
        private Connection connection;
        private String queue;

        public ConsumerThread(Connection connection, String queue) {
            this.connection = connection;
            this.queue = queue;
        }

        @Override
        public void run() {
            try {
                final Channel channel = connection.createChannel();
                channel.basicQos(64);
                channel.basicConsume(this.queue,new DefaultConsumer(channel) {
                    @Override
                    public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                        String msg = new String(body);
                        synchronized (msgMap) {
                            if (msgMap.containsKey(msg)) {
                                Integer count = msgMap.get(msg);
                                count--;
                                if (count > 0) {
                                    msgMap.put(msg,count);
                                }else {
                                    msgMap.remove(msg);
                                }
                            }else {
                                String errorInfo = "unknown msg:" + msg + "\n";
                                try {
                                    log2disk.put(errorInfo);
                                    System.out.println("unknown errorInfo = " + errorInfo);
                                } catch (InterruptedException e) {
                                    e.printStackTrace();
                                }
                            }
                        }
                        channel.basicAck(envelope.getDeliveryTag(),false);
                    }
                });

            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static class DetectionThread implements Runnable {

        @Override
        public void run() {
            while (true) {
                try {
                    TimeUnit.MINUTES.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (msgMap) {
                    if (msgMap.size() > 0) {
                        long now = System.currentTimeMillis();
                        for(Map.Entry<String, Integer> entry : msgMap.entrySet()) {
                            String msg = entry.getKey();
                            if ((now - parseTime(msg)) >= 10*60*1000) {
                                String findLossInfo = "We find loss msg:" + msg + ",now the time is:" + now +
                                        ",and this msg still has " + entry.getValue() + " queue missed \n";
                                try {
                                    log2disk.put(findLossInfo);
                                    System.out.println("findLossInfo = " + findLossInfo);
                                    msgMap.remove(msg);
                                } catch (InterruptedException e) {
                                    e.printStackTrace();
                                }
                            }
                        }
                    }
                }
            }
        }

        public static Long parseTime(String msg) {
            int index = msg.indexOf("-");
            String timeStr = msg.substring(0, index);
            return Long.parseLong(timeStr);
        }
    }

    /**
     * 用途:日志输出文件
     * @author liaoyibin
     * @date 11:54 2023/11/7
     * @params
     **/
    public static class PrintLogThread implements Runnable {
        private String filePath;

        public PrintLogThread(String filePath) {
            this.filePath = filePath;
        }

        @Override
        public void run() {
            try {
                FileWriter fileWriter = new FileWriter(this.filePath);
                // 使用FileWriter对象创建BufferedWriter对象
                BufferedWriter buffer = new BufferedWriter(fileWriter);
                while (true) {
                    System.out.println("@@buffer = " + log2disk.take());
                    buffer.append(log2disk.take());
                    buffer.flush();
                }
            } catch (IOException | InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws IOException, TimeoutException {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost(IP_ADDRESS);
        factory.setPort(PORT);
        factory.setUsername("admin");
        factory.setPassword("admin@123");
        //创建mq连接
        Connection connection = factory.newConnection();

        //实时日志输出
        PrintLogThread printLogThread = new PrintLogThread("output.log");

        //指定消息生产者
        ProducerThread producerThread = new ProducerThread(connection);

        //指定多个消息消费者
        ConsumerThread consumerThread1 = new ConsumerThread(connection, "queue1");
        ConsumerThread consumerThread2 = new ConsumerThread(connection, "queue2");
        ConsumerThread consumerThread3 = new ConsumerThread(connection, "queue3");

        //指定消息检测
        DetectionThread detectionThread = new DetectionThread();
        System.out.println("starting check msg loss ………………");

        //配置线程池
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.submit(printLogThread);
        executorService.submit(producerThread);
        executorService.submit(consumerThread1);
        executorService.submit(consumerThread2);
        executorService.submit(consumerThread3);
        executorService.submit(detectionThread);
        executorService.shutdown();
    }
}

使用Maven 依赖:

<dependencies>
        <dependency>
            <groupId>com.bbossgroups.plugins</groupId>
            <artifactId>bboss-datatran-jdbc</artifactId>
            <version>7.0.8</version>
        </dependency>

        <dependency>
            <groupId>com.bbossgroups.plugins</groupId>
            <artifactId>bboss-elasticsearch-spring-boot-starter</artifactId>
            <version>7.0.8</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>com.rabbitmq</groupId>
            <artifactId>amqp-client</artifactId>
            <version>4.2.1</version>
        </dependency>

        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.5.13</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.79</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.16</version>
        </dependency>
    </dependencies>

5.2、负载均衡

        面对大量业务访问、高并发请求,可以使用高性能的服务器来提升RabbitMQ 服务的负载能力。当单机容量达到极限时,可以采取集群的策略来对负载能力做进一步的提升,但这里还存在一个负载不均衡的问题。试想如果一个集群中有3 个节点,那么所有的客户端都与其中的单个节点node1 建立TCP 连接,那么node1 的网络负载必然会大大增加而显得难以承受,其他节点又由于没有那么多的负载而造成硬件资源的浪费,所以负载均衡显得尤为重要。

        对于RabbitMQ 而言,客户端与集群建立的TCP 连接不是与集群中所有的节点建立连接,而是挑选其中一个节点建立连接如下图所示,在引入了负载均衡之后,各个客户端的连接可以分摊到集群的各个节点之中,进而避免了前面所讨论的缺陷。

引入负载均衡

        负载均衡(Load balance ) 是一种计算机网络技术,用于在多个计算机(计算机集群〉、网络连接、CPU、磁盘驱动器或其他资源中分配负载,以达到最佳资源使用、最大化吞吐率、最小响应时间及避免过载的目的。使用带有负载均衡的多个服务器组件,取代单一的组件,可以通过冗余提高可靠性。

        负载均衡通常分为软件负载均衡和硬件负载均衡两种。

        软件负载均衡是指在一个或者多个交互的网络系统中的多台服务器上安装一个或多个相应的负载均衡软件来实现的一种均衡负载技术。软件可以很方便地安装在服务器上,并且实现一定的均衡负载功能。软件负载均衡技术配置简单、操作也方便,最重要的是成本很低。

        硬件负载均衡是指在多台服务器间安装相应的负载均衡设备,也就是负载均衡器(如F5 )来完成均衡负载技术,与软件负载均衡技术相比,能达到更好的负载均衡效果。由于硬件负载均衡技术需要额外增加负载均衡器,成本比较高,所以适用于流量高的大型网站系统。

5.2.1、客户端内部实现

        对于RabbitMQ 而言可以在客户端连接时简单地使用负载均衡算法来实现负载均衡。负载均衡算法有很多种,主流的有以下几种。

1、轮询法

        将请求按顺序轮流地分配到后端服务器上,它均衡地对待后端的每一台服务器, 而不关心服务器实际的连接数和当前的系统负载。

        如果多个客户端需要连接到这个有3 个节点的RabbitMQ 集群,可以调用RoundRobin.getConnectionAddress() 来获取相应的连接地址。

package com.blnp.net.rabbitmq.other;

import java.util.ArrayList;
import java.util.List;

/**
 * <p>客户端负载均衡-轮询法</p>
 *
 * @author lyb 2045165565@qq.com
 * @createDate 2023/11/7 14:32
 */
public class RoundRobin {
    
    /**
     *  RabbitMQ 集群各节点地址
     **/
    private static List<String> nodeList = new ArrayList<>() {
        {
            add("192.168.56.119");
            add("192.168.56.120");
            add("192.168.56.121");
        }
    };
    private static Integer pos = 0;
    private static final Object lock = new Object();
    
    public static String getConnectionAddress() {
        String ip = null;
        synchronized (lock) {
            ip = nodeList.get(pos);
            if (++pos >= nodeList.size()) {
                pos = 0;
            }
        }
        return ip;
    }
}
2、加权轮询法

        不同的后端服务器的配置可能和当前系统的负载并不相同,因此它们的抗压能力也不相同。给配置高、负载低的机器配置更高的权重,让其处理更多的请求;而配置低、负载高的集群,给其分配较低的权重,降低其系统负载,加权轮询能很好地处理这一问题,并将请求顺序和权重分配到后端。

3、随机法

        通过随机算法,根据后端服务器的列表大小值来随机选取其中的一台服务器进行访问。由概率统计理论可以得知,随着客户端调用服务端的次数增多,其实际效果越来越接近于平均分配调用量到后端的每一台服务器,也就是轮询的结果。

package com.blnp.net.rabbitmq.other;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

/**
 * <p>客户端负载均衡-随机法</p>
 *
 * @author lyb 2045165565@qq.com
 * @createDate 2023/11/7 14:40
 */
public class RandomAccess {

    /**
     *  RabbitMQ 集群各节点地址
     **/
    private static List<String> nodeList = new ArrayList<String>() {
        {
            add("192.168.56.119");
            add("192.168.56.120");
            add("192.168.56.121");
        }
    };
    
    public static String getConnectionAddress() {
        Random random = new Random();
        int pos = random.nextInt(nodeList.size());
        return nodeList.get(pos);
    }
}
4、加权随机法

        与加权轮询法一样,加权随机法也根据后端机器的配置、系统的负载分配不同权重。不同的是,它按照权重随机请求后端服务器,而非顺序。

5、源地址哈希法

        源地址哈希的思想是根据获取的客户端IP 地址,通过啥希函数计算得到面一个数值,用该数值对服务器列表的大小进行取模运算,得到的结果便是客户端要访问服务器的序号。采用源地址哈希法进行负载均衡,同一IP 地址的客户端,当后端服务器列表不变时,它每次都会映射到同一台后端服务器进行访问。

package com.blnp.net.rabbitmq.other;

import lombok.SneakyThrows;

import java.net.InetAddress;
import java.util.ArrayList;
import java.util.List;

/**
 * <p>客户端负载均衡-源地址哈希法</p>
 *
 * @author lyb 2045165565@qq.com
 * @createDate 2023/11/7 14:43
 */
public class IpHash {
    /**
     *  RabbitMQ 集群各节点地址
     **/
    private static List<String> nodeList = new ArrayList<String>() {
        {
            add("192.168.56.119");
            add("192.168.56.120");
            add("192.168.56.121");
        }
    };
    
    @SneakyThrows
    public static String getConnectionAddress() {
        int ipHashcode = InetAddress.getLocalHost().getHostAddress().hashCode();
        int pos = ipHashcode % nodeList.size();
        return nodeList.get(pos);
    }
}
6、最小连接数法

        最小连接数算法比较灵活和智能, 由于后端服务器的配置不尽相同,对于请求的处理有快有慢,它根据后端服务器当前的连接情况,动态地选取其中当前积压连接数最少的一台服务器来处理当前的请求,尽可能地提高后端服务的利用效率,将负载合理地分流到每一台服务器。

5.2.2、使用 HAProxy 实现

        HAProxy 提供高可用性、负载均衡及基于 TCP 和 HTTP 应用的代理,支持虚拟主机,它是免费、快速并且可靠的一种解决方案,包括Twitter、Reddit 、StackOverflow 、GitHub 在内的多家知名互联网公司在使用。HAProxy 实现了一种事件驱动、单一进程模型,此模型支持非常大的井发连接数。

1、安装 HAProxy

        首先需要去 HAProxy 的官网下载 HAProxy 的安装文件,目前最新的版本为
haproxy-2.8.3.tar.gz 。下载地址 或 点击这里

[rabbit@myblnp1 soft]$ wget https://src.fedoraproject.org/repo/pkgs/haproxy/haproxy-2.0.0.tar.gz/sha512/c5beb85522733a6df9741d8a6369cb4e476d6a49aa55163ea61b442715a6893aa7c5063221ff1c36c80ee124653b597499754bdd5764e306a755216b3af1908e/haproxy-2.0.0.tar.gz
--2023-11-07 15:14:59--  https://src.fedoraproject.org/repo/pkgs/haproxy/haproxy-2.0.0.tar.gz/sha512/c5beb85522733a6df9741d8a6369cb4e476d6a49aa55163ea61b442715a6893aa7c5063221ff1c36c80ee124653b597499754bdd5764e306a755216b3af1908e/haproxy-2.0.0.tar.gz
正在解析主机 src.fedoraproject.org (src.fedoraproject.org)... 38.145.60.21, 38.145.60.20
正在连接 src.fedoraproject.org (src.fedoraproject.org)|38.145.60.21|:443... 已连接。
已发出 HTTP 请求,正在等待回应... 200 OK
长度:2527523 (2.4M) [application/x-gzip]
正在保存至: “haproxy-2.0.0.tar.gz”

100%[=======================================================================================================================================================================================================>] 2,527,523    597KB/s 用时 4.1s   

2023-11-07 15:15:04 (597 KB/s) - 已保存 “haproxy-2.0.0.tar.gz” [2527523/2527523])

[rabbit@myblnp1 soft]$ ls
haproxy-2.0.0.tar.gz
[rabbit@myblnp1 soft]$ yum -y install make gcc pcre-devel bzip2-devel openssl-devel systemd-devel
已加载插件:fastestmirror
您需要 root 权限执行此命令。
[rabbit@myblnp1 soft]$ sudo yum -y install make gcc pcre-devel bzip2-devel openssl-devel systemd-devel
已加载插件:fastestmirror
Determining fastest mirrors
 * base: mirrors.aliyun.com
 * extras: mirrors.aliyun.com
 * updates: mirrors.aliyun.com
base                                                                                                                                                                                                                      | 3.6 kB  00:00:00     
extras                                                                                                                                                                                                                    | 2.9 kB  00:00:00     
updates                                                                                                                                                                                                                   | 2.9 kB  00:00:00     
软件包 1:make-3.82-24.el7.x86_64 已安装并且是最新版本
软件包 gcc-4.8.5-44.el7.x86_64 已安装并且是最新版本
软件包 pcre-devel-8.32-17.el7.x86_64 已安装并且是最新版本
软件包 1:openssl-devel-1.0.2k-26.el7_9.x86_64 已安装并且是最新版本
正在解决依赖关系
--> 正在检查事务
---> 软件包 bzip2-devel.x86_64.0.1.0.6-13.el7 将被 安装
---> 软件包 systemd-devel.x86_64.0.219-78.el7_9.7 将被 安装
--> 解决依赖关系完成

依赖关系解决

=================================================================================================================================================================================================================================================
 Package                                                      架构                                                  版本                                                            源                                                      大小
=================================================================================================================================================================================================================================================
正在安装:
 bzip2-devel                                                  x86_64                                                1.0.6-13.el7                                                    base                                                   218 k
 systemd-devel                                                x86_64                                                219-78.el7_9.7                                                  updates                                                216 k

事务概要
=================================================================================================================================================================================================================================================
安装  2 软件包

总下载量:434 k
安装大小:728 k
Downloading packages:
(1/2): bzip2-devel-1.0.6-13.el7.x86_64.rpm                                                                                                                                                                                | 218 kB  00:00:00     
(2/2): systemd-devel-219-78.el7_9.7.x86_64.rpm                                                                                                                                                                            | 216 kB  00:00:00     
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
总计                                                                                                                                                                                                             832 kB/s | 434 kB  00:00:00     
Running transaction check
Running transaction test
Transaction test succeeded
Running transaction
  正在安装    : systemd-devel-219-78.el7_9.7.x86_64                                                                                                                                                                                          1/2 
  正在安装    : bzip2-devel-1.0.6-13.el7.x86_64                                                                                                                                                                                              2/2 
  验证中      : bzip2-devel-1.0.6-13.el7.x86_64                                                                                                                                                                                              1/2 
  验证中      : systemd-devel-219-78.el7_9.7.x86_64                                                                                                                                                                                          2/2 

已安装:
  bzip2-devel.x86_64 0:1.0.6-13.el7                                                                                     systemd-devel.x86_64 0:219-78.el7_9.7                                                                                    

完毕!
[rabbit@myblnp1 soft]$ 

        执行解压并编译安装:

#解压下载好的软件包
[rabbit@myblnp1 soft]$ tar xf haproxy-2.0.0.tar.gz 
[rabbit@myblnp1 soft]$ ls
haproxy-2.0.0  haproxy-2.0.0.tar.gz
[rabbit@myblnp1 soft]$ 
#编译安装
[rabbit@myblnp1 soft]$ 
[rabbit@myblnp1 soft]$ cd haproxy-2.0.0
[rabbit@myblnp1 haproxy-2.0.0]$ ls
BRANCHES  CHANGELOG  contrib  CONTRIBUTING  doc  ebtree  examples  include  INSTALL  LICENSE  MAINTAINERS  Makefile  README  reg-tests  ROADMAP  scripts  src  SUBVERS  tests  VERDATE  VERSION
[rabbit@myblnp1 haproxy-2.0.0]$ make clean
[rabbit@myblnp1 haproxy-2.0.0]$ make -j $(grep 'processor' /proc/cpuinfo |wc -l) \
> TARGET=linux-glibc \
> USE_OPENSSL=1 \
> USE_ZLIB=1 \
> USE_PCRE=1 \
> USE_SYSTEMD=1
  CC      src/ev_poll.o
  CC      src/ev_epoll.o
  CC      src/ssl_sock.o
  CC      src/namespace.o
  CC      src/proto_http.o
  CC      src/cfgparse-listen.o
  CC      src/proto_htx.o
  CC      src/stream.o
  CC      src/mux_h2.o
  CC      src/stats.o
  CC      src/flt_spoe.o
  CC      src/server.o
  CC      src/checks.o
  CC      src/haproxy.o
  CC      src/cfgparse.o
  CC      src/flt_http_comp.o
  CC      src/http_fetch.o
  CC      src/dns.o
  CC      src/stick_table.o
  CC      src/mux_h1.o
  CC      src/peers.o
  CC      src/standard.o
  CC      src/proxy.o
  CC      src/cli.o
  CC      src/log.o
  CC      src/backend.o
  CC      src/pattern.o
  CC      src/sample.o
  CC      src/stream_interface.o
  CC      src/proto_tcp.o
  CC      src/listener.o
  CC      src/h1.o
  CC      src/cfgparse-global.o
  CC      src/cache.o
  CC      src/http_rules.o
  CC      src/http_act.o
  CC      src/tcp_rules.o
  CC      src/filters.o
  CC      src/connection.o
  CC      src/session.o
  CC      src/acl.o
  CC      src/vars.o
  CC      src/raw_sock.o
  CC      src/map.o
  CC      src/proto_uxst.o
  CC      src/payload.o
  CC      src/fd.o
  CC      src/queue.o
  CC      src/flt_trace.o
  CC      src/task.o
  CC      src/lb_chash.o
  CC      src/frontend.o
  CC      src/applet.o
  CC      src/mux_pt.o
  CC      src/signal.o
  CC      src/ev_select.o
  CC      src/proto_sockpair.o
  CC      src/compression.o
  CC      src/http_conv.o
  CC      src/memory.o
  CC      src/lb_fwrr.o
  CC      src/channel.o
  CC      src/htx.o
  CC      src/uri_auth.o
  CC      src/regex.o
  CC      src/chunk.o
  CC      src/pipe.o
  CC      src/lb_fas.o
  CC      src/lb_map.o
  CC      src/lb_fwlc.o
  CC      src/auth.o
  CC      src/time.o
  CC      src/hathreads.o
  CC      src/http_htx.o
  CC      src/buffer.o
  CC      src/hpack-tbl.o
  CC      src/shctx.o
  CC      src/sha1.o
  CC      src/http.o
  CC      src/hpack-dec.o
  CC      src/action.o
  CC      src/proto_udp.o
  CC      src/http_acl.o
  CC      src/xxhash.o
  CC      src/hpack-enc.o
  CC      src/h2.o
  CC      src/freq_ctr.o
  CC      src/lru.o
  CC      src/protocol.o
  CC      src/arg.o
  CC      src/hpack-huff.o
  CC      src/hdr_idx.o
  CC      src/base64.o
  CC      src/hash.o
  CC      src/mailers.o
  CC      src/activity.o
  CC      src/http_msg.o
  CC      src/version.o
  CC      src/mworker.o
  CC      src/mworker-prog.o
  CC      src/debug.o
  CC      src/wdt.o
  CC      src/dict.o
  CC      src/xprt_handshake.o
  CC      ebtree/ebtree.o
  CC      ebtree/eb32sctree.o
  CC      ebtree/eb32tree.o
  CC      ebtree/eb64tree.o
  CC      ebtree/ebmbtree.o
  CC      ebtree/ebsttree.o
  CC      ebtree/ebimtree.o
  CC      ebtree/ebistree.o
  LD      haproxy
[rabbit@myblnp1 haproxy-2.0.0]$ 
#编译安装并指定安装目录
[rabbit@myblnp1 haproxy-2.0.0]$ clear
[rabbit@myblnp1 haproxy-2.0.0]$ sudo make install PREFIX=/usr/local/haproxy
install: 正在创建目录"/usr/local/haproxy"
install: 正在创建目录"/usr/local/haproxy/sbin"
"haproxy" -> "/usr/local/haproxy/sbin/haproxy"
install: 正在创建目录"/usr/local/haproxy/share"
install: 正在创建目录"/usr/local/haproxy/share/man"
install: 正在创建目录"/usr/local/haproxy/share/man/man1"
"doc/haproxy.1" -> "/usr/local/haproxy/share/man/man1/haproxy.1"
install: 正在创建目录"/usr/local/haproxy/doc"
install: 正在创建目录"/usr/local/haproxy/doc/haproxy"
"doc/configuration.txt" -> "/usr/local/haproxy/doc/haproxy/configuration.txt"
"doc/management.txt" -> "/usr/local/haproxy/doc/haproxy/management.txt"
"doc/seamless_reload.txt" -> "/usr/local/haproxy/doc/haproxy/seamless_reload.txt"
"doc/architecture.txt" -> "/usr/local/haproxy/doc/haproxy/architecture.txt"
"doc/peers-v2.0.txt" -> "/usr/local/haproxy/doc/haproxy/peers-v2.0.txt"
"doc/regression-testing.txt" -> "/usr/local/haproxy/doc/haproxy/regression-testing.txt"
"doc/cookie-options.txt" -> "/usr/local/haproxy/doc/haproxy/cookie-options.txt"
"doc/lua.txt" -> "/usr/local/haproxy/doc/haproxy/lua.txt"
"doc/WURFL-device-detection.txt" -> "/usr/local/haproxy/doc/haproxy/WURFL-device-detection.txt"
"doc/proxy-protocol.txt" -> "/usr/local/haproxy/doc/haproxy/proxy-protocol.txt"
"doc/linux-syn-cookies.txt" -> "/usr/local/haproxy/doc/haproxy/linux-syn-cookies.txt"
"doc/SOCKS4.protocol.txt" -> "/usr/local/haproxy/doc/haproxy/SOCKS4.protocol.txt"
"doc/network-namespaces.txt" -> "/usr/local/haproxy/doc/haproxy/network-namespaces.txt"
"doc/DeviceAtlas-device-detection.txt" -> "/usr/local/haproxy/doc/haproxy/DeviceAtlas-device-detection.txt"
"doc/51Degrees-device-detection.txt" -> "/usr/local/haproxy/doc/haproxy/51Degrees-device-detection.txt"
"doc/netscaler-client-ip-insertion-protocol.txt" -> "/usr/local/haproxy/doc/haproxy/netscaler-client-ip-insertion-protocol.txt"
"doc/peers.txt" -> "/usr/local/haproxy/doc/haproxy/peers.txt"
"doc/close-options.txt" -> "/usr/local/haproxy/doc/haproxy/close-options.txt"
"doc/SPOE.txt" -> "/usr/local/haproxy/doc/haproxy/SPOE.txt"
"doc/intro.txt" -> "/usr/local/haproxy/doc/haproxy/intro.txt"
[rabbit@myblnp1 haproxy-2.0.0]$ cd /usr/local/haproxy/
[rabbit@myblnp1 haproxy]$ 
#配置软链接
[rabbit@myblnp1 haproxy]$ sudo ln -s /usr/local/haproxy/sbin/haproxy  /usr/sbin
[rabbit@myblnp1 haproxy]$ 
[rabbit@myblnp1 haproxy]$ 
[rabbit@myblnp1 haproxy]$ which haproxy
/usr/sbin/haproxy
[rabbit@myblnp1 haproxy]$ 

部署各个负载均衡的内核参数:

#添加ip转发与非本地地址绑定参数
[rabbit@myblnp1 haproxy]$ sudo echo 'net.ipv4.ip_nonlocal_bind = 1' >>  /etc/sysctl.conf
bash: /etc/sysctl.conf: 权限不够
[rabbit@myblnp1 haproxy]$ su
密码:
[root@myblnp1 haproxy]# echo 'net.ipv4.ip_nonlocal_bind = 1' >>  /etc/sysctl.conf
[root@myblnp1 haproxy]# 
[root@myblnp1 haproxy]# echo 'net.ipv4.ip_forward = 1' >> /etc/sysctl.conf
[root@myblnp1 haproxy]# 
[root@myblnp1 haproxy]# sysctl -p
net.ipv4.ip_nonlocal_bind = 1
net.ipv4.ip_forward = 1
[root@myblnp1 haproxy]# 
2、HAProxy 配置文件示例
global   //全局配置
        log 127.0.0.1 local3 info  //日志配置
        maxconn 4096    //最大连接限制(优先级低)
        user nobody
        group nobody
        daemon   //守护进程运行
        nbproc 1  //haproxy进程数
defaults   //针对(listen和backend块进行设置没如果块中没设置,则使用默认设置)默认配置
        log                   global  //日志使用全局配置
        mode           http  //模式7层LB
        maxconn 2048   //最大连接数(优先级中)
        retries         3   //健康检查。3次连接失败就认为服务不可用
        option        redispatch   //服务不可用后的操作,重定向到其他健康服务器
        stats        uri  /haproxy    //web页面状态模块功能开启
        stats auth          qianfeng:123  //状态模块认证(用户名qianfeng密码123)
        contimeout        5000   //定义haproxy将客户端!!!请求!!!转发至后端服务器,所等待的超时时长
    clitimeout         50000  //haproxy作为客户,和后端服务器之间!!!空闲连接!!!的超时时间,到时候发送fin指令
        srvtimeout            50000  //haproxy作为服务器,和用户之间空闲连接的超时时间,到时候发送fin指令
#timeout connect 5000
#timeout client 50000
#timeout server 50000
frontend http-in   //前端配置块。面对用户侧
        bind 0.0.0.0:80  //面对用户监听地址和端口
        mode http  //http模式的LB
        log global  //日志使用全局配置
        option httplog  //默认日志格式非常简陋,仅包括源地址、目标地址和实例名称,而“option httplog参数将会使得日志格式变得丰富许多,其通常包括但不限于HTTP请求、连接计时器、会话状态、连接数、捕获的首部及cookie、“frontend”、“backend”及服务器名称,当然也包括源地址和端口号等。
         option httpclose  // 每次请求完毕后,关闭http通道
     acl html url_reg  -i  \.html$  //1. 访问控制列表名称html。规则要求访问以html结尾的url时
     use_backend html-server if  html  //2.如果满足acl html规则,则推送给后端服务器 html-server
     default_backend html-server  // 3:默认的后端服务器是 html-server
backend html-server  //后端服务器名称为  html-server
        mode http  //模式为7层代理
        balance roundrobin  //轮循(rr)
        option httpchk GET /index.html  //允许用http协议检查server 的健康
        cookie SERVERID insert indirect nocache  //轮询的同时,根据插入的cookie SERVERID  的值来做会话保持,将相同的用户请求,转发给相同的真实服务器。
        server html-A web1:80 weight 1 cookie 3 check inter 2000 rise 2 fall 5
        server html-B web2:80 weight 1 cookie 4 check inter 2000 rise 2 fall 5
cookie 3 服务器ID,避免rr算法将客户机请求转发给其他服务器 ,对后端服务器的健康状况检查间隔为2000毫秒,连续2次健康检查成功,则认为是有效的,连续5次健康检查失败,则认为服务器宕机
3、配置HAProxy

        HAProxy 使用单一配置文件来定义所有属性,包括从前端 IP 到后端服务器。下面展示了用于3个 RabbitMQ 节点组成集群的负载均衡配置。

  • HAProxy 主机:192.168.56.119 5671
  • RabbitMQ1:192.168.56.119 5672
  • RabbitMQ2:192.168.56.120 5672
  • RabbitMQ3:192.168.56.121 5672
#全局配置
global
        #日志输出配置
        log 127.0.0.1 local0 info
        #最大连接限制(优先级低)
        maxconn 4096
        #改变当前的工作目录
        chroot /usr/local/haproxy
        #以指定的UID运行HAproxy
        uid 99
        #以指定的GID运行HAproxy进程
        gid 99
        #以守护进程的方式运行HAproxy
        daemon
        #当前进程 pid文件
        pidfile /usr/local/haproxy/haproxy.pid

#默认配置
defaults
        #应用全局的日志配置
        log global
        #默认的模式 mode(tcp | http | health)
        #TCP是4层,HTTP是7层,health只返回 OK
        mode tcp
        #日志类别
        option tcplog
        #不记录健康检查日志信息
        option dontlognull
        #3次失败则认为服务不可用
        retries 3
        #每个进程可用的最大连接数
        maxconn 2000
        #连接超时
        timeout connect 5s
        #客户端超时
        timeout client 120s
        #服务端超时
        timeout server 120s

#绑定配置
listen rabbitmq_cluster
        bind *5671
        #配置TCP模式
        mode tcp
        #简单的轮询
        balance roundrobin
        #RabbitMQ集群节点配置
        server rmq_node1 192.168.56.119:5672 check inter 5000 rise 2 fall 3 weight 1
        server rmq_node2 192.168.56.120:5672 check inter 5000 rise 2 fall 3 weight 1
        server rmq_node3 192.168.56.121:5672 check inter 5000 rise 2 fall 3 weight 1

#HAproxy监控页面配置
listen monitor
        bind *:8100
        mode http
        option httplog
        stats enable

编写 HAproxy 自启service文件

[root@myblnp1 system]# vim /usr/lib/systemd/system/haproxy.service

[Unit]
Description=HAProxy load Balancer
After=syslog.target network.target

[Service]
ExecStartPre=/usr/local/haproxy/sbin/haproxy -f /usr/local/haproxy/haproxy.cfg   -c -q
ExecStart=/usr/local/haproxy/sbin/haproxy -Ws -f /usr/local/haproxy/haproxy.cfg  -p /usr/local/haproxy/haproxy.pid
ExecReload=/bin/kill -USR2 $MAINPID

[Install]
WantedBy=multi-user.target

[root@myblnp1 system]# systemctl daemon-reload
[root@myblnp1 system]# 

启用日志并重启日志服务

[root@myblnp1 system]# vim /etc/rsyslog.conf

[root@myblnp1 system]# systemctl restart rsyslog
[root@myblnp1 system]# systemctl enable --now haproxy.service
[root@myblnp1 haproxy]# systemctl start haproxy.service
[root@myblnp1 haproxy]# systemctl status haproxy.service 
● haproxy.service - HAProxy load Balancer
   Loaded: loaded (/usr/lib/systemd/system/haproxy.service; enabled; vendor preset: disabled)
   Active: active (running) since 二 2023-11-07 16:14:51 CST; 4s ago
  Process: 27031 ExecStartPre=/usr/local/haproxy/sbin/haproxy -f /usr/local/haproxy/haproxy.cfg -c -q (code=exited, status=0/SUCCESS)
 Main PID: 27032 (haproxy)
   CGroup: /system.slice/haproxy.service
           ├─27032 /usr/local/haproxy/sbin/haproxy -Ws -f /usr/local/haproxy/haproxy.cfg -p /usr/local/haproxy/haproxy.pid
           └─27036 /usr/local/haproxy/sbin/haproxy -Ws -f /usr/local/haproxy/haproxy.cfg -p /usr/local/haproxy/haproxy.pid

11月 07 16:14:51 myblnp1 systemd[1]: Starting HAProxy load Balancer...
11月 07 16:14:51 myblnp1 systemd[1]: Started HAProxy load Balancer.
11月 07 16:14:51 myblnp1 haproxy[27032]: [NOTICE] 310/161451 (27032) : New worker #1 (27036) forked
[root@myblnp1 haproxy]# 
[root@myblnp1 haproxy]# sudo firewall-cmd --zone=public --add-port=8100/tcp --permanent
success
[root@myblnp1 haproxy]# firewall-cmd --reload
success
[root@myblnp1 haproxy]# 

        服务启动成功后,访问如下地址:http://192.168.56.119:8100/stats

HAProxy 数据统计页面

        和RabbitMQ 最相关的是"server rmq_node1 192.168.56.119:5672 check inter 5000 rise 2 fall 3 weight 1" 此类型的3 条配置, 它定义了RabbitMQ 服务的负载均衡细节, 其中包含6 个部分。

  • server <name> : 定义RabbitMQ 服务的内部标识,注意这里的"rmq_node1 "是指包含有含义的字符串名称,不是指RabbitMQ 的节点名称。
  • <ip> : <port> : 定义RabbitMQ 服务连接的IP 地址和端口号。
  • check inter <value> : 定义每隔多少毫秒检查RabbitMQ 服务是否可用。
  • rise <value> : 定义RabbitMQ 服务在发生故障之后,需要多少次健康检查才能被再次确认可用。
  • fall <value > : 定义需要经历多少次失败的健康检查之后, HAProxy 才会停止使用此RabbitMQ 服务。
  • weight <value> : 定义当前RabbitMQ 服务的权重。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值