数据库分布式消息队列

数据库分布式消息队列

作者: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. 有新的数据到来就投放。

至于如何选择,也要结合具体的业务场景来决定。比如,对及时性要求高的数据,可用采用方式 3 来完成。

消息投放对象

不管是 JMS 规范中的 Topic/Queue,Kafka 里面的 Topic/Partition/ConsumerGroup,还是 AMQP(如 RabbitMQ)的 Exchange 等等, 都是为了维护消息的消费关系而抽象出来的概念。本质上,消息的消费无外乎点到点的一对一单播,或一对多广播。另外比较特殊的情况是组间广播、组内单播。比较通用的设计是,不同的组注册不同的订阅,支持组间广播。组内不同的机器,如果注册一个相同的 ID,则单播;如果注册不同的 ID(如 IP 地址+端口),则广播。

例如 pulsar 支持的订阅模型有:

  • Exclusive:独占型,一个订阅只能有一个消息者消费消息。

  • Failover:灾备型,一个订阅同时只有一个消费者,可以有多个备份消费者。一旦主消费者故障则备份消费者接管。不会出现同时有两个活跃的消费者。

  • Shared:共享型,一个订阅中同时可以有多个消费者,多个消费者共享 Topic 中的消息。

  • Key_Shared:键共享型,多个消费者各取一部分消息。

通常会在公共存储上维护广播关系,如 config server、zookeeper 等。

2.队列高级特性

常见的高级特性有可靠投递、消息丢失、消息重复、事务等等,他们并非是 MQ 必备的特性。由于这些特性可能是相互制约的,所以不可能完全兼顾。所以要依照业务的需求,来仔细衡量各种特性实现的成本、利弊,最终做出最为合理的设计。

可靠投递

如何保证消息完全不丢失?

直观的方案是,在任何不可靠操作之前,先将消息落地,然后操作。当失败或者不知道结果(比如超时)时,消息状态是待发送,定时任务不停轮询所有待发送消息,最终一定可以送达。但是,这样必然导致消息可能会重复,并且在异常情况下,消息延迟较大。

例如:

  • producer 往 broker 发送消息之前,需要做一次落地。

  • 请求到 server 后,server 确保数据落地后再告诉客户端发送成功。

  • 支持广播的消息队列需要对每个接收者,持久化一个发送状态,直到所有接收者都确认收到,才可删除消息。

即对于任何不能确认消息已送达的情况,都要重推消息。但是,随着而来的问题就是消息重复。在消息重复和消息丢失之间,无法兼顾,要结合应用场景做出取舍。

消费确认

当 broker 把消息投递给消费者后,消费者可以立即确认收到了消息。但是,有些情况消费者可能需要再次接收该消息(比如收到消息、但是处理失败),即消费者主动要求重发消息。所以,要允许消费者主动进行消费确认。

顺序消息

对于 push 模式,要求支持分区且单分区只支持一个消费者消费,并且消费者只有确认一个消息消费后才能 push 另外一个消息,还要发送者保证发送顺序唯一。

对于 pull 模式,比如 kafka 的做法:

  1. producer 对应 partition,并且单线程。

  2. consumer 对应 partition,消费确认(或批量确认),单线程消费。

但是这样也只是实现了消息的分区有序性,并不一定全局有序。总体而言,要求消息有序的 MQ 场景还是比较少的。

三、Kafka

Kafka 是一个分布式发布订阅消息系统。它以高吞吐、可持久化、可水平扩展、支持流数据处理等多种特性而被广泛使用(如 Storm、Spark、Flink)。在大数据系统中,数据需要在各个子系统中高性能、低延迟的不停流转。传统的企业消息系统并不是非常适合大规模的数据处理,但 Kafka 出现了,它可以高效的处理实时消息和离线消息,降低编程复杂度,使得各个子系统可以快速高效的进行数据流转,Kafka 承担高速数据总线的作用。

kafka 基础概念

  • BrokerKafka 集群包含一个或多个服务器,这种服务器被称为 broker。

  • TopicTopic 在逻辑上可以被认为是一个 queue,每条消费都必须指定它的 Topic,可以简单理解为必须指明把这条消息放进哪个 queue 里。为了使得 Kafka 的吞吐率可以线性提高,物理上把 Topic 分成一个或多个 Partition,每个 Partition 在物理上对应一个文件夹,该文件夹下存储这个 Partition 的所有消息和索引文件。

  • PartitionParition 是物理上的概念,每个 Topic 包含一个或多个 Partition。

  • Producer负责发布消息到 Kafka broker。

  • Consumer消息消费者,向 Kafka broker 读取消息的客户端。

  • Consumer Group每个 Consumer 属于一个特定的 Consumer Group(可为每个 Consumer 指定 group name,若不指定 group name 则属于默认的 group)。

3ac7439a65217ba88247fe07d409e87f.png kafka实现原理6

一个典型的 kafka 集群包含若干 Producer,若干个 Broker(kafka 支持水平扩展)、若干个 Consumer Group,以及一个 zookeeper 集群。Producer 使用 push 模式将消息发布到 broker。consumer 使用 pull 模式从 broker 订阅并消费消息。多个 broker 协同工作,producer 和 consumer 部署在各个业务逻辑中。kafka 通过 zookeeper 管理集群配置及服务协同。

