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

137 阅读15分钟

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

一、 本堂课重点内容

1.1   前世今生

1.2   消息队列-Kafka

1.3   消息队列-BMQ

1.4   消息队列-RocketMQ

二、 详细知识点介绍

2.1 前世今生

2.1.1 消息队列发展历程

image.png

业内消息队列对比:

image.png

2.2 消息队列-Kafka

2.2.1 使用场景

image.png

2.2.2 如何使用Kafka

第一步:首先需要创建一个Kka集群,但如果你是在字节工作,恭喜你这一步消息团队的小伙伴已经帮你完成了

第二步:需要在这个集群中创建一个Topic,并且设置好分片数量

第三步:引入对应语言的SDK,配置好集群和Topics等参数,初始化一个生产者,调用Send方法,将你的Hello World发送出去

第四步:引入对应语言的SDK,配置好集群和Topics等参数,初始化一个消费者,调用Pol方法,你将收到你刚刚发送的Hello World

2.2.3 基本概念

2.2.3.1 基本概念

image.png

Topic:逻辑队列,不同Topic可以建立不同的Topic

Cluster:物理集群,每个集群中可以建立多个不同的Topic

Producer:生产者,负责将业务消息发送到Topic中

Consumer:消费者,负责消费Topic中的消息

ConsumerGroup:消费者组,不同组Consumer消费进度互不干涉

Partition:通常topics会有多个分片,不同分片直接消息是可以并发来处理的,这样提高单个Topic的吞吐

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

image.png

Replica:分片的副本,分布在不同的机器上,可用来容灾,Leader对外服务,Follower异步去拉取leader的数据进行一个同步,如果leader挂掉了,可以将Follower提升成leader再对外进行服务

ISR:意思是同步中的副本,对于Follower来说,始终和leader是有一定差距的,但当这个差距比较小的时候,我们就可以将这个follower副本加入到ISR中,不在ISR中的副本是不允许提升成Leader的

image.png

2.2.4 数据复制

下面这幅图代表着Kafka中副本的分布图。途中Broker代表每一个Kafka的节点,所有的Broker节点最终组成了一个集群。整个图表示,图中整个集群包含了4个Broker机器节点,集群有两个Topic,分别是Topic1和Topic2,Topic1有两个分片,Topic2有1个分片,每个分片都是三副本的状态。这里中间有个Broker同时也粉演了Controller的角色,Controller是整个集群的大脑,负责对副本和Broker进行分配

image.png

2.2.5 Kafka架构

image.png

2.2.6 一条消息的自述

image.png

2.2.7 Producer-批量发送

image.png

通过压缩,减少消息大小,目前支持Snappy、Gzip、LZ4、ZSTD压缩算法

2.2.8 Broker-数据的存储

2.2.8.1 消息文件结构

image.png

数据路径:Topic/Partition/Segment/(log|index timeindex|)

2.2.8.2 磁盘结构

移动磁头找到对应磁道,磁盘转动,找到对应扇区,最后写入。寻道成本比较高,因此顺序写可以减少寻道所带来的时间成本。

image.png

2.2.8.3 顺序写

image.png

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

2.2.8.4 如何找到消息

Consumer通过发送FetchRequest请求消息数据,Broker会将指定Offset处的消息,

按照时间窗口和消息大小窗口发送给Consumer,寻找数据这个细节是如何做到的呢?

目标:寻找offset = 28

image.png

1.      二分找到小于目标offset的最大文件。

2.      通过二分找到小于目标offset最大的索引位置,再遍历找到目标offset

image.png

3.      二分找到小于目标时间戳最大的索引位置,在通过寻找offset的方式找到最终数据。

image.png

2.2.8.5 传统数据拷贝

image.png

2.2.8.6 零拷贝

Consumer从Broker中读取数据,通过sendfile的方式,将磁盘读到os内核缓冲区后,直接转到socket buffer进行网络发送。Producer生产的数据持久化到broker,采用mmap文件映射,实现顺序的快速写入

image.png

2.2.9 消息的的接收端

image.png

对于一个Consumer Group来说,多个分片可以并发的消费,这样可以大大提高消费的效率,但需要解决的问题是,Consumer和Partition的分配问题,也就是对于每一个Partition来讲,该由哪一个Consumer来消费的问题。对于这个问题,我们一般有两种解决方法,手动分配和自动分配。

2.2.9.1 Low Level

       image.png

