消息队列 | 青训营笔记

74 阅读14分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 13 天

一、本堂课重点内容

  • 前世今生
  • 消息队列-Kafka
  • 消息队列-BMQ
  • 消息队列-RocketMQ

二、详细知识点介绍

思考问题

对于以下四个场景问题,该如何解决?

  1. 系统崩溃
  2. 服务处理能力有限
  3. 链路耗时长尾
  4. 日志如何处理

系统崩溃

面对存储行为服务崩溃的时候,主要的解决方案就是解耦

使用消息队列将存储行为服务和消息队列解耦,当存储行为服务崩溃的时候,消息队列仍然可以正常工作,将消息存储到磁盘中,当存储行为服务恢复的时候,再从消息队列中读取消息,进行处理。

image.png

服务处理能力有限

面对许多请求同时到达的时候,服务处理能力有限,这个时候可以使用削峰的方式来解决。

使用消息队列,当请求到达的时候,先将请求放到消息队列中,然后再由消息队列进行处理,这样就可以将请求进行削峰。

image.png

链路耗时长尾

面对链路耗时长尾的时候,可以使用异步的方式来解决。

使用消息队列,当接收到请求后,先将其放入消息队列中,成功后就直接返回下单成功,而不是等待处理完成后再返回,这样就可以将耗时长尾的链路进行异步处理。

image.png

日志如何处理

同样,使用消息队列,将日志用消息队列处理。

image.png

什么是消息队列?

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

image.png

前世今生

消息队列发展历程

image.png

业界消息队列对比

消息队列特点
Kafka分布式的、分区的、多副本的日志提交服务,在高吞吐场景下发挥较为出色
RocketMQ低延迟、强一致、高性能、高可靠、万亿级容量和灵活的可扩展性,在一些实时场景中运用较广
Pulsar是下一代云原生分布式消息流平台,集消息、存储、轻量化函数式计算为一体、采用存算分离的架构设计
BMQ和Pulsar架构类似,存算分离,初期定位是承接高吞吐的离线业务场景,逐步替换掉对应的Kafka集群

消息队列-Kafka

使用场景

image.png

如何使用Kafka

总的来说,分为四个步骤:

  1. 首先需要创建一个Kafka集群,但如果你是在字节工作,那么就不用了,以及有消息团队做好了。
  2. 需要在这个集群中创建一个Topic,并设置好分片数量。
  3. 引入对应语言的SDK,配置好集群和Topic等参数,初始化一个生产者,调用 Send方法,将你的Hello World发送出去。
  4. 引入对应语言的SDK,配置好集群和Topic等参数,初始化一个消费者,调用 Poll方法,接收你的Hello World。

流程图可以描述为如下

graph LR

A(创建集群) --> B(新增Topic)
B --> C(编写生产者逻辑)
C --> D(编写消费者逻辑)

基本概念

  • Topic

    逻辑队列,不同的业务场景可以创建不同的Topic,比如用户行为日志、订单日志等。

    • Partition

      分区,一个Topic可以分为多个Partition,每个Partition是一个有序的队列,每个消息都会被分配到一个Partition中,每个Partition中的消息都是有序的,但不同Partition中的消息是无序的。其中不同分片之间的消息是可以并发处理的,这样可以提高单个Topic的吞吐量。所有Partition中的消息加起来就是这个Topic的总消息量。

      • Offset

        偏移量,每个Partition中的消息都会有一个唯一的Offset,用来标识这个消息在这个Partition中的位置。消费者可以通过Offset来消费指定位置的消息。其严格递增。

      • Replica

        副本,每个Partition都会有多个Replica,每个Replica都会存储这个Partition的数据,但是只有一个Replica是Leader,其他的Replica都是Follower。Leader负责处理这个Partition的读写请求,Follower负责同步Leader的数据(异步操作)。如果Leader宕机,那么其中一个Follower会被选举为新的Leader,这样就保证了集群的高可用性。

        • ISR

          同步副本集合,Leader会维护一个ISR集合,用来记录当前已经同步了数据的Follower(与Leader数据最接近的)。如果Follower在一段时间内没有同步到Leader的数据,那么这个Follower就会被从ISR中移除,如果ISR中的Follower数量落后于Leader太多,那么这个Follower就会被移出ISR。其中Leader就是从ISR中选举出来的,不在ISR中的副本是不允许被选举为Leader的。

  • Cluster

    物理集群,由多个Broker组成。

  • Broker

    一个独立的进程,负责处理消息的生产和消费。每个Broker都会存储多个Topic的部分或全部数据,每个Topic的数据都会被分布到多个Broker上,这样就可以实现数据的冗余和扩展。

  • Producer

    生产者,负责将业务消息发送到Topic中(随机选一个Partition)。

  • Consumer

    消费者,负责从Topic中消费消息。

  • ConsumerGroup

    消费者组,一个消费者组中包含多个消费者,消费者组中的消费者共同消费一个Topic中的消息。

以下为Kafka上述概念及架构的图示(画了俩小时,吐血-.-,看不清楚可以放大查看):

