消息队列 | 青训营;

94 阅读16分钟

消息队列 | 青训营;

1.入门

指保存消息的一个容器,本质是个队列。但这个队列呢,需要支持高吞吐,高并发,并且高可用。

1.1 消息队列的使用背景

消息队列的使用背景主要源于解决分布式系统中的各种通信和协调问题。分布式系统是由多个相互协作的计算机节点组成的系统,这些节点可能分布在不同的地理位置上。在这样的系统中,各个节点需要相互通信、共享数据以及协调任务,但由于网络延迟、节点故障和负载均衡等因素,直接的节点间通信可能会面临一些挑战。

  • 系统奔溃
  • 服务处理能力有限
  • 链路耗时长尾
  • 日志如何处理

1.2 解决高并发访问的方案

  • 解耦
  • 削峰
  • 异步
  • 日志处理

2.MQ-Kafka

2.1 使用场景

  1. 写作和创作: 我可以帮助你撰写文章、故事、诗歌、博客帖子,提供灵感、建议和润色。
  2. 学术研究: 如果你在进行研究,我可以为你提供背景资料、解释概念、生成论文段落,并协助你整理思路。
  3. 编程帮助: 如果你是开发者,我可以回答关于编程语言、算法、代码示例等方面的问题,帮助你解决问题。
  4. 语言学习: 我可以为你提供语法解释、词汇学习建议,甚至可以与你进行语言交流练习。
  5. 旅行规划: 如果你正在计划旅行,我可以提供目的地信息、建议行程,甚至可以帮你查找飞机票和酒店。
  6. 问题解答: 无论你有关于科学、历史、技术、文化等方面的问题,我都会尽力为你提供答案。
  7. 心理支持: 我可以倾听你的问题,提供情感支持和建议,但请注意,我不是替代专业心理医生的工具。
  8. 创意生成: 如果你需要创意,无论是设计、营销还是其他领域,我可以为你提供新颖的想法。
  9. 教育辅助: 教师可以将我用于教学材料的创建、课堂辅助和学生问题的解答。
  10. 智能助手: 我可以帮助你设置提醒、管理日程、查找信息等日常任务。
  11. 阅读理解: 我可以解答你在阅读中遇到的问题,解释文章或段落的意思。
  12. 娱乐: 如果你想要一个有趣的对话伙伴,我也可以参与有趣的聊天和游戏。

2.2 如何使用Kafka

image.png

Apache Kafka是一种流行的分布式消息队列系统,通常用于处理大量数据流和实现实时数据传输。以下是使用Apache Kafka的一般步骤:

  1. 安装和配置:

    • 下载并安装Kafka:从Apache Kafka官方网站下载适合你系统的版本,并按照官方文档进行安装。
    • 配置服务器:编辑Kafka配置文件(通常是server.properties),配置Zookeeper连接信息、端口号等。
  2. 启动和管理Kafka集群:

    • 启动Zookeeper:Kafka依赖Zookeeper来管理集群的元数据和状态信息,确保Zookeeper已启动。
    • 启动Kafka Broker:在每个Kafka节点上启动Kafka Broker,它们将组成Kafka集群。
  3. 创建Topic:

    • 使用Kafka自带的命令行工具或编程接口来创建Topic。Topic是消息的逻辑分类,用于将消息分类存储。
  4. 生产者(Producer):

    • 创建一个生产者应用程序,用于将消息发布到指定的Topic。生产者可以使用Kafka的客户端库来实现。
  5. 消费者(Consumer):

    • 创建一个或多个消费者应用程序,用于从指定的Topic消费消息。消费者可以以不同的消费者组进行分组。
  6. 消息处理:

    • 消费者从Topic中拉取消息,进行处理,并可以将处理后的结果发送到其他系统。
  7. 容错和伸缩性:

    • Kafka具有分布式、容错和伸缩性特性。你可以根据需求增加或减少Broker,实现水平扩展。
  8. 监控和管理:

    • 使用Kafka自带的工具或第三方工具,可以监控Kafka集群的健康状态、吞吐量等指标。

2.3 partion和replica

在Apache Kafka中,分区(Partitions)和副本(Replicas)是两个重要的概念,用于管理消息的分布和容错性。让我为你解释一下它们的含义和作用:

分区(Partitions): 分区是Kafka中存储和管理消息的基本单元。每个主题(Topic)可以分为多个分区,每个分区在逻辑上类似一个有序的消息队列。每个分区都有一个唯一的标识符(Partition ID),从0开始递增。