第一、手动分配,也就是Kafka中所说的Low Level消费方式进行消费,这种分配方式的一个好处就是启动比较快,因为对于每一个Consumer来说,启动的时候就已经知道了自己应该去消费哪个消费方式,就好比图中的Consumer Group1来说,Consumer1去消费Partition1,2,3,Consumer2,去消费456Consumer:3去消费7,8。这些Consumer再启动的时候就已经知道分配方案了,但这样这种方式的缺点又是什么呢,想象一下,如果我们的Consumer3:挂掉了,我们的7,8分片是不是就停止消费了。又或者,如果我们新增了一台Consumer4,那是不是又需要停掉整个集群,重新修改配置再上线,保证Consumer4也可以消费数据,其实上面两个问题,有时候对于线上业务来说是致命的。

2.2.9.2 High Level

image.png

       所以Kafka也提供了自动分配的方式,这里也叫做High Level的消费方式,简单的来说,就是在我们的Broker集群中,对于不同的Consumer Group来讲,都会选取一台Broker当做Coordinator,而Coordinator的作用就是帮助Consumer Group进行分片的分配,也叫做分片的rebalance,使用这种方式,如果ConsumerGroup中有发生宕机,或者有新的Consumer加入,整个partition和Consumer都会重新进行分配来达到一个稳定的消费状态

2.2.10 Consumer Rebalance

image.png

2.2.11 Kafka-数据复制问题

image.png

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

2.2.12 Kafka-重启操作

image.png

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

可以不可以并发多台重启吗?

不可以。为什么呢,在一个两副本的集群中,重启了两台机器,对某一分片来讲,可能两个分片都在这台机器上面,则会导致该集群处于不可用的状态。这是更不能接受的。

2.2.13 Kafka-替换、扩容、缩容

如果是替换,和刚刚的重启有什么区别,其实替换,本质上来讲就是一个需要追更多数据的重启操作,因为正常重启只需要追一小部分,而替换,则是需要复制整个leader的数据,时间会更长扩容呢,当分片分配到新的机器上以后,也是相当于要从开始复制一些新的副本而缩容,缩容节点上面的分片也会分片到集群中剩余节点上面,分配过去的副本也会从0开始去复制数据以上三个操作均有数据复制所带来的时间成本问题,所以对于Kafka来说,运维操作所带来的时间成本是不容忽视的。

2.2.14 Kafka-负载不均衡

image.png

这个场景当中,同一个Topc有4个分片,两副本,可以看到,对于分片1来说,数据量是明显比其他分片要大的,当我们机器IO达到瓶颈的时候,可能就需要把第一台Broker.上面的Partition.3迁移到其他负载小的Broker.上面,接着往下看

image.png

但我们的数据复制又会引起Broker1的IO升高,所以问题就变成了,我为了去解决IO升高,但解决问题的过程又会带来更高的IO,所以就需要权衡IO设计出一个极其复杂的负载均衡策略。

2.3 消息队列-BMQ

2.3.1 BMQ简介:兼容Kafka协议,存算分离,云原生消息队列

image.png

2.3.2 运维操作对比

image.png

2.3.3 HDFS写文件流程

image.png

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

2.3.4 BMQ文件结构

image.png

对于Kafka分片数据的写入,是通过先在Leader上面写好文件,然后同步到Follower上,所以对于同一个副本的所有Segmenti都在同一台机器上面。就会有在之前我们所说到的单分片过大导致负载不均衡的问题,但在BMQ集群中,因为对于单个副本来讲,是随机分配到不同的节点上面的,因此不会存在Kafka的负载不均问题。

2.3.5 Broker-Partition状态机

image.png

其实对于写入的逻辑来说,我们还有一个状态机的机制,用来保证不会出现同一个分片在两个Broker.上同时启动的情况,另外也能够保证一个分片的正常运行。首先,Controller做好分片的分配之后,如果在该Broker分配到了Broker,首先会start这个分片,然后进入Recover状态,这个状态主要有两个目的获取分片写入权利,也就是说,对于hdfs来讲,只会允许我一个分片进行写入,只有拿到这个权利的分片我才能写入,第二一个目的是如果上次分片是异常中断的,没有进行save checkpoint,.这里会重新进行一次save checkpoint,然后就进入了正常的写流程状态,创建文件,写入数据,到一定大小之后又开始建立新的文件进行写入。

2.3.5.1 Broker-写文件流程

