这是我参与「第三届青训营 -后端场」笔记创作活动的的第8篇笔记
01 消息队列的前世今生
1.1 业界消息队列对比
- Kafka,分布式的、分区的、多副本的日志提交服务,在高吞吐场景下发挥较为出色
- RocketMQ,低延迟、强一致性、高性能、高可靠、万亿级容量和灵活的扩展性,在一些实时场景中应用较广
- Pulsar,是下一代云原生分布式消息流平台,集消息、存储、轻量化函数式计算为一体、采用存算分离的架构设计
- BMQ,和Plusar架构类似,存算分离,初期定位是承接高吞吐的离线业务场景,逐步替换掉对应的kafka集群
02 消息队列-Kafka
2.1 如何使用kafak
graph TD
创建集群 --> 新增topic --> 编写生产者逻辑 --> 编写消费者逻辑
2.2 基本概念
- 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 数据复制
图中broker表示一个kafka节点,多有的kafka节点组成一个集群。这个图中,包含了四个broker机器节点,两个topic,topic1有两个分片,topic有一个分片,每一个分片有三个副本状态。同时这里有一个还扮演着controller的角色,controller是整个集群的大脑,复制对副本和broker进行分配
2.4 kafka架构
zookeeper:负责存储集群元信息,包括分区分配信息。zookeeper模块其实是存储了集群的元数据信息,比如副本的分配信息,controller计算好的方案都会放到这里
2.5 一条消息的自述
graph TD
producer --> broker --> consumer
2.5.1 发送消息
Q:如果一条一条的发送会有什么问题? 可以批量发送减少IO次数,从而加强发送能力 Q:如果消息量很大,网络带宽不够用,如何解决? 通过压缩,减少消息大小
2.5.2 数据的存储
2.5.2.1 消息文件结构
Q:如何存储到磁盘?
由于磁盘的结构,顺序写入速度是最快的。
2.5.3 如何找到消息
Consumer通过发送fetchrequest请求消息数据,broker会将指定offset处的消息,按照时间窗口和消息大小窗口发送给consumer,寻找数据这个细节是如何做到的?
- 二分查找找到小于目标offset的最大文件
- 二分查找到小于目标offset的最大索引位置
- 二分找到小于目标时间戳最大的索引位置,再通过寻找offset的方式找到最终的数据
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数据的最终一致性,与集群的高可用
2.7 kafka重启操作
kafka的重启需要经历几个步骤
graph TD
关闭重启 --> leader切换追赶数据 --> 数据同步完成 --> leader回切
- 为什么要经历leader回切? 因为是为了避免一个集群在长时间运行之后,所有的leader都集中在了少数的没有重启过的broker上面,这样是为了减少风险,并且减少这些broker的压力
- 为什么重启必须一个一个来? 因为如果有一个两副本集群,如果同时重启会造成服务不可用和数据不一致的问题
2.8 kafka替换、扩容、缩容
- 替换就相当于一个从零开始追赶的重启操作
- 扩容也是一个从零开始复制分片的操作
- 缩容是把这个broker的分片从零在别的机器上进行追赶的一个操作 所以kafka的运维成本是很高的,不容忽视。
2.9 kafka负载不均衡的问题
在这个例子中,这个topic有四个分片两个副本,其中明显分片1的数量要明显多一些,broker1的负载就比较重,所以需要把分片3移动到别的broker上面,但是这样会带来相应的问题,比如,需要设计复杂的负载均衡算法,并且在数据移动的过程中加重了broker的io负担,我们是为了解决io负担的,但是引入了新的io负担
2.10 kafka问题总结
- 运维成本高
- 对于负载不均衡的场景解决方案复杂
- 没有自己的缓存,完成依赖page cache
- controller和coordinate和broker在同一个进程里面,大量io容易造成性能问题
03 BMQ
3.1 BMQ介绍
兼容kafka,存算分离,云原生消息队列
着重强调,proxy和broker都是无状态的
3.2 运维操作对比
实际上对于所有节点的变更操作,都仅仅是集群元数据的变化,通常情况下都是秒级完成,而真正的数据已经下移到下层的分布式文件系统来存储,所有运维的时候不需要额外考虑数据复制所带来的时间成本
3.3 HDFS的写入流程
同一个副本由多个segment组成,在写入的时候选取副本数相同的datanode让后将数据写入这样就保证了数据的高可用性
3.4 BMQ 文件结构
对于kafka分片数据的写入,是通过现在leader上写好文件,然后同步到follower上,对于同一个副本的所有segment在同一台机器上,所以就会有单分片过大的负载不均衡的问题,但是在BMQ集群中,对于单副本来说,是随机分配带不同的节点上面的,因此不会有负载不均衡的问题
3.5 Broker
3.5.1 Partition状态机
保证对于任意分片在同一时刻只能在一个broker上存活
对于写入逻辑来说,我们还有一个状态机的机制,用来保证同一个分片不会在两个不同的broker上启动的情况,另外也能保证一个分片的正常运行。首先,controller做好分片的分配之后,如果在该broker分配到了broker,首先会start这个分片,然后进入recover的状态,进入这个状态有两个目的,1、获取分片的写入权力,对于hdfs来说只允许一个分片进行写入,只有拿到这个权力的分片才能写入2、如果上次分片异常中断,没有进行savepoint,这里会重新savepoint,然后进入正常的写入流程,创建文件,写入数据,到一定大小后开始创建新的文件写入
3.5.2 写文件流程
- 校验完成后需要将数据写入buffer,通过一个异步的write thread线程将数据最终写入到底层的存储系统中。这里的写入是可以配置的,一种策略是将数据写入buffer之后就可能返回,这样可以提高吞吐量,但是在机器宕机后可能会损失一定的数据,另外一种策略是在数据落盘之后再返回这样的话数据的不会丢失,但是会影响性能。
- 再看thread的写入逻辑,首先将数据从buffer中取出来,调用写入逻辑,经过一定的周期后进行flush,flush之后对消息进行创建索引的操作,也就是消息的offset和timestamp,之后进行一次checkpoint,这样消息就可以被消费了
如何没有checkpoint会发生什么问题? 如何flush之后宕机,index还没建立,这个数据是不应该被消费的
3.5.3 写文件failover
如果datanode节点挂了或者其他原因导致我们写文件失败,我们该怎么处理? 可以重新选择一个其他的可以用的节点进行写入
3.6 Proxy
consumer发送一个fetch request,然后会有一个wait流程,这个wait是为了当BMQ的一个topic一直没有数据写入,那么consumer会一直发送fetch,如果consumer过多,server是抗不住的,所以proxy会等待一定时间再返回给用户侧,这样就降低了fetch的请求io次数
3.7 多机房部署
proxy->broker->hdfs
3.8 高级特性
3.8.1 泳道消息
泳道消息的特性是为了满足各种不同的场景的高级特性,特定泳道的消费者只能消费特定泳道的消息,这样就实现了一个隔离的功能,方便测试,并且避免了资源重复创建的问题
3.8.2 Databus
使用原生的SDK会有什么问题?
- 客户端配置较为复杂
- 不支持动态配置,更改配置需要停掉服务
- 对于latency不是很敏感的业务,batch效果不佳,latency是延迟
- 简化消息队列客户端的复杂度
- 解耦业务与topic
- 缓解集群压力,提高吞吐
3.8.3 mirror
使用mirror通过最终一致的方式,解决跨region读写问题
3.8.4 index
如果希望通过写入logid,userid或者其他业务字段进行消息的查询,该怎么办?
直接在BMQ中将数据结构化,配置索引DDL,异步构建索引后,通过index query服务读出数据
3.8.5 parquet
apache parquet是Hadoop生态圈中一种新型的列式存储格式,它可以兼容Hadoop生态圈中的大多数计算框架,被多种查询引擎支持
直接在BMQ中将数据结构化,通过parquet engine,可以使用不用的方式构建parquet格式文件
04 Rocket MQ
使用场景 针对电商业务,其业务涉及广泛,例如注册、订单、库存、物流等,同时也会涉及许多业务峰值时刻,如秒杀活动,周年庆,定期特惠等
4.1 基本概念
4.2 Rocket MQ架构
4.3 存储模型
对于一个broker来说所有的消息都会append到一个commitlog上面,然后按照不同的queue重新dispatch到不同的consumer,这里的qunue里面存储的不是真实的数据,真实的数据只存在commitlog里面,qunue里面只有索引
4.4 高级特性
4.4.1 事务特性
类似于两阶段提交
4.4.2 延迟发送
订餐系统