《RabbitMQ实战指南》整理(六)跨越集群与高阶应用
RabbitMQ可以通过三种方式进行分布式部署:集群、Federation、Shovel,Federation和Shove提供了更高的灵活性,但也提高了部署的复杂性
一、Federation
Federation插件设计的目的是使RabbitMQ在不同的Broker节点之间进行消息传递而无需建立集群,其功能如下:
- 在不同管理域(不同的用户、vhost、或不同版本的RabbitMQ和Erlang)中的Broker或者集群之间传递消息;
- 基于AMQP协议在不同的Broker之间进行通信,并且能够容忍不稳定的网络连接情况;
- 一个Broker节点可以同时存在联邦交换器(或队列)或者本地交换器(或队列),需对指定特定交换器或队列创建Frderation连接(联邦交换器允许一个本地消费者接受来自上游队列的消息);
- 不需要在多个Broker之间创建多个连接
1、联邦交换器
假设broker1和broker2中的exchangeA的交互存在一定的延迟,则可利用Federation在broker1上建立一个同名的exchangeA,并在内部建立交换器exchangeA->broker2 B,并通过路由键将这两个交换器绑定起来,与此同时会在内部建立一个federation:exchangeA->broker B队列。Federation会在federation:exchangeA->broker B队列和exchangeA之间建立一个AMQP连接来实施的消费队列federation:exchangeA->broker B中的数据,其扮演了一个内部交换器的角色。
联邦交换器可以成为另一个交换器的上游交换器,两方交换器也可以互为联邦交换器和上游交换器。需要注意的是不能对默认和交换器和内部交换器使用Federation功能
2、联邦队列
联邦队列可以在多个broker节点或集群之间为单个队列提供负载均衡的功能,一个联邦交换器可以连接一个或多个上游队列,并从这些上游队列中获取消息以满足本地消费者消费消息的需求
假设brokerA和brokerB,brokerB中的队列queue1和queue2需要将brokerA作为上游,使用Federation后会在brokerA中建立同名的队列queue1和queue2,后续brokerB会将拉取brokerA中相应队列的消息,当brokerB中的queue1或2有消息堆积时,则不会继续拉取上游brokerA的消息消费,即消费者可以既消费brokerA中消息又可以消息brokerB中的消息,当一端来不及消费时另一方可以帮忙消费,以此达到某种意义上的负载均衡。
两个队列可以互为联邦队列,一个消息可以在两个联邦队列间转发无限次。需要注意的是联邦队列只能使用Basic.Consume进行消费。此外联邦队列不具有传递性
3、Federation的使用
使用Federation需要配置下述内容:
- 配置一个或多个upstream,每个upstream均定义其它节点的Federation link
- 定义匹配交换器或者队列的一种/多种策略
执行rabbitmq-plugins enable rabbitmq_federation可以开启Federation功能,它会默认开启amqp_client插件,另外如果要开启Federation插件需要执行rabbitmq-plugins enable rabbitmq_feseration_management命令(该插件需要依附rabbitmq_management插件)
有关Federation uostream的信息全部保存在RabbitMQ的Mnesia数据库中,包括用户信息,权限信息,队列信息等。Federation中存在3种级别的配置:①upstreams为每个upstream定义与其它Broker建立连接的消息;②Upstream sets用于对一系列使用Federation功能的upstream进行分组;③Policies:每一个Policy选定一组交换器或队列,或对两者进行限定进而作用于一个单独upstream或者upstream set之上。其中upstream set可以忽略替换为all
upstream和upstream sets都属于运行时参数,每个vhost都持有不同的参数和策略集合。Federation相关的运行时参数和策略可以通过rabbitmqctl工具、HTTP API接口或rabbitmq_federation_management插件提供的Web管理界面配置
二、Shovel
与Federation的数据转发功能类似,Shovel能够可靠、持续地从一个Broker种的队列(源端)拉取数据并转发至另一个Broker种的交换器(目的端),源端的队列和目的端的队列可以位于同一个Broker或不同Broker。其优势在于:
- 松耦合:可以移动位于不同管理域中的Broker上的消息;
- 支持广域网:基于AMQP协议在Broker之间进行通信,可以容忍时断时续的连通,保证消息的可靠性;
- 高度定制:Shovel成功连接后,可以对其配置以执行相关的AMQP命令
1、Shovel的原理
通常情况下,使用Shovel时配置队列作为源端,交换器作为目的端。当配置队列作为目的端时,中间也是经过交换器转发,而这个交换器是默认交换器。当配置交换器作为源端时,broker会新建一个队列并绑定这个交换器,最后从这个队列中拉取消息进行转发。
涉及到Shovel的broker、exchange和queue可以在Shovel link建立之前创建,也可以在Shovel成功连接源端或目的端的Broker之后创建。Shovel可以为源端或目的端配置多个Broker的地址,使得源端或目的端的Broker失效后能够重新连接到其它Broker上(随机选择)。可以设置reconnect_delay参数避免重连行为导致的网络泛洪,或者可以在重连失败后停止连接。
2、Shovel的使用
Shovel插件默认在RabbitMQ发布包中,执行rabbitmq-plugins ebable rabbitmq_shovel命令可以开启Shovel功能,如果要开启Shovel管理插件,需要执行rabbitmq-plugins enable rabbitmq_shovel_management命令。Shovel可以部署在源端或者目的端,另外有两种方式可以部署Shovel:静态方式和动态方式。静态方式是指在config文件中进行设置,动态方式是指通过Runtime Parameter设置
静态方式:config文件中针对Shovel插件的配置信息由单条Shovel条目构成,每一条Shovel条目定义了源端和目的端的转发关系
动态方式:与Federation upstream类似,Shovel动态部署方式的配置信息会被保存到RabbitMQ的Mnesia数据库中,包括权限信息,用户信息和队列信息。每一个ShovelLink由一个相应的Parameter定义,它可以通过rabbitmqctl工具,RabbitMQ Management管理界面或者HTTP API接口方式进行添加。
3、通过Shovel解决消息堆积
当某个队列中的消息堆积严重超过某个阈值时,可以通过Shovel将队列中的消息移交给另一个集群
三、存储机制
1、持久化的消息在到达队列时就被写到磁盘,如果可以会在内存中保存一份备份以提高性能,当内存吃紧时会从内存中清除;非持久化消息一般只保存在内存中,内存吃紧时会保存到磁盘中以节省内存空间;两种消息类型都是在RabbitMQ的持久层中完成。
2、持久层是逻辑上的概念,实际包含两个部分,队列索引rabbit_queue_index和消息存储rabbit_msg_store:
- 队列索引负责维护队列中落盘消息的信息,包括消息的存储地点,是否已被交付等,是否已被消费等,每个队列都有一个队列索引
- 消息存储以键值对的形式存储消息,它被所有队列共享,在每个节点中有且只有一个;具体可以分为msg_store_persisten和msg_store_transient,msg_store_persisten负责持久化消息的持久,msg_store_transient负责非持久化消息的持久
3、消息(包括消息体、属性和headers)可以存储在队列索引中,也可以存储在消息存储中,较佳的配置是较小的消息存储在队列索引中,较大的消息存储在消息存储中,消息大小可以通过queue_index_embed_msgs_below来配置,默认大小为4096B
rabbit_queue_index中以顺序的段文件进行存储,每个段文件中包含固定的SEGMENT_ENTRY_Count条记录,默认值为16384。每个rabbit_queue_index从磁盘中读取消息时至少要在内存中维护一个段文件,所以设置queue_index_embed_msgs_below值要谨慎
4、经过rabbit_msg_store处理的所有消息都会以追加的方式写入文件中,当一个文件大小超过指定的限制时(file_size_limit)时,会关闭这个文件再创建一个新的文件供消息写入,文件名会从0开始累加。
在消息存储时,RabbitMQ会记录消息在文件中的位置映射和文件的相关消息,在读取消息时,会根据消息的ID找到对应存储的文件,文件存在且未被锁住则直接打开文件从指定位置读取文件,如果文件不存在或被锁住则发送请求由rabbit_msg_store处理。消息的删除只是从Rabbit记录的表中删除指定消息的相关信息,并更新对应存储文件的相关信息,并不会立即对文件中的消息进行删除,而仅仅是做一个标志,当检测到前后两个文件的有效数据可以合并时,并且垃圾数据的大小和所有文件(至少3个文件)的数据大小比值超过设定的阈值时(默认0.5)会触发垃圾回收将两个文件进行合并,合并的文件一定是逻辑上相邻的两个文件
1、队列的结构
1、队列通常由rabbitmq_amqqueue_process和backing_queue两部分组成,rabbitmq_amqqueue_process负责协议相关消息的处理,包括接受生产者发布的消息、向消费者交付消息、处理消息的确认。backing_queue是消息存储的具体形式和引擎,并向rabbitmq_amqqueue_process提供相关的接口以供调用
2、消息存入队列后其状态会发生变化,会存在下面4种状态:
- alpha:消息内容和消息索引存储在内存中;
- beta:消息内容存储在磁盘中,消息索引存储在内存中;
- gamma:消息内容存储在磁盘中,消息索引在磁盘和内存中都有;
- delta:消息内容和索引都在磁盘中
对于持久化消息,消息内容和消息索引都必须先保存在磁盘上,才能处于上述状态的一种,gamma消息只有持久化消息才会有。RabbitMQ会在运行时根据消息传送速度定期计算一个当前内存中能够保存的最大消息数量,当alpha状态的消息数据大于这个值时,会引起消息状态的转换,多余的消息会转换到其它状态。alpha状态最耗内存,delta需要读消息索引和读消息内容进行两次IO操作,消耗较多的CPU和磁盘IO,beta和gamma只需要一次磁盘IO操作就可以从消息存储中读取到消息
3、backing_queue默认实现是rabbit_variable_queue,其内部通过5个子队列Q1、Q2、Delat、Q3和Q4来体现消息的各个状态。Q1和Q4只包含alpha状态的消息,Q2和Q3包含beta和gamma状态的消息,Delat只包含delta状态的消息,一般情况下消息按照Q1->Q2->Delta->Q3->Q4顺序进行流动。消费者获取消息也会引起消息状态的转换
2、惰性队列
惰性队列会尽可能的将消息存入磁盘中,而在消费者消费到相应的消息时才会被加载到内存中,其设计目标是为了支持更长的队列支持更多的消息存储。当消费者由于各种原因导致消息堆积时,惰性队列就会很有用。在声明队列的时候可以通过x-queue-mode参数来设置队列的模式,取值为default和lazy
四、内存及磁盘告警
当内存使用超过配置的阈值或磁盘空间低于配置的阈值时,RabbitMQ会暂时阻塞客户端的连接并停止接受从客户端发来的消息,同时心跳检测也会失效。为阻止生产者的同时又不影响消费者的运行,建议将生产者和消费者的逻辑分摊到独立的Connection上。
1、内存告警
RbbitMQ服务器会在启动或执行rabbitmqctl set_vm_memory_high_watermark fraction命令时计算计算机内存的大小,默认情况下vm_memory_high_watermark的值为0.4,即当RabbitMQ使用的内存超过内存的40%时,就会产生内存警告并阻塞生产者的连接。
内存阈值可以通过config文件进行配置或者通过命令rabbitmqctl set_vm_memory_high_watermark fraction命令进行设置,fraction的值即为阈值。正常情况下建议该值在0.4~0.66之间。通过命令设置的阈值会在重启后失效,而config文件设定的阈值则不会失效,但是需要重启才能生效
当某个broker节点触及到内存并阻塞生产者之前它会尝试将队列中的消息换页到磁盘以释放内存空间,默认情况下内存到达阈值的50%时就会进行换页动作,可以在config文件中修改vm_memory_high_watermark_paging_ration项来修改该值
2、磁盘告警
当剩余磁盘空间低于确定的阈值时,RabbitMQ同样会阻塞生产者以避免因非持久化消息持续换页而耗尽磁盘空间导致服务崩溃。默认情况下,磁盘阈值为50MB。正常情况下RabbitMQ会每10秒一次进行磁盘剩余空间的检查,当要到达磁盘阈值时,检测频率为每秒10次。
可以通过config文件中的disk_free_limit来设置磁盘阈值,或者将磁盘阈值设置为集群内存的比值,一般建议取值在1.0~2.0之间
五、流控
流控机制是用来避免消息的发送速率过快而导致服务器难以支撑的情形,内存和磁盘告警相当于全局的流控,而此处的流控是针对的单个Connection
1、流控的原理
Erlang进程之间并不共享内存,而是通过消息传递来通信,每个进程都有自己的进程邮箱,默认情况下Erlang不对进程邮箱的大小进行限制,所有当有大量消息持续发往某个进程时,会导致该进程邮箱过大。RabbitMQ中使用基于信用证算法的流控机制来限制发送消息的速率来解决这个问题,它通过监控各个进程的进程邮箱,当某个进行负载过高时就会开始堆积消息,当堆积到一定的量之后则会开始阻塞不接受消息,进而上游进程也会因堆积消息而停止接受上游的消息,最后负责网络数据包接受的进程会阻塞而暂停接受新的数据
一个Connection触发流控时会处于flow状态,这意味着这个Connection的状态每秒在blocked和unblocked之间来回切换多次,进而将消息发送的速率控制在服务器能够支撑的范围内。流控状态不仅作用于Connection,同样还作用于信道和队列,从Connection到Channel,再到队列,最后是消息持久化存储,形成了一个完成的流程链,只要某个进程阻塞,其上游进程必定阻塞
2、打破性能瓶颈
提高队列的性能一般有两种解决方案,第一种是开启Erlang语言的HiPE功能,能保守提高30%~40%的性能,另一种是寻求打破rabbitmq_amqqueue_process的性能瓶颈,这里是指以多个rabbitmq_amqqueue_process替换单个rabbitmq_amqqueue_process,这样可以充分利用上Connection或者Channel进程中被流控的性能
六、镜像队列
引入镜像队列可以将队列镜像到集权中的其它broker节点之上,当集群中的一个节点失效了,队列能够自动地切换到镜像中的另一个节点上以保证服务的可用性。通常用法下,一个配置镜像的队列都包含一个主节点和若干个从节点,从节点会按照主节点执行命令的顺序进行动作。如果master由于某种原因失效,那么加入时间最早的salve会被提升为新的master
如果消费者与slave建立连接并消费,本质上都是从master上获取消息。镜像队列同时支持publish confirm和事务两种机制,在事务中只有在全部镜像中执行之后,客户端才会收到OK消息,publish confirm中同理
镜像队列的配置是通过添加相应的Policy来完成的。默认情况下,镜像队列中的消息不会主动同步到新的slave中,除非显示调用同步命令,所以由于同步过程的限制,不建议对生产环境中正在使用的队列进行操作。但所有slave都出现未同步状态,如果master由于主动原因停掉,那么slave不会接管master,而如果是系统崩溃等被动原因停掉则会接管,我们可以将ha-promote-on-shutdown设置为always以保证可用性。