image.png

       数据校验:CRC参数是否合法。校验完成后,会把数据放入Buffer中,通过一个异步的Write Thread线程将数据最终写入到底层的存储系统当中。这里有一个地方需要注意一下,就是对于业务的写入来说,可以配置返回方式,可以在写完缓存之后直接返回,另外我也可以数据真正写入存储系统后再返回,对于这两个来说前者损失了数据的可靠性,带来了吞吐性能的优势,因为只写入内存是比较快的,但如果在下一次flush前发生宕机了,这个时候数据就有可能丢失了,后者的话,因为数据已经写入了存储系统,这个时候也不需要担心数据丢失,相应的来说吞吐就会小一些。我们再来看看Thread的具体逻辑,首先会将Buffer中的数据取出来,调用底层写入逻辑,在一定的时间周期上去flush,flush完成后开始建立Index,也就是offset和timestamp对于消息具体位置的映射关系。Index建立好以后,会save一次checkpoint,也就表示,checkpoint后的数据是可以被消费的辣,我们想一下,如果没有checkpoint的情况下会发生什么问题,如果flush完成,之后宕机,index还没有建立,这个数据是不应该被消费的。最后当文件到达一定大小之后,需要建立一个新的segment文件来写入。

2.3.5.2 Broker-写文件Failover

image.png

我们之前说到了,建立一个新的文件,会随机挑选与副本数量相当的数据节点进行写入,那如果此时我们挑选节点中有一个出现了问题,导致不能正常写入了,我们应该怎么处理,是需要在这里等着这个节点恢复吗,当然不行,谁知道这个节点什么恢复,既然你不行,那就把你换了,可以重新找正常的节点创建新的文件进行写入,这样也就保证了我们写入的可用性。

2.3.6 Proxy

image.png

首先Consumer发送一个Fetch Request,然后会有一个Wait流程,那么他的左右是什么呢,想象一个Topic,如果一直没有数据写入,那么,此时consumer就会一直发送Fetch Request,如果Consumer数量过多,BMQ的server端是扛不住这个请求的,因此,我们设置了一个等待机制,如果没有fetch到指定大小的数据,那么proxys会等待一定的时间,再返回给用户侧,这样也就降低了fetch请求的IO次数,经过我们的wait流程后,我们会到我们的Cache里面去找到是否有存在我们想要的数据,如果有直接返回,如果没有,再开始去存储系统当中寻找,首先会Open这个文件,然后通过Index找到数据所在的具体位置,从这个位置开始读取数据。

2.3.7 多机房部署

image.png

为什么需要多机房部署,其实对于一个高可用的服务,除了要防止单机故障所带来的的影响意外,也要防止机房级故障所带来的影响,比如机房断点,机房之间网络故障等等。那我们来看看BMQ的多机房部署是怎么做的Proxy->Broker->Meta->HDFS

2.3.8 BMQ-高级特性

image.png

2.3.9 泳道消息

image.png

BOE:Bytedance Offline Environment,是一套完全独立的线下机房环境

PPE:Product Preview Environment,即产品预览环境

image.png

多个人同时测试,需要等待上一个人测试完成

image.png

每多一个测试人员,都需要重新搭建一个相同配置的Topic,造成人力和资源的浪费。

image.png

对于PPE的消费者来说,资源没有生产环境多,所以无法承受生产环境的流量

image.png

解决主干泳道流量隔离问题以及泳道资源重复创建问题

2.3.10 Databus

image.png

直接使用原生SDK会有什么问题?

1.客户端配置较为复杂

2.不支持动态配置,更改配置需要停掉服务

3.对于latency不是很敏感的业务,batch效果不佳

image.png

1.简化消息队列客户端复杂度

2.解耦业务与Topic

3.缓解集群压力,提高吞吐

2.3.11 Mirror

image.png

使用Mirror通过最终一致的方式,解决跨Region读写问题。

2.3.12 Index

image.png

如果希望通过写入的Logd、Userld或者其他的业务字段进行消息的查询,应该怎么做?

直接在BMQ中将数据结构化,配置索引DDL,异步构建索引后,通过Index Query服务读出数据。

image.png

2.3.13 Parquet

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

image.png

直接在BMQ中将数据结构化,通过Parquet Engine,可以使用不同的方式构建Parquet格式文件。

2.4 消息队列-RocketMQ

2.4.1 RocketMQ基本概念

image.png

2.4.2 RocketMQ架构

image.png

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

2.4.3 存储模型

image.png

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

2.4.4 RocketMQ-高级特性

     2.4.4.1 事物场景

       image.png

先看一下我们最开始说的这个场景,正常情况下,这个下单的流程应该是这个样子,首先我保证库存足够能够顺利-1,这个时候再消息队列让我其他系统来处理,比如订单系统和商家系统,但这里有个比较重要的点,我库存服务和消息队列必须要是在同一个事务内的。

       2.4.4.2 事物消息

image.png

       2.4.4.3 延迟发送

image.png

image.png

       2.4.4.4 消费重试和死信队列

image.png

三、 课后个人总结

本堂课介绍了实际工作中应用的消息队列,从场景,原理,优化等方面进行了阐述。

四、 引用参考

青训营官方课件。