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

141 阅读10分钟

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

01 消息队列的前世今生

1.1 业界消息队列对比

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

02 消息队列-Kafka

2.1 如何使用kafak

graph TD
创建集群 --> 新增topic --> 编写生产者逻辑 --> 编写消费者逻辑

2.2 基本概念

image.png

  • topic:逻辑队列,不同业务可以创建不同的topic
  • cluster:物理集群,每个集群可以创建多个不同的topic
  • producer:生产者,负责消费topic中的消息
  • consumer:消费者,负责消费topic中的消息
  • consumerGroup:不同消费者组的消费进度互不干扰
  • partition:通常topic会有多个分片,不用分片的消息可以并发处理,可以提高单个topic的吞吐

2.2.1 offset

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

2.2.2 replica

  • 每个分片会有多个replica,leader replica将会从ISR(In-Sync Replicas)中选出。
  • ISR:意思是同步中的副本,对于follower来说,始终和leader有一定的差距,但这个差距较小的时候,我们就可以把这个follower副本加入到ISR中,不在ISR中的副本不允许提升成为leader

2.3 数据复制

image.png 图中broker表示一个kafka节点,多有的kafka节点组成一个集群。这个图中,包含了四个broker机器节点,两个topic,topic1有两个分片,topic有一个分片,每一个分片有三个副本状态。同时这里有一个还扮演着controller的角色,controller是整个集群的大脑,复制对副本和broker进行分配

2.4 kafka架构

image.png

zookeeper:负责存储集群元信息,包括分区分配信息。zookeeper模块其实是存储了集群的元数据信息,比如副本的分配信息,controller计算好的方案都会放到这里

2.5 一条消息的自述

graph TD
producer --> broker --> consumer

2.5.1 发送消息

Q:如果一条一条的发送会有什么问题? 可以批量发送减少IO次数,从而加强发送能力 Q:如果消息量很大,网络带宽不够用,如何解决? 通过压缩,减少消息大小

2.5.2 数据的存储

2.5.2.1 消息文件结构

image.png Q:如何存储到磁盘? 由于磁盘的结构,顺序写入速度是最快的。

2.5.3 如何找到消息

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

  1. 二分查找找到小于目标offset的最大文件
  2. 二分查找到小于目标offset的最大索引位置
  3. 二分找到小于目标时间戳最大的索引位置,再通过寻找offset的方式找到最终的数据

image.png

2.5.4 如何高效的发送数据?

使用零拷贝的方式,通过sendfile,将磁盘读到os内核缓冲区后,直接转到soket buffer进行网络发送, producer生产的数据持久化到broker,采用mmap文件映射,实现顺序的快速写入

2.5.5 partition在consumer group中的分配问题

  • 手动分配,在启动之前就对消费组的成员指定好消费的分区,缺点是不够灵活,在机器宕机和添加机器的时候需要重启服务重新分配
  • 自动分配,在comsumer group中选取一个broker当作一个coordinator负责分片的自动的消费分配,这样的好处是当consummer group中有机器宕机,或者有新的机器加入的时候,不用手动进行配置,比较灵活和方便

2.6 kafka数据复制的问题

对于kafka来说,每一个broker都有不同topic的不同副本,并且这些副本扮演的角色也可能不一样,有的副本可能是leader有的是follower,通过副本之间的数据复制来保证kafka数据的最终一致性,与集群的高可用

image.png

2.7 kafka重启操作

kafka的重启需要经历几个步骤

graph TD
关闭重启 --> leader切换追赶数据 --> 数据同步完成 --> leader回切
  1. 为什么要经历leader回切? 因为是为了避免一个集群在长时间运行之后,所有的leader都集中在了少数的没有重启过的broker上面,这样是为了减少风险,并且减少这些broker的压力
  2. 为什么重启必须一个一个来? 因为如果有一个两副本集群,如果同时重启会造成服务不可用和数据不一致的问题

2.8 kafka替换、扩容、缩容

  • 替换就相当于一个从零开始追赶的重启操作
  • 扩容也是一个从零开始复制分片的操作
  • 缩容是把这个broker的分片从零在别的机器上进行追赶的一个操作 所以kafka的运维成本是很高的,不容忽视。

2.9 kafka负载不均衡的问题

image.png 在这个例子中,这个topic有四个分片两个副本,其中明显分片1的数量要明显多一些,broker1的负载就比较重,所以需要把分片3移动到别的broker上面,但是这样会带来相应的问题,比如,需要设计复杂的负载均衡算法,并且在数据移动的过程中加重了broker的io负担,我们是为了解决io负担的,但是引入了新的io负担

2.10 kafka问题总结

  1. 运维成本高
  2. 对于负载不均衡的场景解决方案复杂
  3. 没有自己的缓存,完成依赖page cache
  4. controller和coordinate和broker在同一个进程里面,大量io容易造成性能问题