这样就组成了一个高性能的分布式消息发布和订阅系统。Kafka 有一个细节是和其他 mq 中间件不同的点,producer 发送消息到 broker 的过程是 push,而 consumer 从 broker 消费消息的过程是 pull,主动去拉数据。而不是 broker 把数据主动发送给 consumer。

Producer 发送消息到 broker 时,会根据 Paritition 机制选择将其存储到哪一个 Partition。如果 Partition 机制设置合理,所有消息可以均匀分布到不同的 Partition 里,这样就实现了负载均衡。如果一个 Topic 对应一个文件,那这个文件所在的机器 I/O 将会成为这个 Topic 的性能瓶颈,而有了 Partition 后,不同的消息可以并行写入不同 broker 的不同 Partition 里,极大的提高了吞吐率。

Kafka 特点

优点:

  • 高性能:单机测试能达到 100w tps

  • 低延时:生产和消费的延时都很低,e2e 的延时在正常的 cluster 中也很低

  • 可用性高:replicate+ isr + 选举 机制保证

  • 工具链成熟:监控 运维 管理 方案齐全

  • 生态成熟:大数据场景必不可少 kafka stream

不足:

  • 无法弹性扩容:对 partition 的读写都在 partition leader 所在的 broker,如果该 broker 压力过大,也无法通过新增 broker 来解决问题

  • 扩容成本高:集群中新增的 broker 只会处理新 topic,如果要分担老 topic-partition 的压力,需要手动迁移 partition,这时会占用大量集群带宽

  • 消费者新加入和退出会造成整个消费组 rebalance:导致数据重复消费,影响消费速度,增加延迟

  • partition 过多会使得性能显著下降:ZK 压力大,broker 上 partition 过多让磁盘顺序写几乎退化成随机写

高吞吐机制

顺序存取

如果把消息以随机的方式写入到磁盘,那么磁盘首先要做的就是寻址,也就是定位到数据所在的物理地址,在磁盘上就要找到对应柱面、磁头以及对应的扇区;这个过程相对内存来说会消耗大量时间,为了规避随机读写带来的时间消耗,kafka 采用顺序写的方式存储数据。

页缓存

即使是顺序存取,但是频繁的 I/O 操作仍然会造成磁盘的性能瓶颈,所以 kafka 使用了页缓存零拷贝技术。当进程准备读取磁盘上的文件内容时, 操作系统会先查看待读取的数据是否在页缓存中,如果存在则直接返回数据, 从而避免了对物理磁盘的 I/O 操作;

如果没有命中, 则操作系统会向磁盘发起读取请求并将读取的数据页存入页缓存, 之后再将数据返回给进程。一个进程需要将数据写入磁盘, 那么操作系统也会检测数据对应的页是否在页缓存中,如果不存在, 则会先在页缓存中添加相应的页, 最后将数据写入对应的页。被修改过后的页也就变成了脏页, 操作系统会在合适的时间把脏页中的数据写入磁盘, 以保持数据的 一 致性。

Kafka 中大量使用了页缓存, 这是 Kafka 实现高吞吐的重要因素之 一 。虽然消息都是先被写入页缓存,然后由操作系统负责具体的刷盘任务的, 但在 Kafka 中同样提供了同步刷盘及间断性强制刷盘(fsync),可以通过参数来控制。

同步刷盘能够保证消息的可靠性,避免因为宕机导致页缓存数据还未完成同步时造成的数据丢失。但是实际使用上,我们没必要去考虑这样的因素以及这种问题带来的损失,消息可靠性可以由多副本来解决,同步刷盘会带来性能的影响。

页缓存的好处:

  • I/O Scheduler 会将连续的小块写组装成大块的物理写从而提高性能;

  • I/O Scheduler 会尝试将一些写操作重新按顺序排好,从而减少磁头移动时间;

  • 充分利用所有空闲内存(非 JVM 内存);

  • 读操作可以直接在 Page Cache 内进行,如果消费和生产速度相当,甚至不需要通过物理磁盘交换数据;

  • 如果进程重启,JVM 内的 Cache 会失效,但 Page Cache 仍然可用。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
引用中提到,分布式缓存中间件是一种方便实现缓存的分布式、集群、负载均衡、故障自动转移,并兼容多种缓存存储的中间件。它主要用于解决分布式架构中的缓存环节。而引用中提到的binlog消息队列消费者del cache解决方案是解决缓存一致性问题的一种方法。根据这些引用内容,可以解释以下问题: 分布式缓存是指将缓存数据分布在多个节点上,以提高性能和容错能力。通过将缓存数据存储在离应用程序更近的位置,可以加快数据访问速度和减轻后端数据库的负载。分布式缓存中间件可以帮助实现这种分布式缓存架构,并提供负载均衡、故障转移等功能,使得缓存系统更加稳定和可靠。 分布式消息队列是一种用于在分布式系统中进行异步通信的机制。它可以将消息发送到一个或多个消费者,实现解耦、削峰填谷、异步处理等功能。在分布式架构中,消息队列可以用于解决系统之间的通信问题,将消息发送到对应的消费者进行处理。 所以,分布式缓存和分布式消息队列是两种在分布式系统中常见的组件,分别用于缓存数据和实现异步通信。它们在分布式架构中起到了重要的作用。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [c#消息队列分布式缓存](https://blog.csdn.net/levinspeeder1/article/details/102701004)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *3* [分布式缓存详解](https://blog.csdn.net/qq_44866828/article/details/119919856)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值