原文:https://sq.163yun.com/blog/ar...
做者:Java大蜗牛安全
在上图的模式下,交换器的类型为Direct,伪代码表示消息的生产和消费服务器
#消息发送方法 #messageBody 消息体 #exchangeName 交换器名称 #routingKey 路由键 publishMsg(messageBody,exchangeName,routingKey){ ...... } #消息发送 publishMsg("This is a warning log","exchange","log.warning");
RoutingKey=log.warning,和队列A与交换器的绑定一致,因此消息被路由到了队列A上。架构
对于消息消费而言,消费者直接指定要消费的队列便可,好比指定消费队列A的数据。并发
须要注意的是,在消费者消费完成数据后,返回给RabbitMq ACK消息,RabbitMq会删掉队列中的该条信息。运维
在Exchange这个模块上,RabbitMq主要支持了Direct,Fanout,Topic三种路由模式,RabbitMq在路由模式上下功夫,也说明了他在设计上想要知足多样化的需求。异步
Direct和Fanout模式比较好理解,相似于单播和广播模式,Topic模式比较有意思,它支持自定义匹配规则,按照规则把全部知足条件的消息路由到指定队列,可以帮助开发者灵活应对各种需求。分布式
RabbitMQ的消息默认是在内存里的,实际上不光是消息,Exchange路由等信息实际都在内存中。内存的优势是高性能,问题在于故障后没法恢复。因此RabbitMQ也支持持久化的存储,也就是写磁盘。微服务
要在RabbitMQ中持久化消息,要同时知足三个条件:高并发
RabbitMQ持久化消息的方式是常见的写日志方式:源码分析
消息持久化的优缺点很明显,拥有故障恢复能力的同时,也带来了性能的急剧降低。同时,因为RabbitMQ默认状况下是没有冗余的,假设一个持久化节点崩溃,一致到该节点恢复前,消息和队列都没法恢复。
1.发后即忘
RabbitMQ默认发布消息是不会返回任何结果给生产者的,因此存在发送过程当中丢失数据的风险。
2.AMQP事务
AMQP事务保证RabbitMQ不只收到了消息,并成功将消息路由到了全部匹配的订阅队列,AMQP事务将使得生产者和RabbitMQ产生同步。
虽然事务使得生产者能够肯定消息已经到达RabbitMQ中的对应队列,可是却会下降2~10倍的消息吞吐量。
3.发送方确认
开启发送方确认模式后,消息会有一个惟一的ID,一旦消息被投递给全部匹配的队列后,会回调给发送方应用程序(包含消息的惟一ID),使得生产者知道消息已经安全到达队列了。
若是消息和队列是配置成了持久化,这个确认消息只会在队列将消息写入磁盘后才会返回。若是RabbitMQ内部发生了错误致使这条消息丢失,那么RabbitMQ会发送一条nack消息,固然我理解这个是不能保证的。
这种模式因为不存在事务回滚,同时总体仍然是一个异步过程,因此更加轻量级,对服务器性能的影响很小。
通常的异步服务间,可能会用两组队列实现两个服务模块以前的异步通讯,有趣的是RabbitMQ就内建了这个功能。
RabbitMQ支持消息应答功能,每一个AMQP消息头中有一个Reply_to字段,经过该字段指定消息返回到的队列名称(这是一个私有队列)消息的生产者能够监听该字段对应的队列。
RabbitMQ集群的设计目标:
从实际结果看,RabbitMQ完成设计目标上并不十分出色,主要缘由在于默认的模式下,RabbitMQ的队列实例子只存在在一个节点上(虽而后续也支持了镜像队列),既不能保证该节点崩溃的状况下队列还能够继续运行,也不能线性扩展该队列的吞吐量。
RabbitMQ内部的元数据主要有:
虽然RabbitMQ的队列实际只会在一个节点上,但元数据能够存在各个节点上。举个例子来讲,当建立一个新的交换器时,RabbitMQ会把该信息同步到全部节点上,这个时候客户端无论链接的那个RabbitMQ节点,均可以访问到这个新的交换器,也就能找到交换器下的队列。
如上图所示,队列A的实例实际只在一个RabbitMQ节点上,其它节点实际存储的是只想该队列的指针。
为何RabbitMQ不在各个节点间作复制了,《RabbitMQ实战》给出了两个缘由:
我理解成本这个缘由并不彻底成立,复制并不必定要复制到全部节点,好比一个队列能够只作两个副本,复制带来的内存成本能够交给使用方来评估,毕竟在内存中没有堆积的状况下,实际上队列是不会占用多大内存的。
还有一点是RabbitMQ自己并无保证消息消费的有序性,因此实际上队列被Partition到各个节点上,这样才能真正达到线性扩容的目的(以RabbitMQ的现状来讲,单队列实际是没法扩容的,只有在业务层作切分)。
注:RabbitMQ集群中的节点能够是内存节点也能够是磁盘节点,但要求至少有一个磁盘节点,这样出现故障时才能恢复数据。
RabbitMQ本身也考虑到了咱们以前分析的单节点长时间故障没法恢复的问题,因此RabbitMQ 2.6.0以后它也支持了镜像队列,换个说法也就是副本。
除了发送消息,全部的操做实际都在主拷贝上,从拷贝实际只是个冷备(默认的状况下全部RabbitMQ节点上都会有镜像队列的拷贝),若是使用消息确认模式,RabbitMQ会在主拷贝和从拷贝都安全的接受到消息时才通知生产者。
从这个结构上来看,若是从拷贝的节点挂了,实际没有任何影响,若是主拷贝挂了,那么会有一个重新选主的过程,这也是镜像队列的优势,除非全部节点都挂了,才会致使消息丢失。从新选主后,RabbitMQ会给消费者一个消费者取消通知(Consumer Cancellation),让消费者重连新的主拷贝。
1.RabbitMQ结构
BackingQueue由Q1,Q2,Delta,Q3,Q4五个子队列构成,在Backing中,消息的生命周期有四个状态:
这里以持久化消息为例(能够看到非持久化消息的生命周期会简单不少),从Q1到Q4,消息实际经历了一个RAM->DISK->RAM这样的过程,BackingQueue这么设计的目的有点相似于Linux的Swap,当队列负载很高时,经过将部分消息放到磁盘上来节省内存空间,当负载下降时,消息又从磁盘回到内存中,让整个队列有很好的弹性。所以触发消息流动的主要因素是:1.消息被消费;2.内存不足。
RabbitMQ会更具消息的传输速度来计算当前内存中容许保存的最大消息数量(Traget_RAM_Count),当:内存中保存的消息数量+等待ACK的消息数量>Target_RAM_Count时,RabbitMQ才会把消息写到磁盘上,因此说虽然理论上消息会按照Q1->Q2->Delta->Q3->Q4的顺序流动,可是并非每条消息都会经历全部的子队列以及对应的生命周期。
从RabbitMQ的Backing Queue结构来看,当内部不足时,消息要经历多个生命周期,在Disk和RAM之间置换,者实际会下降RabbitMQ的处理性能(后续的流控就是关联的解决方法)。
2.镜像队列结构
全部对镜像队列主拷贝的操做,都会经过Guarented Multicasting(GM)同步到各个Salve节点,Coodinator负责组播结果的确认。
GM是一种可靠的组播通讯协议,保证组组内的存活节点都收到消息。
GM的主播并非由Master节点来负责通知全部Slave的(目的是为了不Master压力过大,同时避免Master失效致使消息没法最终Ack),RabbitMQ把一个镜像队列的全部节点组成一个链表,由主拷贝发起,由主拷贝最终确认通知到了全部的Slave,而中间由Slave接力的方式进行消息传播。
从这个结构来看,消息完成整个镜像队列的同步耗时理论上是不低的,可是因为RabbitMQ消息的消息确认自己是异步的模式,因此总体的吞吐量并不会受到太大影响。
当RabbitMQ出现内存(默认是0.4)或者磁盘资源达到阈值时,会触发流控机制,阻塞Producer的Connection,让生产者不能继续发送消息,直到内存或者磁盘资源获得释放。
RabbitMQ基于Erlang/OTP开发,一个消息的生命周期中,会涉及多个进程间的转发,这些Erlang进程之间不共享内存,每一个进程都有本身独立的内存空间,若是没有合适的流控机制,可能会致使某个进程占用内存过大,致使OOM。所以,要保证各个进程占用的内容在一个合理的范围,RabbitMQ的流控采用了一种信用证机制(Credit),为每一个进程维护了四类键值对:
如图所示,A进程当前能够发送给B的消息有100条,每发一次,值减1,直到为0,A才会被Block住。B消费消息后,会给A增长新的Credit,这样A才能够持续的发送消息。这里只画了两个进程,多进程串联的状况下,这中影响也就是从底向上传递的。
想学习Java工程化、分布式架构、高并发、高性能、深刻浅出、微服务架构、Spring,MyBatis,Netty源码分析等技术能够加群:479499375,群里有阿里大牛直播讲解技术,以及Java大型互联网技术的视频免费分享给你们,欢迎进群一块儿深刻交流学习。
注:本文基于的RabbitMQ材料可能较为陈旧,新的RabbitMQ可能会有不一样的功能特性
总体来看,RabbitMQ的功能比较丰富(惋惜没有看到延迟,优先级等功能),更适用于偏实时的业务场景,与Kafka这样的队列定位上有明显的区别。它自己应该是一个简单健壮的组件,但若是要应用在一个大规模的分布式系统中,实际仍是须要作一些外部的再次开发,以解决咱们前面提到的队列存储单点,流控等问题。直观上看它的运维成本是会比较高的,须要使用方有必定的经验。