消息队列 | 青训营笔记

125 阅读18分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的第15篇笔记

消息队列简介

消息队列使用方式

image-20220604103452568image-20220604103501130

image-20220604103615666 image-20220604103625186

image-20220604103637277image-20220604103700638

image-20220604103737697

系统崩溃,也就是在用户所有行为记录的时候,服务器需要保存用户的行为的时候,数据库存储服务发生了故障
削峰,使用消息队列实现消息的削峰,将请求存储到消息队列中,服务只取其中的前10个请求

服务处理能力有限,当服务器有需要限制处理的请求数的时候
解耦,记录用户行为是使用消息队列实现,这时候就算存储服务宕机,消息也会保留在消息队列中,不会影响系统的整个流程

链路耗时长尾
异步,将需要快速响应的服务与慢反应的服务进行解耦,使用消息队列进行消息的传递

日志如何处理
日志处理,将写出的日至进行分析,使用消息队列传递到其他组件中进行分析处理,

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

消息队列发展历程

image-20220603201151180

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

消息队列-Kafka

基本概念

使用场景:

  • 日志搜集
  • Metrics数据
  • 用户行为
image-20220604150129316
基本构成:
	Topic:逻辑队列,不同的业务场景可以建立不同的Topic,对于这个业务场景的话,不同的业务场景中的数据都存在业务对应的topic当中
	Partition:Topic的分区,一个Topic中包含的消息可以分别分发到不同的partition之中,以用于消息的并发处理,也就是说一个Topic可以同时接收和发送多个消息
	Cluster:物理集群,每个集群中可以建立不同的Topic
	Producer:生产者,负责将业务消息发送到Topic中
	Consumer:消费者,负责消费Topic中的消息
	ConsumerGroup:消费者组,不同组的Consumer消费进度互不干涉,比如说ConsumerGroup1与ConsumerGroup2接收同一个Topic,他们的进度互不干扰
image-20220604152028110
Offset:消息在partition内的相对位置信息,可以理解为唯一ID,在partition内部严格递增,这样也可以保证消息在队列中的顺序性
image-20220604152611612
Replica:Partition这一层下面还有一层副本的概念,也就是每一个Partition分区有多个Replica副本,实际上每个副本都会分布到集群中的不同机器上,以此来达到容灾的作用。并且每个副本都有自己的角色,分别是Leader与Follower这两种角色,Leader这个角色是真正对外提供生产消费的,Follower会不断从Leader中的数据同步到自身,ISR(In-Sync Replicas)也就是Follower与Leader之间同步的差距(有两种差距分别是offset的差距、时间戳的差距)不大的集合。当Leader发生宕机,新的Leader Replica将会从ISR中的其他Follower选出。

image-20220604154513747

Broker:代表着每一个Kafka集群中的节点,所有的Broker节点最终组成了一个集群。
图中整个集群,包含了4个Broker机器节点,集群有两个Topic,分别是Topic1与Topic2,Topic1有两个分片而Topic2有一个分片,每个分片都是三副本的状态。
这里中间有一个Broker同时也扮演了Controller的角色,Controller是整个集群中的大脑,负责将不同的分片Partition中的不同副本Replica分配到不同节点Broker上

Kafka架构

image-20220604155846159

消息的发送

问题
发送一条消息,等到成功后在发一条会有什么问题
如果消息量很大,网络带宽不够用,如何解决

批量发送消息并且数据压缩,通过批量发送可以减少网络IO次数,通过压缩可以减少消息大小从而减少网络带宽的压力
目前数据压缩支持Snappy、gzip、LZ4、ZSTD压缩算法

数据的存储

image-20220604160839776

对于每一个节点Broker,其分配到的数据实际上是一个Topic中的Partition的一个副本,其副本实际上是以Log的形式记录到磁盘中的

存储每一个副本中的数据都是以log的形式存在,对于每一个log其本身也会按照一定规律切分为一个个的LogSegment日至段,也是便于即时清理过期的数据,每一个日至段中会有一个log日志文件、index偏移量索引文件、timeindex时间戳索引文件和其他文件
偏移量索引文件实际上是记录了偏移量和log日志文件中的偏移量之间的对应关系,而时间戳索引文件则是建立在index偏移量索引文件之上的二级索引,也就是保存了时间戳与偏移量的对应关系


Broker会采用顺序写的方式写入,以提高写入效率

