这是我参与「第五届青训营 」伴学笔记创作活动的第 13 天
一、本堂课重点内容
- 前世今生
- 消息队列-Kafka
- 消息队列-BMQ
- 消息队列-RocketMQ
二、详细知识点介绍
思考问题
对于以下四个场景问题,该如何解决?
- 系统崩溃
- 服务处理能力有限
- 链路耗时长尾
- 日志如何处理
系统崩溃
面对存储行为服务崩溃的时候,主要的解决方案就是解耦。
使用消息队列将存储行为服务和消息队列解耦,当存储行为服务崩溃的时候,消息队列仍然可以正常工作,将消息存储到磁盘中,当存储行为服务恢复的时候,再从消息队列中读取消息,进行处理。
服务处理能力有限
面对许多请求同时到达的时候,服务处理能力有限,这个时候可以使用削峰的方式来解决。
使用消息队列,当请求到达的时候,先将请求放到消息队列中,然后再由消息队列进行处理,这样就可以将请求进行削峰。
链路耗时长尾
面对链路耗时长尾的时候,可以使用异步的方式来解决。
使用消息队列,当接收到请求后,先将其放入消息队列中,成功后就直接返回下单成功,而不是等待处理完成后再返回,这样就可以将耗时长尾的链路进行异步处理。
日志如何处理
同样,使用消息队列,将日志用消息队列处理。
什么是消息队列?
消息队列(MQ),指保存消息的一个容器,本质是个队列。但这个队列,需要支持高吞吐、高并发并且高可用。
前世今生
消息队列发展历程
业界消息队列对比
| 消息队列 | 特点 |
|---|---|
| Kafka | 分布式的、分区的、多副本的日志提交服务,在高吞吐场景下发挥较为出色 |
| RocketMQ | 低延迟、强一致、高性能、高可靠、万亿级容量和灵活的可扩展性,在一些实时场景中运用较广 |
| Pulsar | 是下一代云原生分布式消息流平台,集消息、存储、轻量化函数式计算为一体、采用存算分离的架构设计 |
| BMQ | 和Pulsar架构类似,存算分离,初期定位是承接高吞吐的离线业务场景,逐步替换掉对应的Kafka集群 |
消息队列-Kafka
使用场景
如何使用Kafka
总的来说,分为四个步骤:
- 首先需要创建一个Kafka集群,但如果你是在字节工作,那么就不用了,以及有消息团队做好了。
- 需要在这个集群中创建一个Topic,并设置好分片数量。
- 引入对应语言的SDK,配置好集群和Topic等参数,初始化一个生产者,调用
Send方法,将你的Hello World发送出去。 - 引入对应语言的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上述概念及架构的图示(画了俩小时,吐血-.-,看不清楚可以放大查看):
其中ZooKeeper模块负责存储集群元信息,包括分区分配信息等,Controller计算好的方案都会放在这个地方。
一条消息的自述
从一条消息的视角来看,为什么Kafka能够支撑高吞吐量呢?
首先思考,如果发送一条消息,再接受一条消息,会有什么问题?
很明显,很慢。因此,Kafka引入了一个概念:批量发送消息。也就是说,发送者可以将多条消息打包成一个批次,然后一次性发送Broker,这样就可以减少IO的次数,进而加强发送能力。
那么问题又来了,如果多个消息很大,网络带宽不够用,那么该如何解决?
Kafka引入了一个概念:压缩。也就是说,发送者可以将多条消息压缩成一个批次,然后一次性发送Broker,这样就可以减少网络带宽的使用,进而加强发送能力。目前Kafka支持的压缩算法有:GZIP、Snappy、LZ4、ZSTD。
Broker
数据的存储
Broker接收到消息后,如何写入到磁盘呢?
首先来看一下Kafka最终存储的文件结构:
在每一个Broker中都分布着不同Topic的不同分片。
顺序写
采用顺序写的方式进行写入,以提高写入效率。
如何找到消息
Consumer通过发送FetchRequest请求消息数据,Broker会将指定Offset处的消息,按照时间窗口和消息大小窗口发送给Consumer,寻找数据这个细节是如何做到的?
偏移量索引
文件名为文件中第一条消息的偏移量,通过偏移量索引(二分查找)来实现快速查找消息的功能,偏移量索引的结构如下:
时间戳索引
时间戳索引的结构如下:
其与偏移量索引相比,只是多加了时间索引,也就是先通过二分查找到时间戳对应的偏移量,然后再通过偏移量索引查找到消息。
零拷贝
零拷贝是指,将数据从一个地方拷贝到另一个地方,但是不需要经过用户态和内核态的切换,也就是说,数据直接从内核态拷贝到消费者进程,这样就避免了数据在用户态和内核态之间的多次拷贝,提高了效率。
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
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问题总结
-
运维成本高
Kafka的运维成本非常高,因为Kafka的数据是分布式存储的,所以对于一个集群来说,扩容、缩容、替换的成本都是非常高的,因为这些操作都需要进行数据的复制,而数据的复制是一个非常耗时的操作。
-
对于负载不均衡的场景,解决方案复杂
-
没有自身的缓存,完全依赖于操作系统的缓存,对于磁盘IO比较高的场景,性能不高
-
Controller、Coordinator和Broker都在同一进程中,大量的IO会造成性能下降
消息队列-BMQ
BMQ简介
BMQ兼容Kafka协议,存算分离,云原生队列,初期的定位是承接高吞吐的离线业务场景,逐步替换掉对应的Kafka集群。
其架构图如下:
运维操作对比
| 具体操作 | Kafka | BMQ |
|---|---|---|
| 重启 | 需要数据复制,分钟级重启 | 重启后可直接对外服务,秒级完成 |
| 替换 | 需要数据复制,分钟级替换,甚至天级别 | 替换后可直接对外服务,秒级完成 |
| 扩容 | 需要数据复制,分钟级扩容,甚至天级别 | 扩容后可直接对外服务,秒级完成 |
| 缩容 | 需要数据复制,分钟级缩容,甚至天级别 | 缩容后可直接对外服务,秒级完成 |
实际上所有节点的变更操作,都仅仅是集群元数据的变化,通常情况下都能秒级完成,而真正的数据已经移到下层分布式文件存储去了,所以运维操作不需要关心数据复制所带来的时间成本。
HDFS写文件流程
通过前面的介绍,同一个副本是由多个segment组成,对于单个文件的写入,首先客户端会选择一定数量的DataNode,这个数量是副本数,然后将一个文件写入到这三个节点上,切换到下一个segment后,又会选择三个节点进行写入。这样,对于单个副本的所有segment来将,会随机的分配到分布式文件系统的整个集群中。
也就是之前Kafka中的Replica可以被写入到多个分布式文件系统的节点上,这样就可以解决负载不均衡的问题。
消息队列-RocketMQ
使用场景
针对电商业务线,其业务涉及广泛,如注册、订单、库存、物流等;同时,也会涉及许多业务峰值时刻,如秒杀活动、周年庆、定期优惠等。
基本概念
| 名称 | Kafka | RocketMQ |
|---|---|---|
| 逻辑队列 | Topic | Topic |
| 消息体 | Message | Message |
| 标签 | 无 | Tag |
| 分区 | Partition | ConsumerQueue |
| 生产者 | Producer | Producer |
| 生产者集群 | 无 | Producer Group |
| 消费者 | Consumer | Consumer |
| 消费者集群 | Consumer Group | Consumer Group |
| 集群控制器 | Controller | NameServer |
RocketMQ架构
数据流通过Producer发送给Broker集群,再由Consumer进行消费,Broker节点有Master和Slave的概念,NameServer为集群提供轻量级服务发现和路由。
存储模型
对于一个Broker来说,所有的消息都会append到一个CommitLog上,然后按照不同的Queue,重新Dispatch到不同的Consumer中,这样Consumer就可以按照Queue进行拉取消费,但是需要注意的是,这里的ConsumerQueue所存储的并不是真实的数据,真实的数据其实只存在CommitLog中,这里存的仅仅是这个Queue的所有消息在CommitLog上的位置,相当于是这个Queue的一个密集索引。
高级特性
事务场景
事务消息
除此之外,还有延迟发送、延迟消息以及失败处理的特性。
延迟发送
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中,拥有消息重试和死信队列两种机制,当消息发送失败时,会进行重试,如果重试次数超过阈值,会将消息发送到死信队列中。
三、实践练习例子
本次课程没有给出用于实践的例子。
四、课后个人总结
本次课程从消息队列的发展历史、分类以及架构存储模型等方面对消息队列进行了介绍,总共介绍了三个消息队列,分别是Kafka、BMQ和RocketMQ,后两者都对前者进行了改进,有很大的性能提升。