消息队列原理与实战 | 青训营笔记

175 阅读7分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 13 天

消息队列在互联网的各种业务中发挥着至关重要的作用。本节课可以帮助我们了解业界比较成熟消息队列的底层原理、架构设计以及一些高级特性,对我们在今后的学习和工作中使用消息队列或进行技术选型有帮助

四个场景案例:系统崩溃、服务处理能力有限、链路耗时长尾、日志如何处理

解决方案:面对存储行为服务崩溃时,解耦;削峰;异步;日志处理,Log->消息队列->LogStash->ES->Kibana

消息队列(MQ)指保存消息的一个容器,本质是一个队列,需要支持高吞吐、高并发、高可用

1、背景

1.1 发展历程

TIB->IBM MQ/WebsPhere->MSMQ->JMS->AMQP/RabbitMQ->Kafka->RocketMQ->Pulsar

1.2 业界消息队列对比

Kafka:分布式的、分区的、多副本的日志提交服务,在高吞吐场景下发挥较为出色

RocketMQ:低延迟、强一致、高性能、高可靠、万亿级容量和灵活的可拓展性,在一些实时场景中运用广泛

Pulsar:下一代云原生分布式消息流平台,集消息、存储、轻量化函数式计算为一题、采用存算分离的架构设计

BMQ:和Pulsar架构类似,存算分离,初期定位是承接高吞吐的离线业务场景,逐步替换掉对应的Kafka集群

2、Kafka

2.1 使用场景

Kafka是一个消息系统,是一个存储系统,是一个流式处理平台

2.2 使用Kafka

创建一个Kafka集群->创建一个Topic并且设置好分片数量->编写生产者逻辑->编写消费者逻辑

2.3 基本概念

Topic:逻辑队列,每一个不同的业务场景就是一个不同的Topic,对于这个业务,所有的数据都存储在这个Topic中

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

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

Consumer:消息的消费端

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

2.3.1 Offset

消息在Partition内的相对位置信息可以理解为唯一ID,在Partition内部严格递增

2.3.2 Replica

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

ISR:同步中的副本,对于Follower来说,始终和Leader有一定差距,但当这个差距比较小时,可以将这个Follower副本加入至ISR中,不在ISR中的副本不允许提升为Leader

2.4 数据复制

Kafka中副本分布,途中Broker代表每一个Kafka的节点,所有的Broker节点最终组成了一个集群,Controller是整个集群的大脑,负责对副本和Broker进行分配,对于不同的节点之间通过副本直接的数据复制保证数据的最终一致性与集群的高可用

2.5 Kafka架构

在集群的基础上,增加了ZooKeeper模块,存储了集群的元数据信息,如副本的分配信息、Controller计算好的方案等

2.6 消息的处理流程

Producer生产->Broker->Consumer消费

2.7 Producer

2.7.1 批量发送

减少IO次数

2.7.2 数据压缩

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

2.8 Broker

2.8.1 数据存储

2.8.2 消息文件结构

2.8.3 磁盘结构

磁头->磁道->扇区,寻道成本高,可顺序写入

2.8.4 顺序写

2.8.5 找到消息

Consumer通过发送FetchRequest请求消息数据,Broker会将指定Offset处的消息按照时间窗口和消息大小窗口发送给Consumer

2.8.6 偏移量索引文件

文件名是文件中第一条消息的Offset

通过二分找到小于目标文件的最大文件;通过二分找到小于目标Offset最大的索引位置,再遍历找到目标Offset

2.8.7 时间戳索引文件

通过二分找到时间戳对应的Offset,再重复之前的步骤找到相应文件数据

2.8.8 传统数据拷贝

2.8.9 零拷贝

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

2.9 Consumer

2.9.1 消息的接收端

手动、自动将每个Partition分配给Consumer来消费

2.9.2 手动分配,Low Level

启动快,单个Consumer数量的增加与减少都需要停止整个集群,重新修改配置后上线

2.9.3 自动分配,High Level

在Broker集群中,对于不同的Consumer集群都会选取一台Broker作为Coordinator,帮助进行分片的分配(Rebalance)

2.10 Consumer Rebalance

2.11 数据复制问题

2.12 重启操作

对于一个Broker的重启来说需要进行数据复制,时间成本会非常高,且不可并发多台重启,可能会导致其集群处于不可用的状态

2.13 替换、扩容、缩容

替换:一个需要追更多数据的重启操作,需要复制整个Leader的数据

扩容:从零开始复制新的副本

缩容:缩容节点上的分片被分片到集群剩余节点上,分配的副本也会从零开始复制数据

2.14 负载不均衡

一个较为复杂的解决方案进行数据迁移以权衡IO升高的问题

2.15 Kafka问题

运维成本高;负载不均衡解决方案复杂;没有自己的缓存,完全依赖Page Cache;Controller和Coordinator和Broker在同一进程中,大量IO造成性能下降

3、BMQ

兼容Kafka协议,存算分离,云原生消息队列,初期定位承接高吞吐的离线业务场景,逐步替换对应的Kafka集群

3.1 BMQ架构

Producer->Consumer->Proxy->Broker->HDFS->Controller->Coordinator->Meta

3.2 运维操作

对于所有节点变更的操作都仅是集群元数据的变化,通常情况下秒级完成,真正的数据已迁移至下层分布式文件存储,不需要额外关心数据复制所带来的时间成本

3.3 HDFS写文件流程

客户端写入前选择一定的DataNote,数量对于副本数,将一个文件写入到三个节点上,切换到下一个segment之后重新选择三个节点写入

3.4 文件结构

对于单个副本随机分配到不同的节点上,不存在负载不均衡问题

3.5 Broker

3.5.1 Partition状态机

保证不会出现同一个分片在两个Broker上同时启动的情况,也能保证一个分片的正常运行

3.5.2 写文件流程

CRC->放入buffer->通过异步Write Thread线程将数据最终写入到底层存储系统 (从buffer取数据,调用底层写入逻辑,一定时间周期上去flush,建立Index)

对于业务的写入可以配置返回方式,可以写完缓存之后直接返回,也可以数据真正写入后返回

3.5.3 写文件Failover

DataNode节点挂了或写文件失败,可以重新找正常的节点创建新的文件进行写入

3.6 Proxy

Fetch Request->Wait->Cache->Storage

3.7 多机房部署

Proxy->Broker->Meta->HDFS

3.8 高级特性

泳道->Databus->Mirror->Parquet

3.9 泳道消息

开发->BOE(完全独立线下机房环境)->PPE(产品预览环境)->Prod

3.10 Databus

使用原生SDK会有复杂的客户端配置、更改配置需要停止服务、对于latency不敏感的业务batch效果不佳

简化消息队列客户端复杂度、解耦业务与Topic、缓解集群压力

3.11 Mirror

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

3.12 Index

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

3.13 Parquet

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

4、RocketMQ

针对电商业务线

4.1 RocketMQ架构

4.2 存储模型

4.3 高级特性

4.3.1 事务场景

4.3.2 事务消息

4.3.3 延迟发送

4.3.4 延迟消息

4.3.5 处理失败

4.3.6 消费重试和死信队列

课后个人总结

本节课讲解的是消息队列的原理及实战,主要讲解了三个消息队列,Kafka、BMQ和RocketMQ。通过本节课的学习,帮助我了解了业界比较成熟消息队列的底层原理、架构设计以及一些高级特性,对我在今后的学习和工作中使用消息队列或进行技术选型很有帮助