rabbitmq的消息可靠性

基于前面两大系列: mysql系列 以及 分布式系列 的学习。从本篇开始,我们进入实战环节,针对消息队列的研究分析,打通前面的理论基础。mysql 和 分布式系列,实际上存在以下关系:

  • 都是针对有状态服务
  • mysql系列更偏向于底层 Innodb 的实现,重点是单个有状态服务内部的原理
  • 分布式系列更偏向于有状态集群的实现原理

mysql 是开发者普遍接触的软件,一些经典的设计比如 WAL ,Buffer Pool 等也非常值得学习。mysql 系列文章实际上可以对应 单机消息队列系统,分布式系列文章可以对应集群消息队列系统。

为此,接下来的几篇文章,我们会依次学习如下几种消息队列:

  • rabbitmq
  • rocketmq
  • kafka
  • pulsar

由于 rabbitmq 其底层语言是 erlang,不是那么主流的编程语言,也就导致了大厂使用的项目不是很多。而 rocket、kafka、pulsar都是 JVM 系语言,社区会更加丰富。所以,我们就用 rabbitmq 来抛转引玉,研究思路如下:

  1. rabbitmq 整体结构和关键概念(本篇)
  2. rabbitmq 本身的可靠性(本篇)
  3. 消息生产消费过程的可靠性(本篇)
  4. 幂等处理与性能改进(下篇)

这4点基本离不开前面系列文章中提到的知识点,需要多多回顾。

研究 rabbitmq ,首当其冲的就是 AMQP 协议,因为 rabbitmq 就是基于 AMQP 的实现。AMQP 的整体模型如下图(图片来源)。这张图非常直观、形象地展示了单个 rabbitmq 的内部逻辑结构。

生产者发送消息给 Broker(就是rabbitmq),消费者从 Broker 中接收消息。这就是完整的流程,实际也是各类消息队列典型的应用流程。

Broker 内部又包含三种重要概念:

  • queues:存储消息的队列
  • exchange:直接理解为交换机,也可以理解为转发规则引擎,也就是以什么规则将收到的生产消息进行转发
  • bindings:将 queues 和 exchange 关联起来

所以 Broker 的典型应用流程可以粗略地看做是如下几步:

  1. 建立 queues 、exchanges
  2. 将二者关联起来
  3. 生产者发送消息到 Broker,broker 根据生产者指定的 exchange ,将消息转发到符合路由规则的 queue 中
  4. 消费者监听对应 queue,并获取消息进行消费。

关于这三种概念,其实我们重点要看的是 Queue,因为它才是存储消息的地方。准确地说,应该是消息数据的 Repository,是一种抽象,屏蔽了底层存储的实现方式和管理方式等。

Queue 不仅是为消费者服务,当然还有一些是为 Broker 自身服务。RabbitMQ 中的不同 Queue 之所以会表现出不一样的特性,是因为在创建或者使用 Queue 时,指定了一些参数,诸如:

  • Durable :持久化特性
  • Auto-delete:自动删除特性
  • Queue Type:quorum 或者 classic
  • 等等参数

那么在整个流程中,消息的可靠性是如何保证的呢?注意,我们的 目标在可靠性保证 上。

首先来看 Broker 端,Broker 端和以前的知识有非常大的关联性。

对于单机 Broker ,收到消息,消息需要刷到磁盘后,才能告知生产者写入成功。Queue 是顺序写入的特性,而顺序刷盘比随机写入快得多。只要刷到了磁盘,那么 Broker 上的消息就是可靠的。

这个实际上就是前面学习的 Innodb 中的 WAL 机制。但是,在 WAL 机制中,写入磁盘实际是写入到了操作系统的缓存中,然后由操作系统在适当的时机将数据 真正写入 磁盘。

所以,在写入磁盘之前,假如系统宕机,那么数据就会丢失。在 Innodb 中,是通过控制 innodb_flush_log_at_trx_commit 参数来决定刷盘行为。当其为1时,我们可以认为只要 WAL 成功,那么数据一定是存在磁盘中。其底层当然是通过 Linux 的系统调用来决定如何使用 Linux I/O 的。在 Innodb 的整体架构中,我们也看到了一个关键参数 O_DIRECT,这个参数也是在决定着 I/O 的行为。更具体的讨论可以看 stackoverflow上关于系统调用的讨论

所以,事物都是有联系的,如果只着眼于某一个点,而不去对比联系,那么知识点就是分离的,无法构成对应的知识体系。