数据的读取

image-20220604162032170 image-20220604165536934

image-20220604165757280image-20220604170002419

Consumer通过发送FetchRequest请求消息数据,Broker会将指定的Offset处的消息,按照时间窗口和消息大小窗口发送回Consumer

寻找offset处的文件
第一步,通过二分找到小于等于目标offset的最大文件
第二步,由于kafka索引文件是采用的稀疏索引的方式,所以不一定能找到目标offset,通过二分找到小于等于目标offset的最大索引位置,然后再进行遍历查找

对于时间戳索引本质上就是一层二级索引,时间戳指向的是索引的id,并不是log文件中具体的id

数据拷贝

image-20220604170210113 image-20220604171055775
传统数据拷贝需要进行用户态与内核态的切换
	磁盘需要先拷贝到内核空间中的缓存,然后再切换到内核态将内核中的缓存拷贝到用户缓存,然后切换到用户态将用户态中的数据拷贝到socket文件中,然后再进行数据的发送

而kafka选择的数据拷贝方式可以不经过内核态的切换就可以进行数据拷贝
	在发送数据的时候,本质上是使用了操作系统提供的系统调用sendfile,这个函数的作用就是在内核态中,在两个文件描述符中传递数据,但是这个函数调用的一个注意点就是写入的文件描述符不能是socket文件描述符。在这里的作用就是从磁盘中读到的数据可以直接写到socket文件中
	在写入数据的时候,本质上是使用了操作系统提供的系统调用mmap,这个函数的作用就是,将内核态的一块空间,映射到用户态,从而可以在用户态就可以向内核空间写入数据。在这里的作用就是减少了一次向磁盘写入数据时的内核态切换

消息的接收

image-20220604200340385

image-20220604200434449

消息在读取的时候,是以Topic为单位进行读取的,尽管一个Topic可能会有多个Partition分区,但是消费者是需要将所有分区的数据都要拉取下来的。
在拉取数据的时候,Consumer是以组为单位进行拉取的,也就是说读取的话是以ConsumerGroup为单位进行读取的,尽管ConsumerGroup中可能会存在多个Consumer。


Low Level:手动指定ConsumerGroup中每一个Consumer拉取的Partition分区,Consumer在读取的时候,需要手动指定具体将要读取哪一个分区
优点:
	这样分配的好处就是启动比较快,因为对于每一个Consumer来说,启动的时候就已经知道了自己应该去消费哪个消费方式,
缺点:
	这种方式带来的弊端就是,如果需要添加删除一些Consumer或者是Partition的话,需要手动改变每一个Consumer的获取分区,但是这种改动在线上又是不可取的,所以线上服务很少使用这种方式


High Level:自动指定每一个Consumer具体拉取哪个Partition分区,简单的说就是每一个ConsumerGroup都会从Broker集群中选一台当做Coordinator,而Coordinator的作用就是帮助ConsumerGroup进行分片的分配,也叫作分片的Rebalance
优点:
	这种方式的优点在于,如果ConsumerGroup中有发生宕机,或者有新的Consumer加入,整个partition和Consumer都会自动重新分配来达到一个稳定的消费状态
缺点:
	而缺点就在于Consumer会牺牲一点性能去询问Coordinator自己应该去拉取哪个消息队列的信息


第一步,Consumer会向Broker Cluster消息队列集群中当前负载最低的Broker,发送一个FindCoordinatorRequest的请求,来找到对应协调者是谁。
第二步,Consumer会向Coordinator发送JoinGroupRequset请求,表示要加入当前ConsumerGroup组中进行统一的消息订阅及分配
第三步,Coordinator会向Consumer组发送JoinGroupResponse响应,并选择一个Consumer当做这个组的Leader,这时候用户也可以携带着自定义的分配方案
第四步,Consumer会向Coordinator发送SyncGroupRequest请求,表示想要获取当前的分配方案,并且Leader也会携带着用户自定义的分配方案

在Consumer确认好分配的方案之后,还会定时的向Coordinator发送HeartbeatRequest心跳包请求,表示自身还在工作。当有一个Consumer宕机的话,就会重新进行rebalance过程,来获取Consumer的分配方案

总之简单来说会向协调者发送两次请求,分别是:
	我想要加入当前的消费者组
	我想要获取我自己的分配方案

Kafka的不足

