走进消息队列 | 青训营笔记

183 阅读9分钟

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

1.png

消息队列的发展历程

有关事件时间线:

2.png

流行消息队列的对比:

3.png

消息队列 —Kafka

kafka主要使用场景:

  • 离线的消息处理,比如日志信息;
  • 数据分析,如Metrics数据,通过数据的可视化显示判断现在服务的状态。
  • 用户行为,如搜索、点赞、评论、收藏。

使用Kafka的流程:

  1. 创建集群
  2. 新增Topic
  3. 编写生产者逻辑
  4. 编写消费者逻辑

与Kafka相关的基本概念:

  • Topic:逻辑队列,不同Topic可以建立不同的Topic
  • Cluster:物理集群,每个集群中可以建立多个不同的Topic
  • Producer:生产者,负责将业务消息发送到Topic中
  • Consumer:消费者,负责消费Topic中的消息
  • ConsumerGroup:消费者组,不同组的Consumer消费进度互不干涉

概念图:

4.png

Partition内部视角:

5.png

每个Topic中会有多个partition,而每个partition内部都会存储不同的消息,每个消息都有一个单独的offset,作为存储分区,从而能够在partition中有序存储消息。

Offset:消息在partition内部的相对位置信息,可以理解为唯一ID,在partition内部严格递增

Replica:

每个分片有多个Replica,Leader Replica将会从ISR中选出。

6.png

leader角色:用来对外写入或者读取,也就是从生产者传过来的数据会存储到leader,然后消费者也是从leader进行消费。

follow角色:从leader中不断的把数据拉取下来,努力与leader保持一致的状态。如果follow与leader的差距大于限制的最大值,那么follow就会被踢出负载,现在大多以时间作为限制单元。

数据复制

7.png

Kafka架构:

架构图:

8.png

ZooKeeper:负责存储集群元信息,包括分区分配信息等。

一条消息的流程:

一条消息从Producer 生产到了Broker 然后到Consumer 中进行消费,然而在消息传递的过程中,会产生一系列的问题,比如当我们生产消息到Broker中时,如果是按严格顺序逐个发送的话,效率是很难达到要求的,所以我们需要对消息先进行Batch ,将消息进行批量发送,然后减少IO次数,加强发送能力,但是这样的话就会产生消息量很大,网络带宽不够用的问题,因此就需要进行数据压缩,通过压缩,减少消息的大小,这样我们就完成了消息的高效率生产。

那么一条消息是如何从Broker中存储到本地磁盘中呢?

首先介绍一下Broker消息文件的结构:

9.png

其数据路径即为:/Topic/Partition/Segment/(log | index | timeindex | ...)

在访问磁盘时,根据磁盘结构,首先移动磁头找到对应磁道,磁盘转动,找到对应扇区,最后写入。因为寻道成本比较高,所以顺序写可以减少寻道时间所带来的时间成本,因此Kafka中也是采用顺序写的方式进行写入,以提高写入效率。

消息存储在磁盘中,当Consumer 想要读取消息的时候,首先发送FetchRequest请求消息数据,然后Broker会将指定Offset处的消息,按照时间窗口和消息大小窗口发送给Consumer,Broker在寻找目标数据的时候,会二分找到小于目标offset的最大文件,先找到目标文件,然后在文件内部找到目标索引位置,比如用如下图的情况找到offset为28的目标文件,

10.png

首先通过二分找到小于目标offset的最大值,也就是26,然后通过26的索引值找到目标块,该目标块就是一个batch,该batch中含有26、27、28三个offset,也就找到了目标offset。

Broker时间戳索引文件,就是在刚才的方式上加了一个二级索引,先通过时间戳获取目标offset,然后执行与刚才相同的操作找到目标offset。

将数据发送给Consumer

当我们从磁盘中找到数据,那么就是取出数据然后发送给想要使用该数据的consumer ,那么数据是怎么取出并发送给consumer 的呢?

传统数据拷贝方式:

当我们从磁盘中找到了数据,接下来便是通过操作系统内核空间的readBuffer等IO操作将数据从磁盘中取出,然后拷贝到应用Buffer中,然后再通过内核空间的Socket Buffer发送给网卡内存,最后发送给消费者。

但是在传统发送数据的方式下,因为要涉及到内核态和用户态的转换,开销很大,所以在消息传递的时候采用了一系列手段将整个过程进行优化。

  • 零拷贝

    在内核态中拿到磁盘数据后,不用将其拷贝到用户态下的应用空间中,而是可以直接发送给NIC网卡内存,直接发送给消费者