03 BMQ

3.1 BMQ介绍

兼容kafka,存算分离,云原生消息队列

image.png 着重强调,proxy和broker都是无状态的

3.2 运维操作对比

image.png 实际上对于所有节点的变更操作,都仅仅是集群元数据的变化,通常情况下都是秒级完成,而真正的数据已经下移到下层的分布式文件系统来存储,所有运维的时候不需要额外考虑数据复制所带来的时间成本

3.3 HDFS的写入流程

image.png 同一个副本由多个segment组成,在写入的时候选取副本数相同的datanode让后将数据写入这样就保证了数据的高可用性

3.4 BMQ 文件结构

对于kafka分片数据的写入,是通过现在leader上写好文件,然后同步到follower上,对于同一个副本的所有segment在同一台机器上,所以就会有单分片过大的负载不均衡的问题,但是在BMQ集群中,对于单副本来说,是随机分配带不同的节点上面的,因此不会有负载不均衡的问题

3.5 Broker

3.5.1 Partition状态机

image.png 保证对于任意分片在同一时刻只能在一个broker上存活

对于写入逻辑来说,我们还有一个状态机的机制,用来保证同一个分片不会在两个不同的broker上启动的情况,另外也能保证一个分片的正常运行。首先,controller做好分片的分配之后,如果在该broker分配到了broker,首先会start这个分片,然后进入recover的状态,进入这个状态有两个目的,1、获取分片的写入权力,对于hdfs来说只允许一个分片进行写入,只有拿到这个权力的分片才能写入2、如果上次分片异常中断,没有进行savepoint,这里会重新savepoint,然后进入正常的写入流程,创建文件,写入数据,到一定大小后开始创建新的文件写入

3.5.2 写文件流程

image.png

  • 校验完成后需要将数据写入buffer,通过一个异步的write thread线程将数据最终写入到底层的存储系统中。这里的写入是可以配置的,一种策略是将数据写入buffer之后就可能返回,这样可以提高吞吐量,但是在机器宕机后可能会损失一定的数据,另外一种策略是在数据落盘之后再返回这样的话数据的不会丢失,但是会影响性能。
  • 再看thread的写入逻辑,首先将数据从buffer中取出来,调用写入逻辑,经过一定的周期后进行flush,flush之后对消息进行创建索引的操作,也就是消息的offset和timestamp,之后进行一次checkpoint,这样消息就可以被消费了

如何没有checkpoint会发生什么问题? 如何flush之后宕机,index还没建立,这个数据是不应该被消费的

3.5.3 写文件failover

如果datanode节点挂了或者其他原因导致我们写文件失败,我们该怎么处理? 可以重新选择一个其他的可以用的节点进行写入

3.6 Proxy

image.png consumer发送一个fetch request,然后会有一个wait流程,这个wait是为了当BMQ的一个topic一直没有数据写入,那么consumer会一直发送fetch,如果consumer过多,server是抗不住的,所以proxy会等待一定时间再返回给用户侧,这样就降低了fetch的请求io次数

3.7 多机房部署

image.png proxy->broker->hdfs

3.8 高级特性

image.png

3.8.1 泳道消息

泳道消息的特性是为了满足各种不同的场景的高级特性,特定泳道的消费者只能消费特定泳道的消息,这样就实现了一个隔离的功能,方便测试,并且避免了资源重复创建的问题

3.8.2 Databus

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

  1. 客户端配置较为复杂
  2. 不支持动态配置,更改配置需要停掉服务
  3. 对于latency不是很敏感的业务,batch效果不佳,latency是延迟

image.png

  1. 简化消息队列客户端的复杂度
  2. 解耦业务与topic
  3. 缓解集群压力,提高吞吐

3.8.3 mirror

image.png

使用mirror通过最终一致的方式,解决跨region读写问题

3.8.4 index

如果希望通过写入logid,userid或者其他业务字段进行消息的查询,该怎么办?

image.png

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

3.8.5 parquet

apache parquet是Hadoop生态圈中一种新型的列式存储格式,它可以兼容Hadoop生态圈中的大多数计算框架,被多种查询引擎支持

image.png

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

04 Rocket MQ

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

4.1 基本概念

image.png

4.2 Rocket MQ架构

image.png

4.3 存储模型

image.png 对于一个broker来说所有的消息都会append到一个commitlog上面,然后按照不同的queue重新dispatch到不同的consumer,这里的qunue里面存储的不是真实的数据,真实的数据只存在commitlog里面,qunue里面只有索引

4.4 高级特性

4.4.1 事务特性

image.png 类似于两阶段提交

4.4.2 延迟发送

image.png 订餐系统

4.4.3 延迟消息

image.png

4.4.4 消费重试和死信队列

image.png