image-20220604210230130
1. 运维成本高,有数据复制的问题,有重启的问题,重启的时候需要经过Leader切换、数据同步、Leader切回三个步骤,替换扩容缩容都和重启问题类似
2. 负载不均衡的场景,解决方案复杂,当由于一个Broker由于IO量过大而进行partition分区迁移的时候,又会引入新的IO问题导致死循环。
3. 没有自己的缓存,完全依赖Page Cache
4. Controller和Coordinator和Brocker在同一进程,大量IO会造成其性能下降

消息队列-BMQ

BMQ简介

image-20220604210811107

兼容Kafka协议,在原有生产者消费者框架不用改变的情况下,对Kafka消息队列进行升级
存算分离,可以将原有Kafka中的日志文件直接拷贝到新的集群中就可以使用,并支持分布式文件系统
云原生消息队列


HDFS分布式存储系统
ZK,MetaStorageSystem

在原有生产者消费者不变的情况下,新增了一组Proxy Cluster,并且将Controller与Coordinator独立出来,进行分别管理,文件存储选择了分布式存储系统HDFS,Meta Storage System的作用于zookeeper相似,相当于一个监控系统

写请求与原来不同的是,写请求会先经过Proxy,由Proxy将请求发送到真正写数据的Broker Cluster集群中,并由Broker进行数据的持久化
读请求与原来不同的是,读请求会直接经过Proxy,然后由Proxy直接返回读的数据


这里由于Blocker与Proxy是无状态的,所以他们运维能力是极好的,也就是不用为文件的读写付出极大的性能消耗
不论是重启、扩容、缩容、替换,BMQ都是在秒级的程度完成的,也就是在Proxy或者是在Broker中添加一个或删除一个即可

消息的存储

image-20220604225257648

image-20220607105639125

image-20220607150857094

image-20220607150440158

消息的存储会通过分布式文件系统来存储

BMQ使用的是分布式文件系统,也就是HDFS。与Kafka写入不同的是,当Kafka接收到写入的请求的时候,只会将请求打到负责该分区的Leader Partition中,这样会导致大节点的负载均衡问题,也就是一个节点上可能出现大的Partition导致负责的读写请求过多。而在分布式存储HDFS中就不会出现这种问题,比如说在HDFS中有4个DataNode,每一个Partition都会均匀的分布在3个DataNode中,这三个DataNode每一个都可以负责写入和读取,这样就不会出现大Partition导致的IO过高的问题了

这里的写入机制有几个特点:
加锁
checkpoint

broker状态机,为了保证不会出现并发问题,同一个partition在同一时间只会被一个broker进行写入,一般来说这样的操作需要通过加锁来实现,就是通过不同的broker负责不同的partition来实现的。每个broker负责的partiton互不相同,这样在正常情况下就不会出现并发写入的问题了。
并发问题脑裂现象,就是老的broker被主观下线,但是实际上并没有下线的时候,这样就会出现新的broker与老的broker同时写一个partition,也就是会出现并发问题,在这个时候就需要进行加锁了


完整写入流程:
首先由Controller对partition进行一个分配,也就是分配哪个broker该负责哪些partition的写入,然后通知broker这个分配方案,并开始写入。
broker会先执行start操作,之后会进行recover操作,这个操作共有两个作用,
	一个是会先对partition进行一个抢锁操作,在底层HDFS分布式文件系统中,其写入会有一个保护动作,也就是仅允许当前只有一个进程可以操作一个分区,如果发现当前有别的broker正在写入的话,会将写入权抢走。这里可以保证每次写入的broker都是正确的broker写入,由于每次写入都是由Controller来进行一个分配,则每次写入都是由正确的broker进行写入,如果当前有其他broker写入的话,则一定不是正确的broker。
	另一个就是可以保证每次写入的数据都是以正确顺序写入的,由于每次发送过来的offset都是正确的,所以如果存在offset不一致的话,一定是建立索引环节出现了问题。
在经过recover操作之后,就会进入真正的写入流程了,创建一个segment并开始写入,当写入的文件大小到达一定大小的话则创建一个新的segment继续写入,如果出现写入失败则进入failover流程