image.png

其中ZooKeeper模块负责存储集群元信息,包括分区分配信息等,Controller计算好的方案都会放在这个地方。

一条消息的自述

从一条消息的视角来看,为什么Kafka能够支撑高吞吐量呢?

首先思考,如果发送一条消息,再接受一条消息,会有什么问题?

很明显,很慢。因此,Kafka引入了一个概念:批量发送消息。也就是说,发送者可以将多条消息打包成一个批次,然后一次性发送Broker,这样就可以减少IO的次数,进而加强发送能力。

那么问题又来了,如果多个消息很大,网络带宽不够用,那么该如何解决?

Kafka引入了一个概念:压缩。也就是说,发送者可以将多条消息压缩成一个批次,然后一次性发送Broker,这样就可以减少网络带宽的使用,进而加强发送能力。目前Kafka支持的压缩算法有:GZIP、Snappy、LZ4、ZSTD。

Broker

数据的存储

Broker接收到消息后,如何写入到磁盘呢?

首先来看一下Kafka最终存储的文件结构:

image.png

在每一个Broker中都分布着不同Topic的不同分片。

顺序写

采用顺序写的方式进行写入,以提高写入效率。

image.png

如何找到消息

Consumer通过发送FetchRequest请求消息数据,Broker会将指定Offset处的消息,按照时间窗口和消息大小窗口发送给Consumer,寻找数据这个细节是如何做到的

偏移量索引

文件名为文件中第一条消息的偏移量,通过偏移量索引(二分查找)来实现快速查找消息的功能,偏移量索引的结构如下:

image.png

时间戳索引

时间戳索引的结构如下:

image.png

其与偏移量索引相比,只是多加了时间索引,也就是先通过二分查找到时间戳对应的偏移量,然后再通过偏移量索引查找到消息。

零拷贝

零拷贝是指,将数据从一个地方拷贝到另一个地方,但是不需要经过用户态和内核态的切换,也就是说,数据直接从内核态拷贝到消费者进程,这样就避免了数据在用户态和内核态之间的多次拷贝,提高了效率。

image.png

Consumer从Breaker中读取数据,通过sendfile的方式,将磁盘读取到os缓冲区后,直接转到socket buffer进行网络发送。

Producer生产的数据持久化到broker,采用mmap文件映射,实现顺序的快速写入。

Consumer

消息的接收端

对于Consumer来说,多个分片可以并发的消费,这样可以大大的提高消费的效率,但需要解决的问题是,Consumer和Partition的分配问题,也就是对于每一个Partition来讲,改由哪一个Consumer来消费的问题,对于这个问题,Kafka提供了两种方式:

Low Level

通过手动进行分配,哪一个Consumer消费哪一个Partition完全由业务来决定。

  • 好处
    • 启动快
  • 缺点
    • 不能动态调整,如果有新的Consumer加入,那么就需要重新分配;如果有Consumer下线,那么分片中的就无法被消费

High Level

也就是自动分配,简单来说,就是在Broker集群中,对于不同的Consumer Group来讲,都会选取一台Broker当作Coordinator,而Coordinator的作用就是帮助Consumer Group进行分片的分配,也叫做分片的rebalance,使用这种方式,如果ConsumerGroup中有发送宕机,或者有新的Consumer加入,那么就会触发rebalance,这样就可以动态的进行分片的分配。

Consumer Rebalance

image.png

image.png

image.png

image.png

image.png

Kafka高效的原因

  • Producer

    批量发送、数据压缩

  • Broker

    顺序写、消息索引、零拷贝

  • Consumer

    Rebalance

数据复制问题

前面提到,对于每个Broker,其上都有不同Topic分区的不同副本,而每一个副本会将其数据存储到该Kafka节点上面,对于不同节点之间,通过副本直接的数据复制,来保证数据的最终一致性,与集群的高可用。

重启操作

当需要对一个机器进行重启时,首先我们会关闭一个Broker,此时如果该Broker上存在副本的Leader,那么该副本将发生Leader切换,切换到其他节点上面的ISR中的Follower副本,而此时,因为数据在不断的写入,对于刚刚关闭重启的Broker来说,其和新的Leader之间一定会存在数据的滞后,此时这个Broker会追赶数据,重新加入到ISR中,当数据追赶完成后,我们需要切回Leader,这一步叫做prefer leader,其目的是为了避免一个集群长期运行后,所有的leade都分布在少数节点上,导致数据的不平衡。

通过上述的分析,我们可以发现对于一个Broker重启来说,需要进行数据的复制,时间成本会比较大,比如一个节点重启需要10分钟,一个集群有1000个节点,如果该集群需要重启需要10000分钟,差不多是一个星期,时间成本非常的大。注意不可并发重启,因为对于一个Topic来说,如果其Partition同时分布在两台机器上,那么这两台机器同时重启,会导致该Topic的数据不可用。