那么集群 Broker 又是怎么保证数据可靠性的呢?

这个我们也熟悉!先抬头看看天花板,CAP定理就在那里悬着。这里我们追求的是CP,那么CP系统几乎绕不开共识算法。可以说,不论如何设计一致性方案,最终都会投入到分布式共识算法的怀抱。

如上图,RabbitMQ 是集群模式,但是队列是普通队列。这时每个队列只会存在于某一个 Broker 上,Broker 之间复制的仅仅是队列的元数据。当 Broker 宕机后,就会影响一批生产者和消费者。

Tip:元数据就好比 mysql 中的表结构等信息。

那么,我们想提升 A 怎么办呢?A提升,必然导致 C 的下降。RabbitMQ 最开始提出的镜像队列,是一种一致性的解决方案:

镜像队列实际上就是前面学习过的主从同步一致性模型。此外,还有两个亮点:

  1. 可以配置CA比例,比如配置同步到所有节点(all)、配置同步到指定的节点、配置同步到n个节点(n < all)。这也是分布式数据一致性场景中经常用的方案,即可以灵活修改CA比例。

因为只有Broker将消息同步到该队列配置的所有节点上,才会返回写入成功。所以同步的节点数越多,对于生产者来说,遇到 不可用 的概率就更高。

  1. 集群之间通信的方式,类似 Gossip 协议(前文没讲)。比如下图所描述的,节点会将消息发送到相邻的其他节点,依次转发。

镜像队列的主要问题在于:

  1. 同步复制的过程,尤其是配置为all,性能不好
  2. 新加入的节点,不会被同步之前的消息数据,会导致丢失的概率增加

而 rabbitmq 在3.8版本之后,推出了 Quorum 队列,基于的就是 Raft 算法。

raft 算法我们熟悉,起手三样:领导选举,日志复制,以及保障这些过程中数据的安全性(一致性、可靠性)。由于消息队列承载了系统很大的压力,而 Raft 读写只能从领导者节点读取,那么我们该怎么办?

可以拆分成n个 raft 小集群,每个集群只处理一部分数据。集群内,可以把队列看做是又一次的拆分。

以上是单机 rabbitmq 和集群 rabbitmq 关于可靠性的实现。


我们再来看生产者生产消息的可靠性。如果生产者只发送,而不判断是否发送成功,则无法保证生产的消息写入到 rabbitmq 中。当然,在某些业务场景下这也是可以接受的。比如发送短信的功能,用户可以点击发送验证码再次重新发送。

那么想要实现生产消息的可靠性,就一定需要 Broker 端应答,这从逻辑上是成立的。通信过程就是:

可以看到,是对生产者的某个通信通道进行设置(多个通道共享同一个TCP连接,通道是一种更虚拟的概念,类似会话session),让其开启消息确认机制。当消息成功写入后,即可告知(回调)生产者已经写入成功。

什么时候Broker端才认为消息是写入成功了呢?比如对于持久化队列来说,那就是当消息写入磁盘中,才算成功。对于 Quorum 队列,那就是消息成功写入大部分追随者队列,才算成功。

除了消息确认机制外,还可以利用事务消息机制,但是事务消息会严重损耗性能,所以一般还是以消息确认机制为主。

经过上述分析,生产者写入可靠,rabbitmq 单机存储可靠,rabbitmq 集群存储可靠,那么就剩下消费者消费可靠了。


消费者消费可靠的意思是:只有消费者真正消费了这条消息后,rabbitmq 才会将其删除,否则不能删除。同理,肯定要有确认机制,Broker才能知道消息被成功消费。但是收到了确认,就一定意味着消费成功吗?

当然不一定,假如消费者先进行确认应答,然后执行业务逻辑。那么在执行业务之前宕机,则此条消息既不在 Broker 中,也不在消费者端,消息就丢失了。

所以,消费者收到消息后,可以先存储到本地数据库,然后确认应答,最后执行业务逻辑。这样就可以保证消费者端的可靠性。

这也就是为什么 Rabbitmq 说,消息的可靠性需要生产者、Broker、消费者联合起来,才能完整保证。

在下一篇文章中,我们将会看看消息幂等处理和消息处理性能改进。


注:如果对 rabbitmq 很感兴趣的小伙伴,除了 rabbitmq官方文档 外,推荐看 cloudamqp的相关文章 ,质量不错。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

oatlmy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值