11.png

Consumer的消息接收

12.png

因为每个Consumer Group 都是相互独立的,接收消息时需要主要解决的问题是每个Partition和Consumer的分配问题。

解决手段:

  • 手动分配方式 — Low Level
  • 自动分配方式 — High Level
  • Consumer Rebalance(自动分配方式中协调Consumer Group分配Partition)

Kafka的缺陷

  1. 运维成本高
  2. 对于负载不均衡的场景,解决方案复杂
  3. 没有自己的缓存,完全依赖Page Cache
  4. Controller 和 Coordinator 和 Broker 在同一进程中,大量IO会造成其性能下降

消息队列 — BMQ

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

BMQ架构图:

13.png

Distributed Storage System是为了存算分离而设计的分布式存储系统。

BMQ写文件

BMQ内部采用的HDFS文件系统,以DataNode作为存储节点。

14.png

BMQ的Broker - Partition状态机

15.png

保证对于任意分片在同一时刻只能在一个Broker上存活。

Broker的写文件流程:

16.png

数据校验:CRC校验,参数是否合法

校验完成后,把数据放入Buffer中,通过一个异步的Write Thread线程将数据最终写入到底层的存储系统中。对于业务的写入,可以配置返回方式。

BMQ读取文件

17.png

首先Consumer先发起Fetch Request,然后进入一个Wait流程,这个wait流程的作用是什么呢?如果当我们Topic中一直没有数据写入,但是此时Consumer想要开销一个数据,那么此时发送的Fetch Request信号得不到响应,就会一直发送请求信号,就会使我们的BMQ server端崩溃,所以设定了这个wait流程让不满足要求的proxy在此等待一段时间,减少IO压力。

Wait的两个指标:

  • 时间窗口
  • 数据大小窗口

如果命中了Cache,就会直接返回数据,如果没有命中Cache,便进入磁盘中查找目标数据。

BMQ高级特性:

18.png

所谓的高级特性,就是在本身的基础功能之外,向外提供了一些应对复杂场景的功能。1.

  1. 泳道

  2. Databus的优点:

    1. 简化消息队列客户端复杂度
    2. 解耦业务与Topic
    3. 缓解集群压力,提高吞吐
  3. Mirror:镜像

    19.png

  4. Index

    20.png

  5. Parquet

    是Hadoop生态圈中一种新型列式存储格式,可以兼容Hadoop生态圈中大多数计算框架(Hadoop、Spark等),被多种查询引擎支持(Hive、Impala、Drill等)。

    21.png

消息队列 — RocketMQ

使用场景:实时业务比较多,比如电商业务线,涉及注册、订单、库存、物流等,同时也会涉及许多业务峰值时刻,如秒杀活动、周年庆、定期特惠等。

22.png

基本概念

23.png

RocketMQ基本架构

24.png

存储模型

25.png

RocketMQ的所有消息都会首先发送到Broker中,每个Broker中只有一个CommitLog,那么这个CommitLog就需要承担所有的存储任务,根据不同分区,将数据dispatch到不同的queue中进行存储。ConsumerQueue中的存储序号并不是真实的数据,而是CommitLog中真实数据的索引信息。

高级特性

事务处理:

事务:ACID,即原子性、一致性、隔离性、持久化。

26.png

在编写购物下单逻辑时,用户发起订单,然后库存记录 -1,将消息传递到消息队列,下单成功后由消息队列进行订单记录 +1和通知商家并向用户做出反馈。在这个过程中,库存记录 -1和消息队列对于库存记录的消息接收必须处在一个事务内,下面是事务在RocketMQ中的实现过程:

27.png

延迟消息:

当我们编写定时任务、定时发送的业务时,首先我们提前一天把内容编辑好,然后设置定时任务,等到规定时间时,设定好的定时任务便发送给接受菜单了。那么对于定时任务有没有更好的机制?其实可以把编辑好的内容放入消息队列,然后给这个内容设定好可以消费的时间,那么就实现定时任务了。下面是延迟消息的实现过程:

28.png

处理失败:

如果我们在消费过程中消息处理失败了,该如何处理。RocketMQ对于处理失败设置了两种方案,分别是消费重试死信队列,当发生处理失败时,首先进行消费重试,当重试次数达到重试次数最大限制时,就会进入死信队列,消息进入死信队列后可以在平台上看到这条消息,但是已经不能再进行处理,只有通过人工介入才能判断这个消息为什么处理失败了以及进行后续操作。

29.png