这是我参与「第五届青训营 」笔记创作活动的第13天
1.消息队列技术背景
案例1系统崩溃
graph LR
搜索直播间 --> 搜索行为记录-->点击商品-->点击行为记录
搜索行为记录-->记录存储
点击行为记录-->记录存储
案例2服务能力有限
订单请求量过高,但是服务端处理能力有限。
案例3链路耗时长尾
graph LR
用户 --> 发起订单`5ms`-->库存记录-1`100ms`-->订单记录+1`100ms`-->通知订单商家`30s`-->用户
解决方案
- 解耦
- 削峰
- 异步
消息队列
消息队列(MQ),指保存消息的一个容器,本质上是一个队列。但是这个队列支持高吞吐、高并发和高可用
业界常见消息队列: Kafka(适合高吞吐场景)、RocketMQ(实时场景应用较广)、PULSAR(存算分离的架构设计)、BMQ(字节内部)
2.消息队列Kafka
使用场景
搜索服务、直播服务、订单服务、支付服务、日志信息、Metrics数据、用户行为等数据会存入Kafka中
使用Kafka的方式
graph LR
创建集群 --> 新增Topic --> 编写生产者逻辑 --> 编写消费者逻辑
基本概念
- Topic:逻辑队列,不同的Topic可以建立不同的Topic(一个Topic中有多个Partition来处理消息)
- Cluster:物理集群,每个集群可以建立不同的Topic
- Producer:生产者,负责将业务消息发送到Topic中
- Consumer:消费者,负责消费Topic中的信息
- ConsumerGroup:消费者组,不同组中的Consumer消费进度互不干涉
- Offset:消息在partition内部的相对位置
- Replica:每个分片有多个副本,分为Leader和Follower两个角色,Leader负责读写操作,保持和Leader差距相近的副本可以留在队列里(衡量差距的标准是时间差)
- ZooKeeper:负责存储集群元信息,包括分区分配信息等
数据复制
Broker中有一个Controller,负责分配各个partition的分布
消息的处理流程
消息发送
graph LR
Producer --> Message --> Broker
Broker --> Success --> Producer
当消息量大时,会将Message分为多个Batch来同时传输,并且为了防止网络带宽不足,会使用ZSTD等压缩算法进行压缩。
数据存储
Partition本质上是磁盘上存放数据的文件夹,是提高Kafka数据吞吐的关键,其中存储了多个不同的副本,副本中保存着日志。由于日志的时间戳特性,为了保证按照时间对超时数据清除,会使用Segment对其进行分割。
数据路径:/Topic/Partition/Segment/(log|index|timeindex|...)
使用每个LogSegment第一个Offset的名称作为其日志名称
Broker 磁盘结构
移动磁头找到对应的磁道,磁盘转动,找到对应扇区,最后写入。寻道成本比较高,因此可以顺序写来减少寻道所带来的成本。
Broker采取顺序写的方式进行写入,以提高写入效率
寻找消息
Consumer通过发送FetchRequest来请求消息数据,Broker会指定Offset处的消息,按照时间窗口和大小窗口发送给Consumer.
偏移量索引文件
目标是找到索引小于offset的最大索引位置,方法是通过二分找到小于目标offset的最大索引位置。
如图,Broker存储了各个Offset对应的position的映射,根据二分法找出的offset定位磁盘位置
时间戳索引文件
二分找到小于目标时间戳的最大索引位置,再通过offset的方式找到最终数据
传统数据拷贝
零拷贝
mmap
上面的两个拷贝过程示意图是磁盘Output,Input即为该过程的逆向进行,使用mmap技术可以跳过读入用户缓冲区的过程,本质上是一种零拷贝行为(零拷贝并不是真正的不拷贝,是通过某些映射的手段减少数据在不同的缓冲区间拷贝的过程)
接收消息
关键是解决consumer组在partition中的分配问题。
手动分配
哪一个consumer消费哪一个partion完全由业务决定
问题:不能够自动容灾
自动分配
使用Rebalance机制来不断动态分配Partition给到consumer
- 1.Consumer先发送请求给broker找到作为协调者的broker
- 2.Coordinator在Consumer中选出Leader(通常是第一个)
- 3.Consumer等待分配方案
- 4.每个Consumer不断向Broker发送心跳
重启操作
关闭Broker时写入操作还在进行,因此会重新选取一个Leader,新的Leader等待Follower同步,同步完成要进行Leader的回切,回切可以避免长期的运行导致Leader集中到同一个Broker中(如需要重启100个节点的99个时,第100个节点将会不断成为Leader去同步数据)。
重启时不支持并发多台重启的,因为某一集群的两个分片可能同时位于需要重启的两台机器上,并发重启会直接导致集群不可用
问题总结
- 运维成本高
- 负载不均衡
- 没有自己的缓存,完全依赖文件系统的Page Cache
- Controller和Coordinator和Broker在同一个进程中
3.消息队列BMQ
字节跳动开发的BMQ是为了解决Kafka存在的问题而开发的存算分离且兼容Kafka协议的系统。
运维操作对比
- 重启、替换、扩容、缩容:无需数据复制,秒级完成
HDFS写文件流程
选择一定数量的DataNode进行写入,因为Kafka需要将一个Partition写在同一个设备上,容易发生负载不均衡的情况,而BMQ可以将消息分布在不同设备的DataNode上面,从而分布式存储
Broker-Partition状态机
任意分片在同一时刻只能够在一个Broker上存活。
Broker写文件流程
graph LR
Messages-->数据校验-->Buffer-->Writer_Thread-->Storage
Writer Thread的过程
graph LR
Write_Data-->Flush-->Build_Index-->Checkpoint-->Roll_new_segment_file
Proxy读数据流程
graph LR
A(Cache)
Fetch_Request-->wait-->A
A-->Hit-->return_data
A-->Miss-->Storage
多机房部署
由于一个Partition是分布式部署的,因此需要Proxy需要和存储该Partition的全部Broker,如果其中之一出现问题,向上rebalance consumer,向下rebalance 机房存储。
泳道消息
开发流程:
graph LR
开发 --> BOE --> PPE --> Prod
BOE:Bytedance Offline Environment PPE:Product Preview Environment
Databus
使用原生SDK会导致客户端配置较为复杂,也不支持动态配置,更改配置需要停止服务。
使用Databus时使用了Agent作为代理,可以缓解集群压力,解耦业务与Topic
Mirror
跨Rigion的读写问题:如果采用多机房部署,Proxy需要访问部署在多个国家的机房。
使用Mirror通过最终一致的方式,解决跨Region的读写问题
即各个国家的Proxy写到对应国家的镜像中,再进行异步同步消息。
Index
实现增删改查秒级操作的方法是使用Index进行异步构建索引,这个过程直接在BMQ中发生,只需要对数据进行结构化,构建索引,然后通过Index Query服务读出数据。
Parquet
| ID | Name | Age |
|---|---|---|
| 1 | A | 12 |
| 2 | B | 15 |
| 3 | C | 18 |
行式存储:
| 1 | A | 12 | 2 | B | 15 | 3 | C | 18 |
|---|
列式存储
| 1 | 2 | 3 | A | B | C | 12 | 15 | 18 |
|---|
消息存储是类似于行存的形式,使用Parquet组件可以改变存储形式,提高读写效率。
4.Rocket MQ
主要应用于实时秒杀场景,Rocket MQ可以支持标签等进一步分类消息。
与其他两个消息队列的区别是RocketMQ主要是在Broker层面上就分出了Master和Slave来分配读写权限和同步权限。
事务消息
类似于两阶段提交的方式保证一致性。
延迟发送
graph LR
Producer --> CommitLog --> ConsumerQueue
ConsumerQueue-->ScheduleMessage-->CommitLog
消费重试和死信队列
总结
本节课程主要讲解了消息队列的基本用途、使用方式以及三种常用的消息队列以及它们的高级特性。