AppendData:
broker具体写入流程为,先进行数据校验,然后将数据缓存到一个buffer中,当buffer缓存到一定程度的时候,会开启一个线程来完成异步持久化的操作,异步持久化的操作为,先将数据写入网络IO缓存,然后将数据发送出去,然后开始建立index,并根据建立的缓存更新checkpoint也就是相对于整个partition的offset,当写到一定大小之后会开启一个新的segment继续写入。这里写入数据有两种返回方式,一种是写入缓存时候就返回,另一种是持久化后才返回,前一种保证了高可用但是牺牲了一致性,后一种保证了一致性但不能高可用


Failover:
如果写入过程中出现了故障,也就是写入的时候某个DataNode挂掉了,这时候会在新的DataNode创建一个segment继续写入,以此保证程序不会中断掉

消息的读取

image-20220607155908707


接收到一个读取请求后,首先会进入一个wait程序,也就是阻塞式的获取,有一个数据大小的窗口以及一个事件大小的窗口,防止客户端频繁的请求,可以降低一些IO的压力
并且增加了一种中间层,也就是缓存中间件,如果命中则直接返回,否则将请求打入数据库

多机房部署

image-20220607160240470

容灾措施:多机房部署
将数据部署到多个机房中,以防单机房出故障之后还能够继续提供服务。
BMQ软件可以大致分为三层,分别是Proxy层,Broker层,HDFS层。
在读取数据的时候,只会经过Proxy层和HDFS层,如果一个机房出现了故障,Proxy每个节点都可以提供相同的服务,HDFS本身也有容灾机制,当一个机房的数据出现了故障,则会使用其他机房的数据继续向外提供服务
在写入数据的时候,会经过Proxy、Broker、HDFS,如果一个机房出现了故障,虽然Proxy每个节点都可以提供相同的服务,但是所有Broker要合起来才能提供数据读取服务,所以当一个机房宕机时,除了HDFS自身提供的容灾机制,Broker也要提供相应的容灾机制,这个容灾机制是通过Controller来进行维护的,当出现故障的时候,是需要通过Controller来进行重新分配Broker写入的分片

高级特性

image-20220607165403725

image-20220607170042687

image-20220607170035489

泳道消息:
单独开通一个Topic用来承接测试用的消息,这个Topic与线上Topic的配置完全一致。这个泳道可以解决BOE测试中每个测试都需要重新搭建一个相同配置的Topic以及PPE中,消费者无法承接生产环境的流量这两个问题

image-20220607170221038

Databus:封装后使用的BMQ的API

image-20220607170401042

Mirror用来解决跨区域访问的延迟问题

这里使用Mirror来解决跨区域的延迟问题,这里的Mirror相当于一个异步同步软件,每个区域都会分配一套全量的服务器,相当于ThreadLocal,存储当地的信息,然后通过异步同步的方式,牺牲了短暂的一致性来达到高可用

image-20220607170740053

异步持久化到行式的数据库,用于提供查找的能力

image-20220607170851205

提供了列式存储

消息队列-RocketMQ

基本概念

image-20220607170953697

在Topic中有更细分的Tag

提供了事务的功能

架构

image-20220607171109628

NameServer相当于Controller的作用
Master与Slave是相对于机器设备来说的,可看做一个设备是另一个设备的从设备


消息的读取与写入

image-20220607171450866

消息的写入:
这里的Broker与Kafka一样,都有自己负责的Topic的分区,与Kafka不同的是,数据的写入只会写入到本地的一个文件中,从机器会从这个主机器中获取到写入的数据。

消息的读取:
消息在写入之后,会Dispatch其offset到各个的读队列中,以供消费者读取,这里可以存在多个读队列

高级特性

事务场景

image-20220607172044341

image-20220607172252309

两阶段提交:
如果是正常的流程的话:
开启事务 -> 提交事务ack

如果MQ没有收到ack的话会发送一个check请求,让生产者在检查状态之后告知这个事务是提交还是回滚。
如果是回滚,就算消息已经落盘,最终也不会将消息发送给消费者


定时任务

image-20220607172640204

image-20220607172653066

延迟服务:
当有延迟的消息到来的时候,会记录在CommitLog中,这时候消费者不会接收到这个定时消息,这个定时消息会传入一个ConsumerQueue队列中,然后这个队列会将消息导入延迟服务中ScheduleMessage,这个延迟服务可以定时的将消息再发送回日志中,以便于后续Topic可以正确的接收到消息

消息重试

image-20220607193513538

消息重试,消息在消费失败的时候会发送到延时队列中,然后延期重新投递。如果超过了重试次数还没有成功,则会进入死信队列中等待人工处理