RabbitMQ简介
- 什么是消息中间件
消息
:在应用间传送的数据,消息可以非常简单比如只包含文本字符串、json等,也可以很复杂比如内嵌对象
消息队列中间件
(MQ):利用高效可靠的消息传递机制进行与平台无关的数据交流,并基于数据通信来进行分布式系统的集成。一般有两种传递方式:点对点模式和发布/订阅模式
-
MQ的作用:
解耦(扩展性):MQ在处理过程中插入了一个隐含的、基于数据的接口层,两边的处理过程都要实现这一接口,允许独立扩展或修改两边的处理过程,只要确保它们遵守同样的接口约束即可,方便提高消息入队和处理的效率。
冗余(存储):有些情况下处理数据的过程会失败。MQ可以把消息进行持久化直到它们已经被完全处理,规避了数据丢失风险。在把一个消息从MQ中删除之前,需要处理系统明确指出该消息已经被处理完成,从而确保数据被安全保存直到使用完毕。
削峰:访问量剧增的情况下应用仍然需要继续发挥作用,但是这样的突发流量并不常见,如果以能处理这类峰值为标准而投入资源是巨大的浪费。使用MQ能够使关键组件支撑突发访问压力。
可恢复性:MQ降低了进程间的耦合度,即使一个处理消息的进程挂掉,加入MQ的消息仍可以在系统恢复后进行处理。
顺序保证:在大多数使用场景下,数据处理顺序很重要,大部分MQ支持一定程度上的顺序性。
异步通信:很多时候应用不想也不需要立即处理信息。MQ提供了异步处理机制,允许应用把一些消息放入MQ中但不立即处理它,在之后需要的时候再慢慢处理。
RabbitMQ入门
-
相关概念介绍
RabbitMQ整体上是一个生产者消费者模型,主要负责接受、存储和转发消息。可以把消息传递的过程想象成:当你讲一个包裹送到邮局,邮局会暂存并最终将邮件通过邮递员送到收件人手上。
RabbitMQ就好比由邮局、邮箱和邮递员组成的一个系统。从计算机术语层面上,RabbitMQ模型更像是一种交换机模型。
rabbitMQ模型架构.png-
生产者和消费者
-
Producer:生产者,投递消息的一方。
生产者创建消息,发布到RabbitMQ中
-
消息组成
- 消息体(payload):一般是一个带有业务逻辑结构的数据,比如一个json字符串,可以做进一步的序列化操作。
- 标签(label):表述消息,比如一个交换器名称和一个路由键,用于让MQ以此为依据发给感兴趣的消费者
-
-
Consumer:消费者,接受消息的一方。
连接到MQ并订阅队列
消费一条消息时只是消费消息的消息体,不知道生产者是谁
Broker:消息中间件的服务节点
-
-
队列:RabbitMQ内部对象,消息都只能存储在队列中
- 多个消费者可以订阅同一个队列,此时队列中的消息被平均分摊(轮询)给多个消费者进行处理
-
交换器
-
生产者将消息发送到交换器,由交换器将消息路由到队列
rabbitMQ交换器.png
-
- 4种类型
- fanout
把所有发送到该交换器的消息路由到所有与该交换器绑定的队列中
- direct
把消息路由到BindingKey和RoutingKey完全匹配的队列中
- topic
类似direct,匹配规则上存在*和#进行模糊匹配,BindingKey和RoutingKey都用.来进行分割
- headers
根据发送的消息内容中的headers属性进行匹配,性能很差不实用
-
路由键、绑定
- 生产者将消息发送给交换器时需要一个RoutingKey
- 交换器和队列关联通过一个BindingKey,绑定时使用的路由键
- 交换器相当于投递包裹的邮箱,RoutingKey相当于包裹上的地址,BindingKey相当于包裹的目的地,当填写在包裹上的地址和实际想要投递的地址匹配时就会被正确投递到目的地
-
connection和信道
-
NIO(非阻塞IO)
- 包含三大核心部分:channel、buffer、selector。数据总是从信道读取到缓冲区或者从缓冲区写入信道中。selector用于监听多个信道事件(如连接打开,数据到达等)。单线程可以监听多个数据的线道。
- 每个信道流量不是很大时复用单一的connection可以在产生性能瓶颈的情况下有效节省TCP连接资源。但是当信道本身流量很大时就需要开辟多个connection去均摊。
-
运转流程
- 生产者
- 生产者连接到MQ Broker建立一个连接和开启一个信道
- 生产者声明一个交换器并设置属性(交换机类型、是否持久化等)
- 生产者声明一个队列并设置属性(是否排他、是否持久化、是否自动删除等)
- 生产者通过RoutingKey和BindingKey绑定起来
- 生产者发送消息给MQ Broker
- 相应的交换器根据收到的RoutingKey查找匹配的队列
- 如果找到则将生产者发送过来的消息存入相应队列中
- 如果没找到根据生产者配置的属性选择丢弃或回退给生产者
- 关闭信道
- 关闭连接
- 消费者
- 消费者连接到MQ Broker,建立一个连接和开启一个信道
- 消费者向Broker请求消费相应队列中的消息,可能会设置相应的回调函数以及做一些准备工作
- 等待Broker回应并投递相应队列中的消息,消费者接收消息
- 消费者确认接收到的消息回复ack
- MQ从队列中删除相应已确认的消息
- 关闭信道
- 关闭连接
- 生产者
rabbitMQ信道.png -
-
消费端的确认与拒绝
-
消息确认机制
消费者在订阅队列时可以指定autoAck参数
- autoAck=false
- MQ会等待消费者显式回复确认信号后才移去消息(实质上是打上删除标记之后再删除)
- 对于MQ服务端来说队列中的消息分成了两个部分
- 等待投递给消费者的消息
- 已经投递给消费者但是还没有收到消费确认信号的消息
- 一直没收到消费者确认信号而且消费此消息的消费者断开连接,MQ会安排该消息重新进入队列等待投递
- MQ不会为未确认的消息设置过期时间,设计者允许消费者消费一条消息的时间可以很久很久
- autoAck=true
- MQ会自动把发送出去的消息设置为确认然后删除,不管消费者是否真正消费到了
- autoAck=false
-
消费接收到消息后可以选择回复拒绝
-
requeue=true
重新将消息存入队列以便发送给下一个订阅的消费者
-
requeue=false
MQ立即把消息从队列中移除,死信队列可以通过检测被拒绝或者未送达的消息来追踪
-
-
RabbitMQ进阶
-
消息何去何从
-
mandatory
参数:交换器无法匹配到队列后的处理- true:将消息返回给生产者
- false:直接丢弃
-
immediate
参数:队列无匹配到消费者后的处理- 消息关联的队列上有消费者则立刻投递
- 所有匹配的队列上都没有消费者则直接返回给生产者,不用将消息入队列而等待消费者
- 影响镜像队列的性能增加了代码复杂性,官方建议用TTL和DLX替代
- 备份交换器(Alternate Exchange):未被路由的消息存储在备份交换器中
-
-
过期时间(TTL)
TTL:Time to Live
- 消息TTL
- 对消息本身单独设置,如队列里也有设置则以较小值为准
- 消息过期不会马上从队列中抹去,因为每条消息是否过期是在即将投递到消费者之前判定的
- 队列TTL
- 队列中所有消息都有相同的过期时间
- 一旦消息过期,直接从队列中抹去,因为已过期的消息肯定在队列头部
- MQ会确保过期时间到达后将队列删除,但不保障删除的动作有多及时
- 消息在队列中的生存时间超过TTL时变成死信(Dead Message)
- 消息TTL
-
死信队列(DLX)
进入到DLX的一般情况:
- 消息被拒绝
- 消息过期
- 队列达到最大长度
-
延迟队列
延迟队列存储的对象是对应的延迟消息,消息被发送以后并不想消费者立刻拿到消息而是等待特定时间后消费者才能拿到
-
延迟队列不是MQ本身的功能,通过DLX和TTL可以模拟
rabbitMQ延迟队列.png
-
优先级队列
- 优先级高的消息具备优先被消费的特权。
- 如果消费者的消费速度>生产者的速度且Broker中没有消息堆积,对发送的消息设置优先级就没有实际意义了,因为生产者刚发送完一条消息就被消费者消费了,相当于Broker中至多只有一条信息。
-
持久化
-
交换器持久化
- 如果交换器不设置持久化,MQ服务重启之后相关的交换器元数据会丢失但是消息不会丢失只是不能将消息发送到这个交换器中了。
-
队列持久化
- 如果队列不设置持久化,MQ服务重启之后相关队列的元数据会丢失,数据也会丢失,队列里的消息也丢失了。
- 队列的持久化能保证队列本身的元数据不会因异常情况而丢失,但是并不能保证内部存储的消息不会丢失。
-
消息持久化
- 设置了队列的消息的持久化,MQ服务重启后消息才依旧存在。
- 所有的消息都设置为持久化会严重影响MQ的性能。对于可靠性不是那么高的消息可以不采取持久化处理以提高整体的吞吐量。
- 在选择消息是否持久化时需要可靠性和吞吐量之间做一个权衡。
-
交换器、队列、消息都设置持久化也不能百分百保证消息不丢失
角色 场景 解决方案 消费者 消费者订阅消费队列时 autoAck
设置为true,消费者接收到信息后还没来得及处理就宕机了autoAck设置为false Broker 持久化的消息正确存入MQ之后还需要一段时间(虽然短但是不可忽视)才能存入磁盘中。MQ并不会为每条信息都进行同步存盘的处理,可能仅仅保存到操作系统缓存而不是物理磁盘中,如果这段时间内MQ服务节点发生宕机、重启等异常情况,消息还没来得及落盘消息将会丢失 1. 引入MQ镜像队列,配置副本,主节点在此特殊时间内挂掉可以自动切换到从节点保证高可用,除非整个集群都挂掉 2. 发送者引入事务机制或者发送方确认机制来确保消息已经正确发送存储至MQ中
-
-
生产者确认
- 事务机制
- 只有消息成功被MQ接收事务才能提交成功,捕获异常之后进行事务回滚的同时进行消息重发
- 吸干MQ的性能,从AMQP协议层面来看没有更好办法,但是MQ提供了改进方案-发送方确认机制
- 在一条消息发送之后会使发送端阻塞以等待MQ回应,之后才能继续发下一条消息
- 发送方确认机制
- 生产者将信道设置成confirm模式,所有该信道上发布的消息都会被指派一个唯一的ID,一旦消息被投递到所有匹配的队列后MQ就会发送一个确认(ACK)给生产者(包含消息的唯一ID)。如果消息和队列都是可持久化的,ACK会在消息写入磁盘后发出
- 异步机制,一旦发布一条消息生产者可以在等信道返回确认的同时继续发下一条消息,当消息最终确认之后生产者通过回调方法来处理该确认信息,如果MQ因为自身内部错误导致消息丢失会发送一条nack命令
- 设置multiple参数表示到这个序号之前的所有消息都已经得到了处理
- 事务机制
-
消费者要点
- 消息分发
- 队列拥有多个消费者时,队列收到的消息将以轮询方式分发给消费者。每条消息置灰发送给订阅列表里的一个消费者。负载加重时只需要创建更多的消费者来消费处理消息。
- 如果有n个消费者,MQ会将第m条消息分发给第m%n个消费者,不管消费者是否消费并已经确认了消息。如果某些消费者来不及消费那么多的消息,而有些消费者由于某些原因(如业务逻辑简单、机器性能好等)很快处理完了所分配到的消息,就会造成整体应用吞吐量下降。
- 解决方式:设置允许限制信道上的消费者所能保持的最大未确认消息的数量(对拉模式的消费方式无效)
- 消息顺序性
- 顺序性被打破的常见情形
- 生产者使用了事务机制或者启用
confirm
时要重发消息,如果补偿重发是在另一个线程实现的那么生产者源头就出现了错序。 - 生产者发送的消息设置了不同的超时时间并且设置了死信队列,整体上来说相当于一个延迟队列,那么消费者在消费这个延迟队列时消息的顺序和生产者发送消息的顺序一致
- 消息设置了优先级
- 生产者使用了事务机制或者启用
- 顺序性场景
- 不使用任何MQ的高级特性,没有消息丢失、网络故障之类异常情况发生,只有一个消费者和生产者的情况下可以保证消息的顺序性
- 保证顺序性
- 业务方使用MQ之后做进一步处理,比如在消息体内做全局有序标识(类似
Sequence ID
)来实现
- 业务方使用MQ之后做进一步处理,比如在消息体内做全局有序标识(类似
- 顺序性被打破的常见情形
- 消息分发
-
消息传输保障
一般消息中间件的传输保障分为三个级别
At most once
:最多一次,消息可能会丢失单绝不会重复传输At least once
:最少一次,消息绝不会丢失,但可能重复传输-
Exactly once
:恰好一次,每条消息肯定会被传输且仅传输一次-RabbitMQ支持
At most once
和At least once
-
At most once
实现需要考虑以下方面:生产者需要开启事务机制或者
confirm
机制,保证消息可以可靠传输到MQ中生产者需要配合使用
mandatory
参数或者备份交换器来确保消息能够从交换器路由到队列中,进而能保存下来不被丢弃消息和队列都需要进行持久化处理,以确保MQ服务器在遇到异常情况时不会丢消息
消费者在消费消息的同时将
autoAck
设置为false,通过手动确认的方式去确认已经正确消费消息以避免在消费端引起不必要的消息丢失
At least once
无需考虑以上方面,生产者随意发送,消费者随意消费,不过很难保证消息不会丢失-
Exactly once
是RabbitMQ目前无法保障的- 重复场景
- 消费完一条消息后向MQ发送ACK时网络断开或者其他原因MQ没有收到ACK,MQ不会将此条消息标记删除。重新建立连接后消费者还是会消费到这条消息
- 生产者使用confirm机制时发送完一条消息等待MQ返回确认图通知时网络断开生产者捕获到异常情况选择重新发送,这样MQ有两条同样的消息
- 去重策略
- 一般是业务客户端实现,比如引用
GUID(Globally Unique Identifier)
,需要引入集中式缓存会增加依赖复杂度。实际生产环境中,业务方根据自身的业务特性进行去重,比如业务消息本身具备幂等性,或者借助Redis等进行去重。
- 一般是业务客户端实现,比如引用
- 重复场景
RabbitMQ分布式
-
分布式部署方式(非互斥)
-
集群
- 集群迁移
- 元数据重建
- 数据迁移
- 与客户端连接的切换
- 集群迁移
Federation
-
Shovel
能够可靠、持续地从一个Broker中的队列拉取数据转发到另一个Broker中的交换器,,优势在于
- 松耦合:Shovel可以移动位于不同管理域中的Broker上的消息,这些Broker包含不同的用户和vhost,也可以使用不同的RabbitMQhe Erlang版本
- 支持广域网:Shovel插件同样基于AMQP协议在Broker之间进行通信,被设计成可以容忍时断时续的连通情形并且能够保证消息的可靠性
- 高度定制:当Shovel成功连接后可以对其进行配置以执行相关AMQP命令
-
-
消息堆积
- 双刃剑
- 适量堆积可以有削峰、缓存只用
- 堆积严重可能影响其他队列的使用导致整体服务质量下降
- 处理方案
- 清空队列或者采用空消费程序丢弃部分消息(不重要的数据)
- 增加下游消费者的消费能力
- 后期优化代码逻辑(远水解不了近渴)
- 增加消费者实例数
- 通过Shovel把队列中的消息移交给另一个集群
- 双刃剑
-
队列结构
-
组成
rabbit_amqqueue:复杂协议相关的消息处理即接收生产者消息、向消费者交付消息、处理消息的确认(包括生产端的confirm和消费端的ack)
backing_queue:消息存储的具体形式和引擎,向rabbit_amqqueue_process提供相关接口以供调用
-
队列消息状态
-
alpha:消息内容(消息体、属性和headers)和消息索引都存储在内存中
- 最耗内存但很少消耗CPU
beta:消息内容保存在磁盘中,消息索引保存在内存中
-
gamma(持久化的消息才有):消息内容保存在磁盘中,消息索引在磁盘和内存中都有
- 开启confirm时只有到了gamma状态才会确认该消息已被接收
-
delta:消息内容和索引都在磁盘中
- 基本不消耗内存但是需要更多的CPU和磁盘IO操作
对于持久化的消息,消息内容和消息索引都必须先保存在磁盘上,才会出于上述状态中的一种
-
-
-
惰性队列
- 设计场景
- 默认情况下生产者将消息发送到MQ的时候队列中的消息会尽可能存储在内存中这样可以更快速地将消息发送给消费者。
- 即便是持久化的消息,在被写入磁盘的同时也会在内存中驻留一份备份。
- 当MQ需要释放内存的时候会将内存中的消息换页至磁盘中,这个操作会消费较长时间且阻塞队列操作,进而无法接收新的消息
- 设计目标
- 尽可能将消息存入磁盘中,在消费者消费到相应的消息时才被加载到内存中
- 能够支持更长的队列,即支持更多的消息存储
- 设计场景
-
镜像队列
-
可用性场景
- 消息在发送之后和被写入磁盘并执行刷盘动作之间存在一个短暂且会发生问题的时间窗,即使通过
confirm
机制能让客户端知道哪些消息已存入磁盘但是一般不希望因单点故障导致服务不可用
- 消息在发送之后和被写入磁盘并执行刷盘动作之间存在一个短暂且会发生问题的时间窗,即使通过
-
主从结构
rabbitMQ主从结构.png
-
- 将队列镜像到集群中的其他Broker节点,如果集群中的一个节点失效队列能自动切换到镜像中的另一个节点上以保证服务的可用性
- slave会准确按照master执行命令的顺序进行动作,故slave与master上维护的状态应该是相同的。如果master由于某种原因失效,那么“资历最老”(加入时间最长)的slave会被提升为新的master。
- 发送到镜像队列的所有消息会被同时发往master和所有slave上,如果此时master挂了消息还会在slave上这样slave提升为master的消息消息也不会丢失。
- 消费者与slave建立连接并进行订阅消费时,实质上都是从master上获取消息,只不过看起来是从slave上消费而已。大多的读写压力都是落在了master。
- 这里的master和slave是针对队列而言的,队列可以均匀散落在集群的各个Broker节点以达到负载均衡的目的
- 真正的负载是针对实际的机器而言的而不是内存中驻留的队列进程
- 至于为什么不像Mysql一样读写分离,RabbitMQ从编程逻辑上完全可以实现但是得不到更好的收益即不能进一步优化负载,却会增加编码实现的复杂度增加出错可能
-
确认机制
- RabbitMQ镜像队列支持
confirm
和事务两种机制- 在事务机制中,只有前事务在全部镜像中执行之后客户端才会收到提交成功的消息。
- 在confirm机制中,生产者进行当前确认的前提是该消息被全部进行所接收了。
- RabbitMQ镜像队列支持
-
节点机制
- master挂了
- 与master连接的客户端连接全部断开
- 选举最老的slave作为新的master,因为最老的slave与旧的master之间的同步状态应该是最好的。如果此时所有slave处于未同步状态则未同步的消息会丢失
- 新的master重新入队所有unack的消息,因为新的slave无法区分这些unack消息是否已经达到客户端,或者是ack信息丢失在老的master链路上。出于消息可靠性的考虑重新入队所有unack的消息,客户端可能会有重复消息
- master挂了
-
网络分区
- 设计思路
- 镜像队列有比较强的一致性,如果出现网络波动或者网络故障等异常情况整个数据链的性能大大降低,所以引入网络分区来讲异常的节点剥离出整个分区,以确保MQ服务的可用性及可靠性。等待网络恢复后进行相应的处理将之前异常节点加入集群中。
- 判定思路
- 如果某节点出现网络故障,或者是端口不通,则会致使与此节点的交互出现中断,这里会有个超时判定机制
- 设计思路