为什么要使用分区?

  • 并行处理: 将一个主题分为多个分区允许多个消费者并行地处理消息,提高吞吐量。
  • 容量扩展: 分区允许Kafka集群的扩展,因为每个分区可以分布在不同的节点上。
  • 保留策略: 可以针对每个分区设置消息的保留策略,控制消息在分区内的保留时间。

副本(Replicas): 副本是为了确保数据的高可用性和容错性。每个分区可以有多个副本,其中一个是主副本(Leader Replica),其他副本是从副本(Follower Replicas)。主副本负责处理读写请求,而从副本则仅用于备份和复制数据。

为什么要使用副本?

  • 容错性: 如果主副本发生故障,Kafka可以自动将一个从副本提升为新的主副本,确保消息的可用性。
  • 负载均衡: 副本允许在不同的Broker节点之间分布负载,提高集群的整体性能。
  • 就近读取: 消费者可以从就近的副本读取数据,减少网络延迟。

总结:

2.4 数据复制过程

基本步骤:

  1. 主副本写入:

    • 生产者将消息发送到一个特定的分区,这个分区有一个主副本(Leader Replica)。
    • 主副本负责处理写入请求,将消息追加到分区的日志中。
  2. 副本同步:

    • 除了主副本外,分区还可以有一些从副本(Follower Replicas)。
    • 主副本将写入的消息通过网络发送给从副本。
    • 从副本接收到消息后,会将消息追加到自己的日志中,但不会立即确认给主副本。
  3. 确认和同步:

    • 从副本在复制数据后,会向主副本发送确认消息,告知自己已经接收了消息。
    • 主副本等待所有副本都确认接收了消息,确保数据的复制完整性。
  4. 数据读取:

    • 消费者可以从主副本或从副本中读取消息,但通常会从就近的从副本读取,以减少延迟。
    • 从副本会定期地拉取主副本的新数据,保持数据的同步。
  5. 主副本切换:

    • 如果主副本发生故障,Kafka会自动将一个从副本提升为新的主副本,确保数据的可用性。
    • 新的主副本会从其他副本同步缺失的数据,然后恢复正常的写入和读取。

2.5 batch-数据批量发送

image.png

每个 Kafka 分区(Partition)都对应着一系列的消息,这些消息以日志文件的形式存储在 Broker 的磁盘上。每个分区包含一个或多个消息文件,这些文件被称为段(Segment)。

一个段通常包含一段时间范围内的消息。每当一个分区接收到新的消息,Kafka 会将这些消息追加到当前的段中。当段达到一定大小或时间限制时,Kafka 会创建一个新的段来存储新的消息。

段文件的命名通常包含以下信息:

  • 分区 ID
  • 段的起始偏移量
  • 段的结束偏移量

这些信息有助于 Kafka 跟踪和管理不同段文件之间的消息。

每个段文件包含的内容通常包括:

  • 消息数据:以二进制格式存储的消息内容。
  • 消息索引:用于快速查找消息的索引,包括消息的偏移量、大小等信息。

通过维护消息索引,Kafka 可以高效地进行消息的读取和查询,而无需扫描整个段文件。

2.6 Kafka优点与缺点

优点:

  1. 高吞吐量和低延迟: Kafka 被设计为高性能系统,能够处理大规模的数据流。它能够提供高吞吐量和低延迟的消息传递,适用于实时数据处理和大规模事件驱动的应用。
  2. 持久性和可靠性: Kafka 支持消息持久化,可以将消息存储在磁盘上,即使在节点故障或断电情况下也不会丢失数据。这使得 Kafka 适用于关键业务数据的传输和存储。
  3. 水平扩展: Kafka 可以水平扩展,通过添加更多的节点来提高吞吐量和容量。这使得它能够适应不断增长的数据负载。
  4. 发布-订阅模型: Kafka 使用发布-订阅模型,可以支持多个消费者订阅同一个主题,实现数据的广播和多播。
  5. 数据保留和清理策略: Kafka 允许根据时间或数据大小设置数据保留策略,可以自动删除过期的消息,帮助管理存储空间。
  6. 多语言客户端支持: Kafka 提供了多种编程语言的客户端,使开发者能够在不同的语言中使用 Kafka 进行数据传输和处理。
  7. 社区活跃: Kafka 是一个开源项目,拥有活跃的社区支持,可以获取到丰富的文档、教程和问题解答。

