当前位置:   article > 正文

分布式消息队列

分布式消息队列

5d8ffe5fb61c13d34c012688186b1c89.gif

作者:vincentchma,腾讯 IEG 后台开发工程师

一、消息队列的演进

分布式消息队列中间件是是大型分布式系统中常见的中间件。消息队列主要解决应用耦合、异步消息、流量削锋等问题,具有高性能、高可用、可伸缩和最终一致性等特点。消息队列已经逐渐成为企业应用系统内部通信的核心手段,使用较多的消息队列有 RabbitMQ、RocketMQ、ActiveMQ、Kafka、ZeroMQ、Pulsar 等,此外,利用数据库(如 Redis、MySQL 等)也可实现消息队列的部分基本功能。

1.基于 OS 的 MQ

单机消息队列可以通过操作系统原生的进程间通信机制来实现,如消息队列、共享内存等。比如我们可以在共享内存中维护一个双端队列:

6b36fb7b546a2a20b61e7e4f5426f03a.png

消息产出进程不停地往队列里添加消息,同时消息消费进程不断地从队尾有序地取出这些消息。添加消息的任务我们称为 producer,而取出并使用消息的任务,我们称之为 consumer。这种模式在早期单机多进程模式中比较常见, 比如 IO 进程把收到的网络请求存入本机 MQ,任务处理进程从本机 MQ 中读取任务并进行处理。

单机 MQ 易于实现,但是缺点也很明显:因为依赖于单机 OS 的 IPC 机制,所以无法实现分布式的消息传递,并且消息队列的容量也受限于单机资源。

2.基于 DB 的 MQ

即使用存储组件(如 Mysql 、 Redis 等)存储消息, 然后在消息的生产侧和消费侧实现消息的生产消费逻辑,从而实现 MQ 功能。以 Redis 为例, 可以使用 Redis 自带的 list 实现。Redis list 使用 lpush 命令,从队列左边插入数据;使用 rpop 命令,从队列右边取出数据。与单机 MQ 相比, 该方案至少满足了分布式, 但是仍然带有很多无法接受的缺陷。

  • 热 key 性能问题:不论是用 codis 还是 twemproxy 这种集群方案,对某个队列的读写请求最终都会落到同一台 redis 实例上,并且无法通过扩容来解决问题。如果对某个 list 的并发读写非常高,就产生了无法解决的热 key,严重可能导致系统崩溃

  • 没有消费确认机制:每当执行 rpop 消费一条数据,那条消息就被从 list 中永久删除了。如果消费者消费失败,这条消息也没法找回了。

  • 不支持多订阅者:一条消息只能被一个消费者消费,rpop 之后就没了。如果队列中存储的是应用的日志,对于同一条消息,监控系统需要消费它来进行可能的报警,BI 系统需要消费它来绘制报表,链路追踪需要消费它来绘制调用关系……这种场景 redis list 就没办法支持了

  • 不支持二次消费:一条消息 rpop 之后就没了。如果消费者程序运行到一半发现代码有 bug,修复之后想从头再消费一次就不行了。

针对上述缺点,redis 5.0 开始引入 stream 数据类型,它是专门设计成为消息队列的数据结构,借鉴了很多 kafka 的设计,但是随着很多分布式 MQ 组件的出现,仍然显得不够友好, 毕竟 Redis 天生就不是用来做消息转发的。

3. 专用分布式 MQ 中间件

随着时代的发展,一个真正的消息队列,已经不仅仅是一个队列那么简单了,业务对 MQ 的吞吐量、扩展性、稳定性、可靠性等都提出了严苛的要求。因此,专用的分布式消息中间件开始大量出现。常见的有 RabbitMQ、RocketMQ、ActiveMQ、Kafka、ZeroMQ、Pulsar 等等。

二、消息队列设计要点

消息队列本质上是一个消息的转发系统, 把一次 RPC 就可以直接完成的消息投递,转换成多次 RPC 间接完成,这其中包含两个关键环节:

1.消息转储;

2.消息投递:时机和对象;

基于此,消息队列的整体设计思路是:

  • 确定整体的数据流向:如 producer 发送给 MQ,MQ 转发给 consumer,consumer 回复消费确认,消息删除、消息备份等。

  • 利用 RPC 将数据流串起来,最好基于现有的 RPC 框架,尽量做到无状态,方便水平扩展。

  • 存储选型,综合考虑性能、可靠性和开发维护成本等诸多因素。

  • 消息投递,消费模式 push、pull。

  • 消费关系维护,单播、多播等,可以利用 zk、config server 等保存消费关系。

  • 高级特性,如可靠投递,重复消息,顺序消息等, 很多高级特性之间是相互制约的关系,这里要充分结合应用场景做出取舍。

91ae62d62960b65cc03cf714ea36643a.png
1.MQ 基本特性
RPC 通信

MQ 组件要实现和生产者以及消费者进行通信功能, 这里涉及到 RPC 通信问题。消息队列的 RPC,和普通的 RPC 没有本质区别。对于负载均衡、服务发现、序列化协议等等问题都可以借助现有 RPC 框架来实现,避免重复造轮子。

存储系统

存储可以做成很多方式。比如存储在内存里,存储在分布式 KV 里,存储在磁盘里,存储在数据库里等等。但归结起来,主要有持久化和非持久化两种。

持久化的形式能更大程度地保证消息的可靠性(如断电等不可抗外力),并且理论上能承载更大限度的消息堆积(外存的空间远大于内存)。但并不是每种消息都需要持久化存储。很多消息对于投递性能的要求大于可靠性的要求,且数量极大(如日志)。这时候,消息不落地直接暂存内存,尝试几次 failover,最终投递出去也未尝不可。常见的消息队列普遍两种形式都支持。

从速度来看,理论上,文件系统>分布式 KV(持久化)>分布式文件系统>数据库,而可靠性却相反。还是要从支持的业务场景出发作出最合理的选择。

高可用

MQ 的高可用,依赖于 RPC 和存储的高可用。通常 RPC 服务自身都具有服务自动发现,负载均衡等功能,保证了其高可用。存储的高可用, 例如 Kafka,使用分区加主备模式,保证每一个分区内的高可用性,也就是每一个分区至少要有一个备份且需要做数据的同步。

推拉模型

push 和 pull 模型各有利弊,两种模式也都有被市面上成熟的消息中间件选用。

1.慢消费

慢消费是 push 模型最大的致命伤,如果消费者的速度比发送者的速度慢很多,会出现两种恶劣的情况:

1.消息在 broker 的堆积。假设这些消息都是有用的无法丢弃的,消息就要一直在 broker 端保存。

2.broker 推送给 consumer 的消息 consumer 无法处理,此时 consumer 只能拒绝或者返回错误。

而 pull 模式下,consumer 可以按需消费,不用担心自己处理不了的消息来骚扰自己,而 broker 堆积消息也会相对简单,无需记录每一个要发送消息的状态,只需要维护所有消息的队列和偏移量就可以了。所以对于慢消费,消息量有限且到来的速度不均匀的情况,pull 模式比较合适。

2.消息延迟与忙等

这是 pull 模式最大的短板。由于主动权在消费方,消费方无法准确地决定何时去拉取最新的消息。如果一次 pull 取到消息了还可以继续去 pull,如果没有 pull 取到则需要等待一段时间重新 pull。

消息投放时机

即消费者应该在什么时机消费消息。一般有以下三种方式:

  1. 攒够了一定数量才投放。

  2. 到达了一定时间就投放。

  3. 有新的数据到来就投放。

至于如何选择࿰

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/喵喵爱编程/article/detail/939068
推荐阅读
相关标签
  

闽ICP备14008679号