这是我参与「第三届青训营 -后端场」笔记创作活动的的第16篇笔记
一、 本堂课重点内容
1.1 前世今生
1.2 消息队列-Kafka
1.3 消息队列-BMQ
1.4 消息队列-RocketMQ
二、 详细知识点介绍
2.1 前世今生
2.1.1 消息队列发展历程
业内消息队列对比:
2.2 消息队列-Kafka
2.2.1 使用场景
2.2.2 如何使用Kafka
第一步:首先需要创建一个Kka集群,但如果你是在字节工作,恭喜你这一步消息团队的小伙伴已经帮你完成了
第二步:需要在这个集群中创建一个Topic,并且设置好分片数量
第三步:引入对应语言的SDK,配置好集群和Topics等参数,初始化一个生产者,调用Send方法,将你的Hello World发送出去
第四步:引入对应语言的SDK,配置好集群和Topics等参数,初始化一个消费者,调用Pol方法,你将收到你刚刚发送的Hello World
2.2.3 基本概念
2.2.3.1 基本概念
Topic:逻辑队列,不同Topic可以建立不同的Topic
Cluster:物理集群,每个集群中可以建立多个不同的Topic
Producer:生产者,负责将业务消息发送到Topic中
Consumer:消费者,负责消费Topic中的消息
ConsumerGroup:消费者组,不同组Consumer消费进度互不干涉
Partition:通常topics会有多个分片,不同分片直接消息是可以并发来处理的,这样提高单个Topic的吞吐
Offset:消息在partition内的相对位置信息,可以理解为唯一lD,在partition内部严格递增
Replica:分片的副本,分布在不同的机器上,可用来容灾,Leader对外服务,Follower异步去拉取leader的数据进行一个同步,如果leader挂掉了,可以将Follower提升成leader再对外进行服务
ISR:意思是同步中的副本,对于Follower来说,始终和leader是有一定差距的,但当这个差距比较小的时候,我们就可以将这个follower副本加入到ISR中,不在ISR中的副本是不允许提升成Leader的
2.2.4 数据复制
下面这幅图代表着Kafka中副本的分布图。途中Broker代表每一个Kafka的节点,所有的Broker节点最终组成了一个集群。整个图表示,图中整个集群包含了4个Broker机器节点,集群有两个Topic,分别是Topic1和Topic2,Topic1有两个分片,Topic2有1个分片,每个分片都是三副本的状态。这里中间有个Broker同时也粉演了Controller的角色,Controller是整个集群的大脑,负责对副本和Broker进行分配
2.2.5 Kafka架构
2.2.6 一条消息的自述
2.2.7 Producer-批量发送
通过压缩,减少消息大小,目前支持Snappy、Gzip、LZ4、ZSTD压缩算法
2.2.8 Broker-数据的存储
2.2.8.1 消息文件结构
数据路径:Topic/Partition/Segment/(log|index timeindex|)
2.2.8.2 磁盘结构
移动磁头找到对应磁道,磁盘转动,找到对应扇区,最后写入。寻道成本比较高,因此顺序写可以减少寻道所带来的时间成本。
2.2.8.3 顺序写
采用顺序写的方式进行写入,以提高写入效率
2.2.8.4 如何找到消息
Consumer通过发送FetchRequest请求消息数据,Broker会将指定Offset处的消息,
按照时间窗口和消息大小窗口发送给Consumer,寻找数据这个细节是如何做到的呢?
目标:寻找offset = 28
1. 二分找到小于目标offset的最大文件。
2. 通过二分找到小于目标offset最大的索引位置,再遍历找到目标offset
3. 二分找到小于目标时间戳最大的索引位置,在通过寻找offset的方式找到最终数据。
2.2.8.5 传统数据拷贝
2.2.8.6 零拷贝
Consumer从Broker中读取数据,通过sendfile的方式,将磁盘读到os内核缓冲区后,直接转到socket buffer进行网络发送。Producer生产的数据持久化到broker,采用mmap文件映射,实现顺序的快速写入
2.2.9 消息的的接收端
对于一个Consumer Group来说,多个分片可以并发的消费,这样可以大大提高消费的效率,但需要解决的问题是,Consumer和Partition的分配问题,也就是对于每一个Partition来讲,该由哪一个Consumer来消费的问题。对于这个问题,我们一般有两种解决方法,手动分配和自动分配。
2.2.9.1 Low Level
第一、手动分配,也就是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
所以Kafka也提供了自动分配的方式,这里也叫做High Level的消费方式,简单的来说,就是在我们的Broker集群中,对于不同的Consumer Group来讲,都会选取一台Broker当做Coordinator,而Coordinator的作用就是帮助Consumer Group进行分片的分配,也叫做分片的rebalance,使用这种方式,如果ConsumerGroup中有发生宕机,或者有新的Consumer加入,整个partition和Consumer都会重新进行分配来达到一个稳定的消费状态
2.2.10 Consumer Rebalance
2.2.11 Kafka-数据复制问题
通过前面的介绍我们可以知道,对于Kafka:来说,每一个Broker上都有不同topic分区的不同副本,而每一个副本,会将其数据存储到该Kafka节点上面。对于不同的节点之间,通过副本直接的数据复制,来保证数据的最终一致性,与集群的高可用。
2.2.12 Kafka-重启操作
举个例子来说,如果我们对一个机器进行重启。首先,我们会关闭一个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-负载不均衡
这个场景当中,同一个Topc有4个分片,两副本,可以看到,对于分片1来说,数据量是明显比其他分片要大的,当我们机器IO达到瓶颈的时候,可能就需要把第一台Broker.上面的Partition.3迁移到其他负载小的Broker.上面,接着往下看
但我们的数据复制又会引起Broker1的IO升高,所以问题就变成了,我为了去解决IO升高,但解决问题的过程又会带来更高的IO,所以就需要权衡IO设计出一个极其复杂的负载均衡策略。
2.3 消息队列-BMQ
2.3.1 BMQ简介:兼容Kafka协议,存算分离,云原生消息队列
2.3.2 运维操作对比
2.3.3 HDFS写文件流程
首先客户端写入前会选择一定数量的DataNode,这个数量是副本数,然后将一个文件写入到这三个节点上,切换到下一个segment之后,又会重新选择三个节点进行写入。这样来,对于单个副本的所有segment:来讲,会随机的分配到分布式文件系统的整个集群中。
2.3.4 BMQ文件结构
对于Kafka分片数据的写入,是通过先在Leader上面写好文件,然后同步到Follower上,所以对于同一个副本的所有Segmenti都在同一台机器上面。就会有在之前我们所说到的单分片过大导致负载不均衡的问题,但在BMQ集群中,因为对于单个副本来讲,是随机分配到不同的节点上面的,因此不会存在Kafka的负载不均问题。
2.3.5 Broker-Partition状态机
其实对于写入的逻辑来说,我们还有一个状态机的机制,用来保证不会出现同一个分片在两个Broker.上同时启动的情况,另外也能够保证一个分片的正常运行。首先,Controller做好分片的分配之后,如果在该Broker分配到了Broker,首先会start这个分片,然后进入Recover状态,这个状态主要有两个目的获取分片写入权利,也就是说,对于hdfs来讲,只会允许我一个分片进行写入,只有拿到这个权利的分片我才能写入,第二一个目的是如果上次分片是异常中断的,没有进行save checkpoint,.这里会重新进行一次save checkpoint,然后就进入了正常的写流程状态,创建文件,写入数据,到一定大小之后又开始建立新的文件进行写入。
2.3.5.1 Broker-写文件流程
数据校验: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
我们之前说到了,建立一个新的文件,会随机挑选与副本数量相当的数据节点进行写入,那如果此时我们挑选节点中有一个出现了问题,导致不能正常写入了,我们应该怎么处理,是需要在这里等着这个节点恢复吗,当然不行,谁知道这个节点什么恢复,既然你不行,那就把你换了,可以重新找正常的节点创建新的文件进行写入,这样也就保证了我们写入的可用性。
2.3.6 Proxy
首先Consumer发送一个Fetch Request,然后会有一个Wait流程,那么他的左右是什么呢,想象一个Topic,如果一直没有数据写入,那么,此时consumer就会一直发送Fetch Request,如果Consumer数量过多,BMQ的server端是扛不住这个请求的,因此,我们设置了一个等待机制,如果没有fetch到指定大小的数据,那么proxys会等待一定的时间,再返回给用户侧,这样也就降低了fetch请求的IO次数,经过我们的wait流程后,我们会到我们的Cache里面去找到是否有存在我们想要的数据,如果有直接返回,如果没有,再开始去存储系统当中寻找,首先会Open这个文件,然后通过Index找到数据所在的具体位置,从这个位置开始读取数据。
2.3.7 多机房部署
为什么需要多机房部署,其实对于一个高可用的服务,除了要防止单机故障所带来的的影响意外,也要防止机房级故障所带来的影响,比如机房断点,机房之间网络故障等等。那我们来看看BMQ的多机房部署是怎么做的Proxy->Broker->Meta->HDFS
2.3.8 BMQ-高级特性
2.3.9 泳道消息
BOE:Bytedance Offline Environment,是一套完全独立的线下机房环境
PPE:Product Preview Environment,即产品预览环境
多个人同时测试,需要等待上一个人测试完成
每多一个测试人员,都需要重新搭建一个相同配置的Topic,造成人力和资源的浪费。
对于PPE的消费者来说,资源没有生产环境多,所以无法承受生产环境的流量
解决主干泳道流量隔离问题以及泳道资源重复创建问题
2.3.10 Databus
直接使用原生SDK会有什么问题?
1.客户端配置较为复杂
2.不支持动态配置,更改配置需要停掉服务
3.对于latency不是很敏感的业务,batch效果不佳
1.简化消息队列客户端复杂度
2.解耦业务与Topic
3.缓解集群压力,提高吞吐
2.3.11 Mirror
使用Mirror通过最终一致的方式,解决跨Region读写问题。
2.3.12 Index
如果希望通过写入的Logd、Userld或者其他的业务字段进行消息的查询,应该怎么做?
直接在BMQ中将数据结构化,配置索引DDL,异步构建索引后,通过Index Query服务读出数据。
2.3.13 Parquet
Apache Parquet是Hadoops生态圈中一种新型列式存储格式,它可以兼容Hadoop生态圈中大多数计算框架(Hadoop、Spark等),被多种查询引擎支持(Hve、Impala、Drill等)。
直接在BMQ中将数据结构化,通过Parquet Engine,可以使用不同的方式构建Parquet格式文件。
2.4 消息队列-RocketMQ
2.4.1 RocketMQ基本概念
2.4.2 RocketMQ架构
先说数据流也是通过Producer2发送给Broker集群,再由Consumeri进行消费,Broker节点有Master和Slave的概念。NameServer为集群提供轻量级服务发现和路由
2.4.3 存储模型
接下来我们来看看RocketMQ消息的存储模型,对于一个Broker来说所有的消息的会append到一个CommitLog.上面,然后按照不同的Queue,重新Dispatch到不同Consumer中,这样Consumer就可以按照Queuei进行拉取消费,但需要注意的是,这里的ConsumerQueue所存储的并不是真实的数据,真实的数据其实只存在CommitLog中,这里存的仅仅是这个Queue所有消息在CommitLog上面的位置,相当于是这个Queue的一个密集索引
2.4.4 RocketMQ-高级特性
2.4.4.1 事物场景
先看一下我们最开始说的这个场景,正常情况下,这个下单的流程应该是这个样子,首先我保证库存足够能够顺利-1,这个时候再消息队列让我其他系统来处理,比如订单系统和商家系统,但这里有个比较重要的点,我库存服务和消息队列必须要是在同一个事务内的。
2.4.4.2 事物消息
2.4.4.3 延迟发送
2.4.4.4 消费重试和死信队列
三、 课后个人总结
本堂课介绍了实际工作中应用的消息队列,从场景,原理,优化等方面进行了阐述。
四、 引用参考
青训营官方课件。