缺点:

  1. 复杂性: Kafka 的设置和维护相对较复杂,特别是对于初学者来说。需要合理的规划、配置和监控,以确保系统稳定运行。
  2. 资源消耗: Kafka 的高性能和高吞吐量需要一定的资源支持,包括内存、磁盘和网络带宽。不合理的使用可能导致资源浪费或性能问题。
  3. 实时性: 尽管 Kafka 通常提供低延迟的消息传递,但在某些情况下,对实时性要求非常高的应用可能需要额外的优化和调整。
  4. 运维复杂性: 部署和维护 Kafka 集群可能需要一定的运维知识和技能,包括监控、故障排除和扩展。
  5. 消息顺序保证: 虽然 Kafka 在单个分区内保证消息的有序性,但在多个分区的情况下,消息的全局有序性可能需要应用层的额外处理。

3. BMQ

image.png

3.1 简介

  • 兼容Kafka协议,存算分离,云原生消息队列

3.2 写入文件流程

image.png

3.3 文件结构对比

image.png

3.4 高级特性

  • 泳道消息
  • Databus
  • Mirror
  • parquet

4.RocketMQ

4.1 使用场景

例如,针对电商业务线,其业务涉及广泛,如注册、订单、库存、物流等;同时,也会涉及许多业务峰值时刻,如秒杀活动、周年庆、定期特惠等

4.2 对比

image.png

4.3 架构

  1. Producer(生产者): 生产者负责产生消息并发送到 RocketMQ 集群。消息可以分为多个主题(Topic),生产者可以指定消息发送到特定的主题。生产者还可以指定消息的标签(Tag)来对消息进行分类。
  2. Broker Server(消息代理服务器): Broker 是 RocketMQ 的核心组件,负责存储消息、转发消息和提供消息的查询和拉取功能。Broker 通过主题(Topic)来组织消息,一个主题可以有多个消息队列(Queue),每个消息队列存储着一部分消息。
  3. Name Server(命名服务器): Name Server 负责管理 Broker 的路由信息。生产者和消费者通过访问 Name Server 获取 Broker 的地址信息,从而实现负载均衡和动态扩缩容。
  4. Consumer(消费者): 消费者从 Broker 拉取消息并进行处理。消费者可以根据主题和标签进行订阅,然后从 Broker 拉取消息进行消费。消费者还可以使用负载均衡和广播模式来处理消息。
  5. Commit Log(消息存储): Commit Log 是 Broker 存储消息的物理文件,消息在存储时会写入 Commit Log。RocketMQ 支持异步刷盘和同步刷盘机制,以确保消息的可靠存储。
  6. Consume Queue(消息消费队列): 每个主题的每个消息队列都有一个对应的消费队列,用于存储消息的消费进度和消费状态。消费者从消费队列拉取消息进行消费。
  7. Index File(消息索引文件): 消息索引文件存储消息的索引信息,用于加速消息的查询操作。消费者可以通过索引文件快速定位和拉取消息。

4.4 事务场景

image.png

RocketMQ 提供了支持事务的消息发送和处理机制,适用于需要保证消息的可靠传递和一致性的分布式事务场景。在分布式事务中,通常涉及多个步骤的操作,需要保证这些操作要么全部成功,要么全部回滚,以保持数据的一致性。以下是 RocketMQ 中事务场景的一些应用示例和工作原理:

使用场景示例:

  1. 订单支付系统: 在处理订单支付时,需要保证订单支付和账户扣款的操作是一个原子操作,要么都成功,要么都失败。
  2. 库存管理系统: 在商品出库时,需要保证商品减少库存和生成出库记录的操作是一个原子操作,要么都成功,要么都失败。
  3. 分布式应用状态同步: 在多个分布式应用之间需要保持状态同步,例如在微服务架构中,一个服务的状态更新需要通知其他相关服务进行相应操作。

工作原理:

  1. 生产者发送事务消息:生产者发送事务消息时,会将消息发送到 Broker,但消息在 Broker 中不会立即对消费者可见。相反,Broker 会暂时存储消息,同时通知生产者。
  2. 事务状态回查:RocketMQ 会定期执行事务状态回查,向生产者发起回查请求,询问事务消息的状态。生产者需要在回查请求中返回事务的状态,可以是提交或回滚。
  3. 消息提交或回滚:根据生产者返回的事务状态,Broker 决定是提交还是回滚该消息。如果生产者返回提交状态,消息将被标记为可消费;如果生产者返回回滚状态,消息将被删除。

