硬卷消息中间件系列(十一):RabbitMQ 流控、镜像队列、网络分区
????这是一个或许对你有用的社群????一对一交流/面试小册/简历优化/求职解惑,欢迎加入「芋道快速开发平台」知识星球。下面是星球提供的部分资料:《项目实战(视频)》:从书中学,往事中“练”《互联网高频面试题》:面朝简历学习,春暖花开《架构 x 系统设计》:摧枯拉朽,掌控面试高频场景题《精进 Java 学习指南》:系统学习,互联网主流技术栈《必读 Java 源码专栏》:知其然,知其所以然????
👉 这是一个或许对你有用的社群
🐱 一对一交流/面试小册/简历优化/求职解惑,欢迎加入「芋道快速开发平台」知识星球。下面是星球提供的部分资料:
《项目实战(视频)》:从书中学,往事中“练”
《互联网高频面试题》:面朝简历学习,春暖花开
《架构 x 系统设计》:摧枯拉朽,掌控面试高频场景题
《精进 Java 学习指南》:系统学习,互联网主流技术栈
《必读 Java 源码专栏》:知其然,知其所以然
👉这是一个或许对你有用的开源项目
国产 Star 破 10w+ 的开源项目,前端包括管理后台 + 微信小程序,后端支持单体和微服务架构。
功能涵盖 RBAC 权限、SaaS 多租户、数据权限、商城、支付、工作流、大屏报表、微信公众号等等功能:
Boot 地址:https://gitee.com/zhijiantianya/ruoyi-vue-pro
Cloud 地址:https://gitee.com/zhijiantianya/yudao-cloud
视频教程:https://doc.iocoder.cn
来源:blog.csdn.net/yi_qingjun/
article/details/128496818
前面介绍了 RabbitMQ 消息收发与交换机、消息确认机制、重试机制、队列与消息过期、持久化、存储机制与内存管理等相关的知识点,今天我将详细的为大家介绍 RabbitMQ 流控、镜像队列、网络分区 相关知识,希望大家能够从中收获多多!如有帮助,请点在看 、转发 支持一波!!!
流控
前面我们在讲解存储机制时,提到过流量控制。当消息积压时,消息会进入到队列深处,消费消息会使服务器性能大大降低。而内存告警和磁盘告警就是通过设置阈值来预防此情形,当达到阈值后,阻塞集群中所有的Connection,直到对应项恢复正常,属于全局性的流量控制 (Global Flow Control)。而这里将要提到的流控是针对连接Connection来的(Per-Connection Flow Control或者Internal Flow Control)。
流控作用
流控机制是用来避免消息的发送速率过快而导致服务器难以支撑的情形,从而保障服务器资源在安全范围内,提高业务的稳定性。
流控机制
流控是对什么进行控制?是对进程邮箱的阈值进行控制。
rabbitmq进程邮箱
进程通信方式有四种,分别为主从式、会话式、消息传递(邮箱机制)、共享存储区。而Erlang 进程之间并不共享内存 (binary 类型的除外),而是通过消息传递来通信,每个进程都有自己的进程邮箱(mailbox)。
默认情况下,Erlang并没有对进程邮箱的大小进行限制,所以当有大量消息持续发往某个进程时,会导致该进程邮箱过大,最终内存溢出而崩溃。
在rabbitmq中,如果生产者持续高速发送,而消费者消费速度较低时,如果没有流控,很快就会使内部进程邮箱的大小达到内存阈值。
流控机制是什么?
rabbitmq使用了一种基于信用证算法(credit-based algorithm)的流控机制,来限制发送消息的速率来解决进程邮箱大小达到内存阈值而导致服务器崩溃问题。
通过监控各个进程的进程邮箱,当某个进程负载过高而来不及处理消息时,这个进程的进程邮箱就会开始堆积消息。
当堆积到一定量时,就会阻塞而不接收上游的新消息。从而慢慢地,上游进程的进程邮箱也会开始堆积消息。当堆积到一定量时也会阻塞而停止接收上游的消息,最后就会使负责网络数据包接收的进程阻塞而暂停接收新的数据。
流控原理
流控原理流程
进程XXX——> 进程A——> 进程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 值的通知时,若此时上游进程处于阻塞状态则解除阻塞,开始按收更上游进程的消息,一个一个传导最终能够解除最上游的阻塞状态。
综上分析可知,基于信用证的流控机制最终将消息发送进程的发送速率限制在消息处理进程的处理能力范围之内。
流控状态显示
当一个连接(Connection)触发流控时会处于“flow”的状态,代表此Connection状态每秒在 blocked 和 unblocked 之间来回切换数次。以此可以将消息发送的速率控制在服务器能够支撑的范围之内。web页面可以查看,也可以通过 rabbitmactl list_connections 命令查看。
流控对象
流控机制对象
连接(Connection)
信道 (Channel)
队列(Queue)
如下图,从Connection进程——> Channel进程——> Queue进程——> 消息持久化存储进程,是一条消息从接收到存储的一个必需的流控连,对于处于整个流控链中的任意进程,只要该进程阻塞,上游的进程必定全部被阻塞。
主要进程
rabbit_reader进程:连接的处理进程,负责接收、解析 AMQP 协议数据包等。
rabbit_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 状态时,这就意味着在消息持久化时出现了性能瓶颈。
存在情形:持久化消息入盘导致服务器 I/O 负载过高,尤其是在发送大量较大的持久化消息时最容易发生。
性能提升
一般情况下,向一个队列里推送消息时,往往会在rabbit_amqqueue_process中(即队列进程中) 产生性能瓶颈。
提升队列性能方式
第一种,若Erlang版本在18.x以上,可以开启HiPE 功能,可以提高 30%~40%的性能。
第二种,代码层面提升,将单个rabbit_amqqueue_process替换成多个rabbit_amqqueue_process。这里并不是使用多个队列,而是将交换器、队列、绑定关系、生产和消费的方法全部进行封装,这样对于应用来说好是在操作一个(逻辑) 队列。至于怎么封装的,纯运维就不用去了解了,开发就自行百度测试。
基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
项目地址:https://github.com/YunaiV/ruoyi-vue-pro
视频教程:https://doc.iocoder.cn/video/
镜像队列
镜像队列在解决什么?
镜像队列 (Mirror Queue) 机制,可以将队列镜像到集群中的其他Broker节点之上。当集群中的一个节点失效了,队列能自动地切换到镜像中的另一个节点上,以此可以保证服务的可用性,而不是节点实现其上的队列不可用。
机制原理
一般情况下,给每一个配置镜像的队列,即镜像队列都包含一个主节点(master) 和若千个从节点 (slave)。如下图,给qingjun_queue队列配置镜像包含了一个主节点和两个从节点。这里要注意镜像的主从节点和rabbitmq服务主从节点的区别。
实现原理
镜像队列的slave节点会准确地按照镜像队列的master节点执行命令的顺序进行动作,所以镜像队列的master节点和slave节点数据是相同的,维护状态也应该是相同的。
当镜像队列的master节点失效,"资历最老"的镜像队列slave1就会被提升为新的master。选主机制是根据镜像队列save节点加入时间顺序来的,时间最长的slave 即为“资历最老”。
发送到镜像队列的所有消息会被同时发往 master 和所有的 slave 上,如果此时 master 挂掉了,消息还会在 slave 上,这样 slave提升为 master 的时候消息也不会丢失。除发送消息 (Basic.Publish) 外的所有动作都只会向 master 发送,然后再由 master 将命令执行的结果广播给各个 slave。
注意事项
1.若消费者与slave建立连接并进行订阅消费,其实质上都是从 master 上获取消息,只不过看似是从 slave 上消费而已。
2.比如消费者与 slave 建立了 TCP 连接之后执行一个 Basic.Get的操作,那么首先是由 slave 将 Basic.Get 请求发往 master,再由 master 准备好数据返回给slave,最后由 slave 投递给消费者。
集群结构
如下图,集群一共三个节点,broker1、broker2、broker3。每个broker节点都包含1个镜像队列的master 和2 个镜像队列的slave。
qingjun_queue镜像队列的负载大多都集中在broker1上,baimu_queue镜像队列的负载大多都集中在broker2上,wuhan_queue镜像队列的负载大多都集中在broker3 上。
只要确保镜像队列的 master 节点均匀散落在集群中的各 Broker 节点上就可以确保很大程度上的负载均衡。为什么不是绝对?因为每个队列的流量会有不同,因此均匀散落各个队列的 master 也无法确保绝对的负载均衡)。
注意事项
rabbitmq镜像队列同时支持发送方确认机制(publisher confirm )和事务机制。
在事务机制中,只有当前事务在全部镜像中执行之后,客户端才会收到 Tx.Commit-ok 的消息。
在publisher confirmm 机制中,生产者进行当前消息确认的前提是该消息被全部进行所接收了。
镜像结构
镜像队列结构和普通队列结构不同点在于backing_queue的不同,普通队列使用的是rabbit_variable_queue,而镜像队列使用的backing_queue内部包裹了普通backing_queue进行本地消息消息持久化处理,在此基础上增加了将消息和 ack 复制到所有镜像的功能。
如下图,master使用的backing_queue是 rabbit_mirror_queue_master,slave使用的backing_queue是rabbit_mirror_queue_slave。
普通队列结构
镜像队列结构
组播GM
GM的作用
所有对 rabbit_mirror_queue_master 的操作都会通过组播 GM(GuaranteedMulticast)的方式同步到各个 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 上。
加入新节点
如下图,新增了一个节点4,整个过程就像在链表中间插入一个节点。
注意事项: 每当个节点加入或者重新加入到这个镜像链路中时,之前队列保存的内容会被全部清空。
节点宕机的影响
当 slave 挂掉之后,除了与 slave 相连的客户端连接全部断开,没有其他影响。
当 master 挂掉之后,会有以下连锁反应:
第一步,先与 master 连接的客户端连接全部断开。
第二步,选举最老的slave作为新的 master。因为最老的 slave 与旧的 master 之间的同步状态是最好的。如果此时所有 slave 处于未同步状态,则未同步的消息会丢失。
第三步,新的 master 重新入队所有 unack 的消息。因为新的 slave 无法区分这些 unack 的消息是否已经到达客户端,或者是 ack 信息丢失在老的 master 链路上,再或者是丢失在老的 master 组播 ack 消息到所有 slave 的链路上,所以出于消息可靠性的考虑,重新入队所有 unack 的消息,带来的弊端就是客户端可能会有重复消息。
第四步,若客户端连接着 slave,并且 Basic.Consume 消费时指定了 x-cancel-on-ha:failover 参数,那么断开时客户端会收到一个 Consumer Cancellation Notifcation 的通知,消费者客户端中会回调 consumer 接口的 handleCancel 方法。如果未指定 x-cancel-on-ha-failover 参数,那么消费者将无法感知 master 宕机。
配置镜像队列
镜像队列的配置主要是通过添加相应的 Policy 来完成的,两种方式,一是在代码里提前写好配置,二是使用管理命令配置。
注意事项
镜像队列中最后一个停止的节点会是 master,启动顺序必须是 master 先启动。
如果 slave先启动,它会先等待30 秒等待master 的启动,Master节点加入到集群中后,slave才启动。
如果 30 秒内 master没有启动,slave 会自动停止。
当所有节点因故(断电等) 同时离线时,每个节点都认为自己不是最后一个停止的节点,要恢复镜像队列,可以尝试在 30 秒内启动所有节点。
定义参数
参数含义
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(手动)。
ha-promote-on-shutdown #可选参数when-synced(何时同步)和always(始终同步)。
ha-promote-on-failure #可选参数when-synced(何时同步)和always(始终同步)。
注意事项:
ha-mode参数对排他(exclusive) 队列不生效,因为排他队列是连接独占的,当连接断开时队列会自动删除,所以实际上这个参数对排他队列没有任何意义。
将新节点加入已存在的镜像队列时,默认情况下 ha-sync-mode 取值为 manual(手动),镜像队列中的消息不会主动同步到新的 slave 中,除非显式调用同步命令。当调用同步命令后,队列开始阻塞,无法对其进行其他操作,直到同步完成。
若master 因主动原因停掉,比如通过rabbitmgctl stop命令停止,那么slave不会接管 master,此时镜像队列不可用。
若master 因被动原因停掉,比如 Erlang 虚拟机或者操作系统崩溃,那么 slave 会接管 master。
当 ha-sync-mode 设置为 automatic(自动)时,新加入的 slave 会默认同步已知的镜像队列。由于同步过程的限制,所以不建议对生产环境中正在使用的队列进行操作。
当所有slave都出现未同步状态,且ha-promote-on-shutdown参数设为 always,那么不论 master 因为何种原因停止,slave 都会接管 master,优先保证可用性,存在消息丢失情况。当ha-promote-on-shutdown参数设置为 when-synced(默认)时,分两种情况:
命令配置
命令格式:rabbitmqctl set_policy [-p vhost] [–prioritypriority] [–apply-to apply-to] {name} {pattern} {definition}
参数释义
[-p vhost]参数 #可选参数,针对指定vhost下的queue进行设置。
[–prioritypriority] #可选参数,policy的优先级。
[–apply-to apply-to ]参数 #可选参数,指定对象为交换器还是队列。
{name}参数 #自定义策略名称。
{pattern}参数 #queue的匹配模式(正则表达式)
{definition}参数 #ha-mode 、ha-params、ha-sync-mode三部分。
1.查看要进行镜像的队列。
2.设置策略。
> 基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
>
> * 项目地址:<https://github.com/YunaiV/yudao-cloud>
> * 视频教程:<https://doc.iocoder.cn/video/>
# 优先级为0。
#指定对象为所有交换器。
#策略名称为qingjun_policy。
#匹配正则为"以qingjun开头的所有交换器"。
#镜像策略为,在3个节点上进行镜像,并设置消息同步方式为自动同步。
[root@node1 ~]# rabbitmqctl set_policy --priority 0 --apply-to exchanges qingjun_policy "^qingjun*" '{"ha-mode":"exactly","ha-params":3,"ha-sync-mode":"automatic"}'
3.查看结果。
4.查看状态
相关命令
查看节点消息同步状态
[root@node1 ~]# rabbitmqctl list_queues name slave_pids synchronised_slave_pids
取消某队列同步消息功能
#取消qingjue_queue队列的消息同步功能,注意这个队列是能匹配到上面设置的镜像队列策略的。
[root@node1 ~]# rabbitmqctl cancel_sync_queue qingjue_queue
手动同步某队列消息
# 手动给qingjue_queue队列进行消息同步,注意这个队列是能匹配到上面设置的镜像队列策略的。
[root@node1 ~]# rabbitmqctl sync_queue qingjue_queue
网络分区
网络分区的意义
rabbitmq集群的网络分区的容错性不是很高,一般情况可以使用 Federation 或者 Shovel插件就可以解决广域网中的使用问题。不过即使是在局域网环境下,网络分区也不可能完全避免,网络设备(比如中继设备、网卡) 出现故障也会导致网络分区。极端情况下还会造成数据丢失,影响服务的可用性。
当一个集群发生网络分区时,这个集群会分成两个部分或者更多,它们各自为政,互相都认为对方分区内的节点已经挂了,包括队列、交换器及绑定等元数据的创建和销毁都处于自身分区内,与其他分区无关。如果原集群中配置了镜像队列,而这个镜像队列又牵涉两个或者更多个网络分区中的节点时,每一个网络分区中都会出现一个 master 节点,对于各个网络分区,此队列都是相互独立的。当然也会有一些其他未知的、怪异的事情发生。
在一致性数据模型下,若出现网络波动或者网络故障等异常情况,会导致整个数据链的性能就会大大降低。例如有A、B、C、D四节点为一个数据链,倘若其中的 C 节点网络异常,那么整个 A——> B——> C——>D——> A 的数据链就会被阻塞,继而相关服条也会被阻塞,这种情况下就需要将异常节点C剥离整个分区,以确保rabbitmq服务的可用性及可靠性。等待网络恢复之后,可以进行相应的处理来将此前的异常节点加入集群中。
网络分区的影响
数据丢失;
服务阻塞不可用;
网络恢复后,网络分区状态仍然存在。
网络分区的判断
判断机制
rabbitmq集群节点内部通信端口默认为 25672,两两节点之间都会有信息交互。如果某节点出现网络故障,或者是端口不通,则会致使与此节点的交互出现中断,这里就会有个超时判定机制,继而判定网络分区。
超时判定机制与 net_ticktime 参数有关,该参数默认值为 60 秒。若超时,则会有 net_tick_timeout 的信息报出。这个参数与 heartbeat_time 有些区别,heartbeat_time 是指客户端与 RabbitMQ 服务之间通信的心跳时间,针对 5672 端口而言。
节点之间的应答机制(判断流程)
rabbitmq集群内部的每个节点之间会每隔四分之一的net_ticktime 计一次应答 (tick)。若有任何数据被写入节点中,则此节点被认为已经被应答 (ticked) 了。如果连续 4 次,某节点都没有被 ticked,则可以判定此节点已处于“down”状态,其余节点可以将此节点剥离出当前分区。
应答时间范围
如下图,将连续 4 次的 tick 时间记为 T,那么 T 的取值范围为:0.75 * net_ticktime < T<1.25 * net_ticktime。
图中每个节点代表一次 tick 判定的时间戳,在2个临界值 0.75 * net_ticktime 和1.25 * net_ticktime 之间可以连续执行 4 次的 tick 判定。默认情况下,在 45s < T< 75s之间会判定出 net_tick_timeout。
判定方式
日志方式
rabbitmq不仅会将队列、交换器及绑定等信息存储在 Mnesia 数据库中,而且许多围绕网络分区的一些细节也都和这个Mnesia的行为相关。
如果一个节点不能在下时间连上另一个节点,那么 Mnesia 通常认为这个节点已经挂了,就算之后两个节点又重新恢复了内部通信,但是这两个节点都会认为对方已经挂了,Mnesia 此时认定了发生网络分区的情况。这些会被记录到RabbitMO 的服务日志之中。
命令查看
通过命令rabbitmqctl cluster_status 可以看到集群相关信息。
当没有发生网络分区时,在Network Partitions这一块显示无。
当发生网络分区时,Network Partitions这一块会显示相关信息。
监控页面提示
也可以通过 Web 管理界面来查看。如果出现了下图这种告警,即发生了网络分区。
API查看
第三种,通过 HTTP API 的方式调取节点信息来检测是否发生网络分区。
其中会有 partitions 的相关项,如果在其中发现 partitions 项中有内容则为发生了网络分区。举例,将 node2 分离出 nodel和 node3的主分区之后,调用/api/nodes 这个接口的部分信息
#在node1节点上查看,partitions一栏显示有node2节点,说明node1和node2节点发生了网络分区。
[root@node1 ~]# curl -XGET http://192.168.130.129:15672/api/nodes?pretty -i -u qingjun:citms -H "content-type:application/json"
模拟网络分区
模拟网络分区的方式有多种,主要分为以下 3 大类:
iptables 封禁/解封 IP 地址或者端口号。
关闭/开启网卡。
挂起/恢复操作系统
封禁端口
若是通过防火墙屏蔽端口只能模拟出net_tick_timeout,不能触发网络分区。
由于rabbitmq集群内部节点通信端口默认为 25672,可以封禁25672端口方式来模拟出net_tick_timeout。开启防火墙只能模拟触发节点的net_tick_timeout,但是模拟不了节点的网络分区现象。
比如我这里集群有3个节点,node1、node2、node3。此时我们要模拟 node2 节点被剥离出当前分区的情形,即模拟[nodel,node3]和[node2]两个分区。就在 node2节点执行封禁端口命令。
封禁node2节点上的25672端口。
[root@node2 ~]# iptables -A INPUT -p tcp --dport 25672 -j DROP
[root@node2 ~]# iptables -A OUTPUT -p tcp --dport 25672 -j DROP
此时可以查看node1和node3节点日志,显示node2已经触发了节点之间的应答机制。
接触node2节点上的25672封禁,此时才会判定node2节点发生了网络分区。
[root@node2 ~]# iptables -D OUTPUT 1
[root@node2 ~]# iptables -D INPUT 1
此时再去查看node1和node3节点日志,显示node2节点出现了网络分区。而node2节点上的日志显示,node1和node3节点已经掉线。
分别查看三个节点上集群状态。
查看web监控页面。
封禁IP
假设整个rabbitmq集群的节点名称与其IP地址对应如下:
node1 192.168.130.129
node2 192.168.130.130
node3 192.168.130.131
若要模拟出[node1,node3]和[node2]两个分区的情形,可以在 node2 节点上执行封禁命令。
#封禁。
iptables -I INPUT -s 192.168.130.129 -j DROP
iptables -I INPUT -S 192.168.130.131 -j DROP
#解封。
iptables -D INPUT 1
iptables -D INPUT 1
也可以分别在 node1 和 node3 节点上执行封禁命令。
#封禁。
iptables -I INPUT -s 192.168.130.130 -j DROP
#解封。
iptables -D INPUT 1
如果集群的节点部署跨网段,可以采取禁用整个网络段的方式模拟网络分区。假设rabbitmq集群中 3 个节点和其对应的 IP 关系如下:
node1 192.168.0.2
node2 192.168.1.3
node3 192.168.0.4
模拟出[node1,node3]和[node2]两个分区的情形,可以在 node2 节点上封禁命令。
#封禁。
iptables -I INPUT -s 192.168.0.0/24 -j DROP
#解封.
iptables -D INPUT 1
封禁网卡
先使用 ifconfig 命令来查询出当前的网卡编号,如下所示,一般情况下单台机器只有-个网卡。
[root@node1 ~]# ifconfig
同样假设 node1、node2 和 node3 这三个节点组成 RabbitMQ 集群,node2 的网卡编号为ens33,此时要模拟网络分区[nodel,node3]和node2]的情形,需要在 node2 上执行封禁网卡命令。
#封禁。
ifdown ens33 或者 systemctl stop network
#解封。
ifup ens33 或者 systemctl start network
挂起恢复操作系统
操作系统的挂起和恢复操作也会导致集群内节点的网络分区。因为发生挂起的节点不会认为自身已经失败或者停止工作,但是集群内的其他节点会这么认为。
如果集群中的一个节点运行在一台笔记本电脑上,然后你合上了笔记本电脑,那么这个节点就挂起了。或者一个更常见的现象,集群中的一个节点运行在某台虚拟机上,然后虚拟机的管理程序挂起了这个虚拟机节点,这样节点就被挂起了。在等待节点应答时间后,判定出 net_tick_timeout,再恢复挂起的节点即可以复现网络分区。
网络分区的影响
未配置镜像队列情况下
对于未配置镜像的集群,网络分区发生之后,队列也会伴随着宿主节点而分散在各自的分区之中。
对于消息发送方而言,可以成功发送消息,但是会有路由失败的现象,要需要配合 mandatory 等机制保障消息的可靠性。
对于消息消费方来说,有可能会有诡异不可预知的现象发生,比如对于已消费消息的 ack 会失效。如果网络分区发生之后,客户端与某分区重新建立通信链路,其分区中如果没有相应的队列进程,则会有异常报出。如果从网络分区中恢复之后,数据不会丢失,但是客户端会重复消费。
已配置镜像队列情况下
当镜像配置都是 ha-sync-mode=automatic 时,
若有新的 slave 出现时,此 slave 会自动同步 master 中的数据。同步过程中,集群的整个服务都不可用,客户端连接会被阻塞。
若master 中有大量的消息堆积,必然会造成 slave 的同步时间增长,进步影响了集群服务的可用性。
当镜像配置都是 ha-sync-mode=manual时,
若有新的 slave 创建时,此slave不会去同步 master 上旧的数据,如果此时 master 节点又发生了异常,那么此部分数据将会丢失同样 ha-promote-on-shutdown 这个参数的影响也需要考虑进来。
处理网络分区
手动处理
恢复步骤:
第一步,挑选一个信任分区。
第二步,重启非信任分区中的节点,若重启后还有网络分区的告警,再紧接着重启信任分区中的节点。
挑选信任分区:
挑选信任分区有几个指标,优先级从前到后为:
分区中要有 disc 节点;
分区中的节点数最多分区中的队列数最多;
分区中的客户端连接数最多。
重启方式(2种方式):
第一种:先rabbitmqctl stop命令关闭,再用 rabbitmq-server -detached 命令启动。
第二种,先rabbitmqctl stop_app命令关闭,再用 rabbitmqctl start_app 命令启动。(推荐)
重启顺序(2种方式):
第一种,先停止其他非信任分区中的所有节点,然后再启动这些节点。如果此时还有网络分区的告警,则再重启信任分区中的节点以去除告警。比如我上面这种网络分区情况,先关闭node2上的服务,在启动node2服务,若还存在网络分区告警,则在重启node1和node3节点。
第二种,先关闭整个集群中的节点,然后再启动每一个节点,这里需要确保启动的第一个节点在信任的分区之中。
注意事项:
在选择哪种重启顺序之前,首先考虑一下队列“漂移”的现象。所谓的队列“漂移”是在配置镜像队列的情况下才会发生的。
一定要按照这里提及的重启方式重启。若选择挨个节点重启的方式,虽也可以处理网络分区,但是会出现Mnesia 内容权限的归属问题,而且还可能会引起二次网络分区的发生。比如有两个分区[node1,node2]和node3,node4],其中[node1,node2]为信任分区。此时若按照挨个重启的方式进行重启,比如先重启 node3,在 node3 节点启动之时无法判断其节点的 Mnesia 内容是向node1,node2]分区靠齐还是向 node4 节点靠齐。至此,如果挨个一轮重启之后,最终集群中的Mnesia 数据是[node3,node4]这个非信任分区,就会造成无法估量的损失。挨个节点重启。
镜像队列漂移现象
漂移现象:
若存在镜像队列,从发生网络分区到恢复的过程中队列可能会出现“漂移”的现象。所谓漂移就是队列的master节点都“漂移”到一个节点上,除了发布消息之外,其他操作都在master节点上完成,从而造成负载不均衡。
缓解方法:
可以重启之前先删除镜像队列的配置,这样能够在一定程度上阻止队列的“过分漂移”,即阻止可能所有队列都“漂移”到一个节点上的情况。
处理方法:
第一种,最常见的就是直接在监控页面上删除。
第二种,若事先没有开启 RabbitMQ Management 插件,就只能使用 rabbitmqctl 工具的方式。但是需要在每个分区上都执行删除镜像队列配置的操作,以确保每个分区中的镜像都被删除。比如我这里就要在node1和node2节点上都要执行删除命令,或者在node3和node2节点上执行。
查看命令:rabbitmqctl list_policies
删除命令:rabbitmqctl clear_policy [-p vhost] {mirror_queue_name}
[root@node1 ~]# rabbitmqctl list_policies
[root@node1 ~]# rabbitmqctl clear_policy baimu_policy
处理流程总结
网终分区处理步骤总结如下:
步骤 1:先挂起生产者和消费者进程。这样可以减少消息不必要的丢失,若进程数过多情形又比较紧急,也可跳过此步骤。
步骤 2:删除镜像队列的配置。
步骤 3:挑选信任分区。
步骤4:关闭非信任分区中的节点。采用 rabbitmqctl stop_app 命令关闭。
步骤 5:启动非信任分区中的节点。采用与步骤4 对应的 rabbitmqctl start_app命令启动。
步骤 6:检查网络分区是否恢复。若已经恢复,则转步骤 8;若还有网络分区的报警,则转步骤 7。
步骤 7:重启信任分区中的节点。
步骤 8:添加镜像队列的配置。
步骤 9:恢复生产者和消费者的进程。
自动处理网络分区
在 rabbitmg.config 配置文件中配置 cluster_partition_handling参数即可实现相应的功能。有三种方法自动地处理网络分区:pause_minority 模式、pause_if_all_down 模式和 autoheal 模式。
默认是 ignore 模式,即不自动处理网络分区,所以在这种模式下,当网络分区的时候需要人工介入。
pause_minority 模式
若cluster_partition_handling = pause_minority,当发生网络分区时,集群中的节点在观察到某些节点“down”掉的时候,会自动检测其自身是否处于“少数派”(分区中的节点小于或者等于集群中一半的节点数),rabbitmq会自动关闭少数派节点的rabbitmq应用,Erlang虚拟机不关闭。
根据 CAP3 原理,这里保障了 P,即分区耐受性。这样确保了在发生网络分区的情况下,大多数节点(当然这些节点得在同一个分区中) 可以继续运行。“少数派”中的节点在分区开始时会关闭,当分区结束时又会启动。相当于执行了 rabbitmgctl stop_app命令。处于关闭的节点会每秒检测一次是否可连通到剩余集群中,如果可以则启动自身的应用。相当于执行 rabbitmqctl start app 命令。
CAP 原理:
CPA原理,又称之为 CAP 定理,是指在一个分布式系统中,Consistency (一致性)、Availability (可用性)和 Partition tolerance (分区耐受性) 三者不可兼得。
注意事项:
若集群中只有两个节点时,不适合采用 pause_minority 模式。因为其中任何一个节点失败而发生网络分区时两个节点都会关闭。当网络恢复时,有可能两个节点会自动启动恢复网络分区,也有可能仍保持关闭状态。
若集群节点数远大于 2 个时,采用pause_minority 模式比 ignore 模式更加可靠,特别是网络分区通常是由单节点网络故障而脱离原有分区引起的。
若出现对等分区时,会关闭这些分区内的所有节点,只有等待网络恢复之后,才会自动启动所有的节点以求从网终分区中恢复。比如集群组成为[ node1,node2,node3,node4 ]四个节点,由于某种原因分裂成类似[node1,node2]和[node3,node4]这两个网络分区的情形。这种情况在跨机架部署时就有可能发生,当 node1和node2 部署在机架 A 上,而 node3 和 node4 部署在机架 B 上,那么有可能机架 A 与机架 B之间网络的通断会造成对等分区的出现。对于[node1,node2]和[node3,node4]而言,这四个节点上的rabbitmq应用都会被关闭。
pause_if_all_down 模式
若cluster_partition_handling = pause_if_all_down,rabbitmq集群中的节点在和配置检查节点列表中的任何节点不能交互时才会关闭。
如上图配置表示:
若一个节点与rabbitmq_1@node1或rabbitmq_2@node2节点无法通信时,则会关闭自身的rabbitmq应用。
若是rabbitmq_1@node1或rabbitmq_2@node2本身发生了故障造成网络不可用,而其他节点都是正常的情况下,这种规则会让所有的节点中rabbitmq应用都关闭,待rabbitmq_1@node1或rabbitmq_2@node2中的网络恢复之后,各个节点再启动自身应用以从网络分区中恢复。
注意事项:
pause_if_all_down 模式下有 ignore 和 autoheal 两种配置。若出现对等分区时,node1 和 node2 部署在机架 A 上,而 node3 和 node4 部署在机架 B 上。而配置里是node1和node3节点名称,那么当机架A 和机架B 的通信出现异常时,由于 nodel 和 node2 保持着通信,node3 和 node4 保持着通信,这 4 个节点都不会自行关闭,但是会形成两个分区,所以这样不能实现自动处理的功能。所以如果将配置中的 ignore替换成 autoheal 就可以处理此种情形。
在 autoheal 模式下,如果集群中有节点处于非运行状态,那么当发生网络分区的时候,将不会有任何自动处理的动作。
autoheal 模式:
在 autoheal 模式下,当认为发生网络分区时,rabbitmq会自动决定一个获胜 (winning)的分区,然后重启不在这个分区中的节点来从网络分区中恢复。
获胜分区:
一个获胜的分区是指客户端连接最多的分区,若产生一个平局,即有两个或者多个分区的客户端连接数一样多,那么节点数最多的一个分区就是获胜分区。若此时节点数也一样多,将以节点名称的字典序来挑选获胜分区。
总结:
对于 pause-minority 模式,关闭节点的状态是在网络故障时,也就是判定出 net_tick_timeout之时,会关闭“少数派”分区中的节点,等待网络恢复之后,即判定出网络分区之后,启动关闭的节点来从网络分区中恢复。
autoheal 模式在判定出 net_tick_timeout之时不做动作,要等到网络恢复之时,在判定出网络分区之后才会有相应的动作,即重启非获胜分区中的节点。
各模式优缺点
每种模式优缺点:
ignore 模式:发生网络分区时,不做任何动作,需要人工介入。
pause-minoriy 模式:对于对等分区的处理不够优雅,可能会关闭所有的节点。一般情况下,可应用于非跨机架、奇数节点数的集群中。
pause-if-all-down 模式:对于受信节点的选择尤为考究,尤其是在集群中所有节点硬件配置相同的情况下。此种模式可以处理对等分区的情形。
autoheal 模式:可以处于各人情形下的网络分区。但是如果集群中有节点处于非运行状态,则此种模式会失效。
欢迎加入我的知识星球,全面提升技术能力。
👉 加入方式,“长按”或“扫描”下方二维码噢:
星球的内容包括:项目实战、面试招聘、源码解析、学习路线。
文章有帮助的话,在看,转发吧。
谢谢支持哟 (*^__^*)
更多推荐
所有评论(0)