1. Message 状态
Message 在投递时,如果当前 Queue 没有 Message,且有 Consumer 已经订阅了这个 Queue,那么该 Message 会直接发送给 Consumer,不会经过 Queue 存储 Message 的这一步
当 Message 无法直接投递给 Consumer 时,Message 会存储在 Queue 中;在 RabbitMQ 中,Queue 中存储的 Message 有4种状态,且 Message 随着系统的变化而不断更新自己的状态
四种状态:
- alpha:消息索引和消息内容都存在内存中
- beta:消息索引存内存,消息内存存磁盘
- gama:消息索引内存和磁盘都有,消息内容存磁盘
- delta:消息索引和内容都存磁盘
alpha 状态:由于所有的东西都保存在内存中,所以这种状态下最消耗内存
delta 状态:由于所有的东西都保存在磁盘中,也就意味着在读取消息时,每次都要进行 I/O 操作;在这种状态下,最消耗 CPU 中的资源
gama 状态:只有持久化消息才会有该种状态
RabbitMQ 会根据负载和 Message 传递的速度,定期的计算在内存中可以存放 Message 的最大数量,当 alpha 状态的 Message 超过计算的最大数量时,就会引起状态的变化
2. 惰性队列
2.1 简介
惰性队列尽可能地将消息存入磁盘中,并在消费者消费到相应的消息的时候才会被加载到内存中
默认情况下,当 Producer 将 Message 发送到 RabbitMQ 的时候,Queue 中的 Message 会尽可能地存在内存中,这样可以更快的将 Message 发送给 Consumer,即便是持久化消息也会在内存中保持一个副本
而当 RabbitMQ 所占用的内存超过限额时,就需要释放内存,此时会将内存中保存的 Message 写到磁盘中;在这个过程中,队列将无法接收到新的消息
而对于惰性队列来说,接收到 Message 之后,会直接将其写入磁盘中,不管此条 Message 是持久化还是非持久化。在使用惰性队列时,会降低内存的消耗,但是不可避免的是频繁的 I/O 操作
但是要注意的是,惰性队列如果存储非持久化的消息,重启后会丢失
当 Message 是持久化消息时,更适合惰性队列
也适合于消费者不能正常消息 还能保证消息接收的吞吐量的场景
2.2 与普通队列对比
普通队列占用内存较多,惰性队列占用内存较少
当对于相同的大量消息来对比的话,惰性队列的耗时会比普通队列的要少。因为普通队列当消息过多的存储在内存中时,会触发将消息写入磁盘的动作。上面有提到,当内存不足,消息写入磁盘时,队列会阻塞
3. 警告和流控
当内存占用超过配置的值或磁盘剩余空间低于配置的值时,RabbitMQ 会暂时阻塞所有 Producer 的连接,并停止接收 Producer 发来的消息,以免服务崩溃。与此同时客户端与服务端的心跳检测也会失效
心跳检测:用来检测通信的对端是否存活
当客户端与 RabbitMQ 之间一段时间没有进行交互,那么服务器将会自动断开与客户端之间的 TCP 连接;心跳检测就是用于检测当前的 TCP 连接是否还在进行交互
其原理是,向客户端发送一个心跳检测包,如果在一段时间内没有回应的话,那么就会断掉本次的 TCP 链接
心跳检测实际上是启用两个进程,分别检测两种情况:
- 定时检测 TCP 连接上是否有数据发送。
如果在一段时间内没有数据发送给客户端,则会发送一个心跳包给客户端,然后循环进行下一次检测 - 定时检测 TCP 连接上是否有数据接收
如果一段时间内没有收到任何数据,则判定为心跳超时,最终会关闭 TCP 连接
3.1 内存警告
在默认情况下,当内存低于40%的时候,RabbitMQ 中的所有 Producer 就会停止发送消息,然后触发将内存中的消息写进磁盘的动作,从而释放内存空间
3.2 磁盘警告
默认情况下,当磁盘剩余空间低于50M的时候,RabbitMQ 会阻塞所有的 Producer ,同时停止将内存中的消息写进磁盘的操作
磁盘空间的检查频率并不是固定的,他会随着上一次的检查结果而变化
当检测到上一次的剩余空间接近临界值时,那么检查的频率会加快,反之
3.3 流量控制
上面的内存警告和磁盘警告中,当低于阈值时发生的动作是针对所有的 Connection 做出的处理;而流量控制是针对单个 Connection
流量控制用来避免消息发送过快而导致服务器难以处理的情况
Connection 在 RabbitMQ 中存在5种状态:
- running:运行中
- flow:流控
- idle:空闲
- blocked:阻塞
- unblocked:未阻塞
当 Connection 的状态为 flow 时,意味着 Connection 的状态在 blocked 和 unblocked 中来回切换
flow 状态其实和 running 状态较大的区别其实只有他们的发送效率
4. 镜像(Mirror)队列
4.1 背景
在现在的高并发场景中,队列一般都是以集群的方式来部署的;但是如果集群中只存在一个 Broker 节点的话,该节点一旦失效,将会导致整体服务也不可用,且会造成消息的丢失
此时应该会有人想到,如果消息都是持久化的消息,是不是当节点失效时,也不会导致消息的丢失呢?
其实不是的,的确是可以将消息设置为可持久化消息,但是在上一篇关于 RabbitMQ 的文章有说过,写入文件的操作不是立刻执行的,具体的触发条件在上一篇文章已经说过,此处不再赘述。在消息发送之后,接着写入磁盘会有一定的时间间隔,我们称其为“时间窗”。如果在这个时间窗内节点发送故障,那么也会导致消息的丢失
所以引入镜像队列,将队列镜像到其他节点上。此时如果集群中一个节点不可用之后,队列会自动切换到另一个镜像队列上,保证服务的可用
在镜像队列的使用中,对每一个配置了镜像的队列都会包含一个主节点(master)和多个从节点(slave)
4.2 工作原理
当消息发送到配置了镜像的队列时,消息会同时往自己的 slave 节点进行同步
如果因为某些原因,master 变成不可用,但是因为消息同步给了 slave ,所以消息并不会丢失,只需等待其中的一个 slave 节点被推举为 master 之后,服务将会重新变成可用
推举的规则也很简单,哪一个 slave 存在的时间最长,那么就会推举他为新的 master
配置了镜像的队列,其发布确认机制也会和普通的队列不同。镜像队列要所有的节点都发送了确认消息之后,才真正的算“确认”
要注意的是,虽然是配置了 slave ,但是平常的所有读写工作,也都是由 master 来完成,slave 只做一个备份数据的工作。哪怕是有 TCP 与 slave 建立了链接,slave 也不会直接对其进行处理,而是将请求做一个转发的动作,让 master 来完成本次链接的请求
如果在一个已存在的镜像队列中添加一个新的节点,默认情况下,新的节点不会接收到数据的同步
除非服务器显示的调用同步数据的命令,那么此时所有的队列都会进入阻塞状态,等待数据同步完给新的节点时,才会恢复正常
在镜像队列启动的时候,也是 master 先启动;如果是 slave 先启动的话,slave 会进入等待状态,如果在指定时间内发现 master 没有启动,那么已经启动的 slave 会自动停止。这个原理只要大家玩过集群,我想应该都会知道的
4.3 宕机原理
当 slave 挂掉之后,与该 slave 相连的客户端全部断开之外,不会有其他的影响
当 master 挂掉之后,会进行如下动作:
- 与 master 的全部客户端断开
- 选举存活时间最长的 slave 作为新的 master,如果此时 slave 未同步,那么未同步的消息会丢失
- 新的 master 重新入队所有未确认的消息,因为可能 master 未能同步已确认的消息,所以有些消息会被重复消费
4.4 架构图
5. 仲裁(Quorum)队列
5.1 背景
RabbitMQ 3.8 版本中的重要特性
在 3.8 版本之前,实现高可用队列的方式只有上面的镜像队列
镜像队列其实解决的问题是:消息同步,其实实现的过程,简单的说就是将消息从 master 复制一份去 slave
但是有没有想过一个问题:一个节点宕机,然后一段时间之后,该节点重新上线,重新上线的节点以前的所有数据都会丢失;此时会面临一个问题,我们要不要将数据重新同步到该队列中?
如果同步的话,同步的过程中,所有的队列都会处于阻塞状态,此时就会堆积大量的消息
如果不同步的话,仅仅让新的消息复制到这个重新上线的队列中,老的消息不进行同步;那么当现在的 master 宕机之后,该队列没有同步所有的消息,会增加消息丢失的风险
而仲裁队列的出现,旨在解决镜像队列之间的性能和同步问题
5.2 工作原理
每个仲裁队列都有多个副本,其中包含一个主副本和多个从副本
每个副本都在不同的 RabbitMQ 节点上
和镜像队列一样,生产者和消费者都只会和主副本进行交互,从副本仅仅作为一个数据的备份
在主副本所在的节点宕机后,在另外节点的从副本才会被选举为新的主副本,然后继续提供服务
之所以叫做仲裁队列,是因为消息的同步和主副本的选举,都要超过半数的副本同意
当生产者发送一条消息的时候,要超过半数的副本将消息写入磁盘以后,才会向生产者发送确认信号;这也意味着,有些比较慢的副本不会影响整个队列的速度
为什么说仲裁队列解决了镜像队列的同步问题?
细心的人应该有看见,副本是将消息写入磁盘的,也就是说,仲裁队列中的消息都是持久化的;当节点重新上线时,直接从上次中断的地方开始在磁盘中复制消息,且复制的过程是非阻塞的。而镜像队列中的消息因为不全是持久化的消息,所以才会出现不同步的问题
要注意的是,如果超过半数的副本丢失,那么队列的数据就代表永久丢失;虽然还存在一些副本,但是队列是没有办法恢复的,只能被强制删除
5.3 带来的问题
5.3.1 磁盘使用
如果有一条消息是要投递到多个队列中,比如使用的交换机的主题交换机或扇形交换机:
普通的队列只会将消息在磁盘中存储一次,其他的队列只会保存这条消息的引用;也就是说,只会对磁盘进行一次写入操作
而在仲裁队列中,每个副本都会将这条消息写入磁盘;也就是说,有多少个副本,就会对磁盘进行多少次的写入操作
所以,在仲裁队列的使用中,要注意磁盘的写入次数,因为 IO 的操作是很消耗性能的
在这个例子中,也可以看出,仲裁队列并不适合与扇形交换机一起使用
5.3.2 内存使用
仲裁队列中的消息虽然都是持久化的,但是所有的消息也会一直保存在内存中
如果不及时的进行消费,内存会一直处于负荷状态,可能会导致生产者停止工作
6. 死信(Dead-Letter)队列
6.1 简介
“死信”:其实也是消息的一种,只不过满足了一些条件之后的一种称呼
消息变成死信,有如下三种情况:
- 消息被拒绝消费
- 消息过期
- 队列达到了最大长度
当消息满足上面的三种条件的其中一种之后,消息就会被定义成“死信”,然后被重新发送到另一个交换机中,这个交换机也被叫做“死信交换机(DLX)”,用于绑定的队列,也被称之为“死信队列”
DLX 与其他正常的交换机没有什么区别,只是用于存放一些 “死信“
其设计目的是为了存储没有被正常消费的消息,便于排查和重新投递
6.2 架构图
7. 延时(Delay)队列
7.1 简介
延时队列存储的是延时消息
不是所有的消息在发送之后,都需要立刻被消费的,要看具体的使用场景
举个很常见的例子:淘宝下单,大家应该都有那种下单之后,但是没有支付,然后跳转到另一个界面;界面里有个倒计时,显示请在多少分钟内完成支付
这其实就是一种延时处理消息的例子
当向延时队列添加元素的时候,会给元素设置上一个延迟时间(Delay)
队列会根据延迟时间作为排序条件,较小的元素会优先放在队列的首部
队列中的元素只有到了设置的延迟时间,才允许从队列中取出