替换、扩容、缩容

  • 替换

    替换的成本比重启的更高,因为替换需要全部重写,而重启只需要追加部分数据即可。

  • 扩容

    成本也很高,因为当分片分配到新的机器上后,也是需要从0开始复制一些新的副本的。

  • 缩容

    成本也很高,因为当分片从某个机器上移除后,由于会重新被分配到集群的其他节点,也是需要从0开始复制一些新的副本的。

总结来说,对于一个集群来说,扩容、缩容、替换的成本都是非常高的,因为这些操作都需要进行数据的复制,而数据的复制是一个非常耗时的操作。

负载不均衡

当一个Broker中的Partition非常大,而导致该Broker机器达到IO瓶颈,此时可能就需要将此Broker上的其他Partition迁移到其他的Broker上,这样可以减少单个Broker的负载,提高集群的整体性能。但是数据的复制又会导致IO的升高,因此解决问题的过程中又带来了新的问题。所以要设计出一个合理的负载均衡策略,是一个非常复杂的问题。

Kafka问题总结

  1. 运维成本高

    Kafka的运维成本非常高,因为Kafka的数据是分布式存储的,所以对于一个集群来说,扩容、缩容、替换的成本都是非常高的,因为这些操作都需要进行数据的复制,而数据的复制是一个非常耗时的操作。

  2. 对于负载不均衡的场景,解决方案复杂

  3. 没有自身的缓存,完全依赖于操作系统的缓存,对于磁盘IO比较高的场景,性能不高

  4. Controller、Coordinator和Broker都在同一进程中,大量的IO会造成性能下降

消息队列-BMQ

BMQ简介

BMQ兼容Kafka协议,存算分离,云原生队列,初期的定位是承接高吞吐的离线业务场景,逐步替换掉对应的Kafka集群。

其架构图如下:

image.png

运维操作对比

具体操作KafkaBMQ
重启需要数据复制,分钟级重启重启后可直接对外服务,秒级完成
替换需要数据复制,分钟级替换,甚至天级别替换后可直接对外服务,秒级完成
扩容需要数据复制,分钟级扩容,甚至天级别扩容后可直接对外服务,秒级完成
缩容需要数据复制,分钟级缩容,甚至天级别缩容后可直接对外服务,秒级完成

实际上所有节点的变更操作,都仅仅是集群元数据的变化,通常情况下都能秒级完成,而真正的数据已经移到下层分布式文件存储去了,所以运维操作不需要关心数据复制所带来的时间成本。

HDFS写文件流程

通过前面的介绍,同一个副本是由多个segment组成,对于单个文件的写入,首先客户端会选择一定数量的DataNode,这个数量是副本数,然后将一个文件写入到这三个节点上,切换到下一个segment后,又会选择三个节点进行写入。这样,对于单个副本的所有segment来将,会随机的分配到分布式文件系统的整个集群中。

也就是之前Kafka中的Replica可以被写入到多个分布式文件系统的节点上,这样就可以解决负载不均衡的问题。

消息队列-RocketMQ

使用场景

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

基本概念

名称KafkaRocketMQ
逻辑队列TopicTopic
消息体MessageMessage
标签Tag
分区PartitionConsumerQueue
生产者ProducerProducer
生产者集群Producer Group
消费者ConsumerConsumer
消费者集群Consumer GroupConsumer Group
集群控制器ControllerNameServer

RocketMQ架构

image.png

数据流通过Producer发送给Broker集群,再由Consumer进行消费,Broker节点有Master和Slave的概念,NameServer为集群提供轻量级服务发现和路由。

存储模型

对于一个Broker来说,所有的消息都会append到一个CommitLog上,然后按照不同的Queue,重新Dispatch到不同的Consumer中,这样Consumer就可以按照Queue进行拉取消费,但是需要注意的是,这里的ConsumerQueue所存储的并不是真实的数据,真实的数据其实只存在CommitLog中,这里存的仅仅是这个Queue的所有消息在CommitLog上的位置,相当于是这个Queue的一个密集索引。

高级特性

事务场景

image.png

事务消息

image.png

除此之外,还有延迟发送、延迟消息以及失败处理的特性。

延迟发送

graph TD
User-->提前编辑菜单
提前编辑菜单-->消息队列
消息队列-->定时发送
定时发送-->接收菜单

延迟消息

graph LR
Producer-->|1.生产信息|CommitLog
CommitLog-->|2.ScheduleTopic|ConsumerQueue1[ConsumerQueue]
ConsumerQueue1-->|3.延迟服务|ScheduleMessage
ScheduleMessage-->|4|CommitLog
CommitLog-->|5.目标Topic|ConsumerQueue
ConsumerQueue-->|6|Consumer

失败处理

在RocketMQ中,拥有消息重试和死信队列两种机制,当消息发送失败时,会进行重试,如果重试次数超过阈值,会将消息发送到死信队列中。

image.png

三、实践练习例子

本次课程没有给出用于实践的例子。

四、课后个人总结

本次课程从消息队列的发展历史、分类以及架构存储模型等方面对消息队列进行了介绍,总共介绍了三个消息队列,分别是Kafka、BMQ和RocketMQ,后两者都对前者进行了改进,有很大的性能提升。

五、引用参考