4.5 消费重试和死信队列

消费重试和死信队列是在消息队列系统中常见的处理机制,用于处理消息消费过程中可能出现的异常情况或失败情况。

消费重试:

消费重试是指在消费者尝试处理某条消息时发生错误或失败,系统会自动尝试重新处理该消息一定次数,以期在某个尝试中成功消费该消息。这是为了应对一些临时性的问题,例如网络故障、资源不足等。

消费重试的策略通常包括:

  1. 指数退避: 在每次重试之间引入退避机制,逐步延长重试间隔,以避免短时间内连续失败的情况。
  2. 最大重试次数限制: 设置一个最大重试次数,如果消息超过了指定的重试次数仍然无法消费成功,就可以考虑将其放入死信队列。
  3. 监控和报警: 监控消费重试情况,及时发现和处理异常,以保证系统的稳定性。

死信队列:

死信队列(Dead Letter Queue,DLQ)是一个专门用于存储无法被成功消费的消息的队列。当消息在消费重试达到一定次数后仍然无法被消费成功,系统可以将该消息发送到死信队列中,以便后续的人工分析和处理。

使用死信队列的优势包括:

  1. 问题分析: 将无法消费的消息发送到死信队列,可以帮助开发人员分析问题,找出为什么无法消费成功的原因。
  2. 数据保留: 死信队列可以保留未成功消费的消息,防止数据丢失。
  3. 人工处理: 死信队列中的消息可能需要人工介入来处理,可能是修复问题、清理数据或进行其他操作。
  4. 隔离影响: 无法消费的消息不会影响正常消费流程,因为它们被放入了独立的死信队列。

4.6 Go使用RocketMQ

RocketMQ 提供了针对多种编程语言的客户端库,包括 Go。以下是在 Go 中使用 RocketMQ 的基本步骤:

  1. 安装 RocketMQ Go 客户端库: 首先,您需要在您的 Go 项目中引入 RocketMQ 的 Go 客户端库。您可以使用 Go 的模块管理工具来获取和管理依赖项。通常,您需要在项目的模块文件中添加依赖项的条目,然后运行 go mod tidy 命令来获取所需的依赖项。
  2. 配置 RocketMQ 连接信息: 在您的 Go 代码中,您需要配置连接到 RocketMQ 服务器的信息,包括 Name Server 地址、生产者和消费者的配置等。
  3. 使用生产者发送消息: 如果您想要发送消息到 RocketMQ,您需要创建一个生产者并使用它发送消息。以下是一个简化的示例:
 goCopy codepackage main
 ​
 import (
     "fmt"
     "github.com/apache/rocketmq-client-go/v2/producer"
 )
 ​
 func main() {
     p, _ := rocketmq.NewProducer(
         producer.WithNameServer([]string{"your_nameserver"}),
     )
     defer p.Shutdown()
 ​
     err := p.Start()
     if err != nil {
         fmt.Println("Producer start error:", err.Error())
         return
     }
 ​
     msg := &producer.Message{
         Topic: "your_topic",
         Body:  []byte("Hello, RocketMQ!"),
     }
 ​
     result, err := p.SendSync(context.Background(), msg)
     if err != nil {
         fmt.Println("Send message error:", err.Error())
         return
     }
 ​
     fmt.Printf("Send message result: %v\n", result)
 }
  1. 使用消费者接收消息: 如果您想要从 RocketMQ 接收和处理消息,您需要创建一个消费者并注册消息处理函数。以下是一个简化的示例:
 goCopy codepackage main
 ​
 import (
     "context"
     "fmt"
     "github.com/apache/rocketmq-client-go/v2/consumer"
 )
 ​
 func main() {
     c, _ := rocketmq.NewPushConsumer(
         consumer.WithNameServer([]string{"your_nameserver"}),
     )
     defer c.Shutdown()
 ​
     err := c.Subscribe("your_topic", consumer.MessageSelector{}, func(ctx context.Context, msgs ...*primitive.MessageExt) (consumer.ConsumeResult, error) {
         for _, msg := range msgs {
             fmt.Printf("Received message: %s\n", msg.Body)
         }
         return consumer.ConsumeSuccess, nil
     })
 ​
     if err != nil {
         fmt.Println("Subscribe error:", err.Error())
         return
     }
 ​
     err = c.Start()
     if err != nil {
         fmt.Println("Consumer start error:", err.Error())
         return
     }
 ​
     select {}
 }