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

132 阅读16分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第16篇笔记, 本文主要介绍了几种常见的消息队列。

消息队列相关案例

案例一 系统崩溃

解决方案:解耦

案例二:服务能力有限

订单服务同时只能处理10个订单请求

解决方案:削峰

案例三 链路耗时长尾

最后一步通知商家特别慢

解决方案:异步

案例四 日志存储

有服务器坏掉了,本地日志都丢了

解决方案:日志处理

消息队列简介

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

业界消息队列对比

消息队列Kafka

适用场景

如何使用Kafka

基本概念

  • Topic:逻辑队列,不同 Topic 可以建立不同的 Topic
  • Cluster:物理集群,每个集群中可以建立多个不同的 Topic
  • Producer:生产者,负责将业务消息发送到 Topic 中
  • Consumer:消费者,负责消费 Topic 中的消息
  • ConsumerGroup:消费者组,不同组 Consumer 消费进度互不干涉

每个 Topic 可以划分多个分区(每个 Topic 至少有一个分区),同一 Topic 下的不同分区包含的消息是不同的。每个消息在被添加到分区时,都会被分配一个 offset,它是消息在此分区中的唯一编号,Kafka 通过 offset 保证消息在分区内的顺序,offset 的顺序不跨分区,即 Kafka 只保证在同一个分区内的消息是有序的

Offset

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

对于每个Partition来说,每一条消息都有一个唯一的Offset,消息在partition内的相对位置信息,并且严格递增

Replica

每个分片有多个 Replica,Leader Replica 将会从 ISR 中选出

  • Replica:分片的副本,分布在不同的机器上,可用来容灾, Leader对外服务,Follower异步去拉取leader的数据进行一个同步,如果leader挂掉了,可以将Follower提升成leader再对外进行服务
  • ISR:意思是同步中的副本,对于Follower来说,始终和leader是有一定差距的 ,但当这个差距比较小的时候,我们就可以将这个follower副本加入到ISR中,不在ISR中的副本是不允许提升成Leader的

数据复制

上面这幅图代表着Kafka中副本的分布图。

  • 图中Broker代表每一个Kafka的节点,所有的Broker节点最终组成了一个集群。
  • 图中整个集群,包含了4个Broker机器节点,集群有两个Topic, 分别是Topic1和Topic2, Topic1,有两个分片,Topic2有1个分片,每个分片都是三副本的状态。
  • 这里中间有一个Broker同时也扮演了Controller的角色 ,Controller是整个集群的大脑,负麦对副本和Broker进行分配

Kafka架构

而在集群的基础上 ,还有一个模块是ZooKeeper,这个模块其实是存储了集群的元数据信息,比如副本的分配信息等等,Controller计算好的方案都会放到这个地方

一条消息的自述

思考:如果发送一条消息,等到其成功后再发一条会有什么问题?

Producer

批量发送

思考题:如果消息量很大,网络带宽不够用,如何解决?

数据压缩

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

Broker

数据的存储

如何存储到磁盘?

消息文件结构

每一个broker中,散步着不同的消息分片

数据路径:/Topic/Partition/Segment/(log | index | timeindex | …)

磁盘结构

移动磁头找到对应磁道,磁盘转动,找到对应扇区,最后写入。寻道成本比较高,因此顺序写可以减少寻道所带来的时间成本。

顺序写

采用顺序写的方式进行写入,以提高写入效率

如何找到消息

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

偏移量索引文件

目标:寻找 offset = 28

文件名是第一条消息的offset

二分找到小于目标 offset 的最大文件。对应的消息就在这个文件里面

二分找到小于目标offset的最大索引位置,在通过寻找 offset 的方式找到对应的batch,然后找到对应的数据

传统数据拷贝

必须通过系统调用拷贝数据

零拷贝

broker发送给consumer,用到了sendfile系统调用,从磁盘把数据读到内核空间之后,内核空间可以把数据直接转到socket buffer进行网络发送,减少了三次数据拷贝

Producer生产的数据持久化到broker,采用mmap文件映射,实现顺序的快速写入

Consumer

消息的接收端

\

对于一个ConsumerGroup来说,多个分片可以并发的消费,这样可以大大提高消费的效率,但需要解决的问题是,Consumer和Partition的分配问题,也就是对于每一个Partition来讲,该由哪一个Consumer来消费的问题。对于这个问题,我们一般有两种解决方法,手动分配和自动分配

如何解决 Partition 在 Consumer Group 中的分配问题?

Consumer Low Level

通过手动进行分配,哪一个 Consumer消费哪一个 Partition 完全由业务来决定

好处:启动快,启动的时候就知道自己去消费那个消费方式

思考一下,这种方式的缺点是什么?

  • 如果consumer3挂掉了,partition7,8就不会被消费了
  • 新增partition时,也只能停机重新分配

Consumer High Level

就是自动分配的方式,在Broker集群中,对于每一个不同的Consumer Group来说 ,都会选取一台Broker当做Coordinator(统筹者),Coordinator的作用就是帮助Consumer Group进行分片的分配,也叫分片rebalance,如果Consumer Group中有发生宕机,或者有新的Consumer加入,整个Partition和Consumer都会重新进行分配来达到一个稳定的消费状态。

Consumer Rebalance

  • group中的consumer随机找broker 发送FindCoodinatorRequest请求,他们会返回自己的协调者是谁
  • 告诉协调者要加入这个组中 发送JoinGroupRequest请求,会从当前的consumer中选取一台作为leader,leader来计算分配策略
  • 返回leader(一般是第一台机器)
  • leader发送SyncGroupRequest请求给Broker,其中包含分配方案
  • 每个consumer间隔一定时间给broker发送心跳

\

小结: 刚刚总共讲了哪一些可以帮助Kafka提高吞吐或者稳定性的功能?

  • Producer:批量发送、数据压缩
  • Broker:顺序写,消息索引,零拷贝
  • Consumer:Rebalance

Kafka 数据复制的问题

对于Kafka来说,每一个Broker上都有不同topic分区的不同副本,而每一个副本,会将其数据存储到该Kafka节点上面,对于不同的节点之间,通过副本直接的数据复制,来保证数据的最终一致性,与集群的高可用。

Kafka重启操作

举个例子来说,如果我们对一个机器进行重启。首先,我们会关闭一个Broker,.此时如果该Broker上存在副本Leader,.那么该副本将发生Leader切换,切换到其他节点上面并且在SR中的Follower副本,可以看到图中是切换到了第二个Broker.上面

而此时,因为数据在不断的写入,对于刚刚关闭重启的Broker来说,和新Leader之间一定会存在数据的滞后,此时这个Broker会追赶数据,重新加入到ISR当中

当数据追赶完成之后,我们需要回切leader,,这一步叫做prefer leader,这一步的目的是为了避免,在一个集群长期运行后,所有的leader都分布在少数节点上,导致数据的不均衡

通过上面的一个流程分析,我们可以发现对于一个Breaker的重启来说,需要进行数据复制,所以时间成本会比较大,比如一个节点重启需要10分钟,一个集群有1000个节点,如果该集群需要重启升级,则需要10000分钟,那差不多就是一个星期,这样的时间成本是非常大的。

可以不可以并发多台重启?不可以。在一个两副本的集群(一个follower)中, 重启了两台机器,对某一分片来讲,可能两个分片都在这台机器上面,则会导致该集群处于不可用的状态。这是更不接受的

Kafka替换,扩容,缩容

和重启相似,但是可能要追的数据更多

Kafka-负载不均衡

partition1数据多,可以把partition3迁移到2号broker,但是这样因为要迁移又多了新的io问题

  • 为了解决io问题,牵扯出另一个io问题

Kafka问题总结

  • 运维成本高
    • 因为有数据复制问题
  • 对于负载不均衡的场景,解决方案复杂
    • 需要有一个较复杂的方案进行数据迁移,权衡IO升高的问题
  • 没有自己的缓存,完全依赖 Page Cache
  • Controller 和 Coordinator和Broker 在同一进程中,大量 IO会造成其性能下降
    • Broker承载了大量的IO,会导致Controller和Coordinator性能下降

消息队列BMQ

介绍

兼容 Kafka 协议,存算分离,云原生消息队列

  • 加了Proxy Cluster和 Broker Cluster
  • Controller和Coordinator分离出来
  • Proxy和Broker无状态

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

运维操作对比

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

HDFS 写文件流程

通过前面的介绍,我们知道了,同一个副本是由多个segment组成,我们来看看BMQ对于单个文件写入的机制是怎么样的,首先客户端写入前会选择一定数量的DataNode,这个数量是副本数,然后将一个文件写入到这三个节点上,切换到下一个segment之后,又会重新选择三个节点进行写入。这样一来,对于单个副本的所有segment来讲,会随机的分配到分布式文件系统的整个集群中

BMQ 文件结构

  • 对于Kafka分片数据的写入,是通过先在Leader.上面写好文件,然后同步到Follower上,所以对于同一个副本的所有Segment都在同一台机器上面。就会存在之前我们所说到的单分片过大导致负裁不均衡的问题。
  • 但在BMQ集群中 ,因为对于单个副本来讲,是随机分配到不同的节点上面的,因此不会存在Kafka的负裁不均问题

Broker-Partition 状态机

其实对于写入的逻辑来说,我们还有一个状态机的机制,用来保证不会出现同一个分片在两个Broker上同时启动的情沉,另外也能够保证一个分片的正常运行。

首先,Controller做好分片的分配之后,如果在该Broker分配到了Broker,首先会start这个分片,然后进入Recover状态,这个状态主要有两个目的:获取分片写入权利,也,就是说,对于hdfs来讲,只会允许我一个分片进行写入,只有拿到这个权利的分片我才能写入,第二个目的是如果上次分片是异常中断的,没有进行save checkpoint,这里会重新进行一次saveheckpoint,然后就进入了正常的写流程状态,创建文件,写入数据,到一定大小之后又开始建立新的文件进行写入。

Broker-写文件流程

数据校验: CRC,参数是否合法

校验完成后,会把数据放入Buffer中

通过一个异步的Write Thread线程将数据最终写入到底层的存储系统当中

这里有一个地方需要注意一下,就是对于业务的写入来说,可以配置返回方式,可以在写完缓存之后直接返回,另外我也可以数据真正写入存储系统后再返回,对于这两个来说前者损失了数据的可靠性,带来了吞吐性能的优势,因为只写入内存是比较快的,但如果在下一次flush前发生宕机了,这个时候数据就有可能丢失了,后者的话,因为数据已经写入了存储系统,这个时候也不需要担心数据丢失,相应的来说吞吐就会小一些

我们再来看看Thread的具体逻辑,首先会将Buffer中的数据取出来,调用底层写入逻辑,在一定的时间周期上去flush, flush完成后开始建立Index,也就是offsetatimestamp对于消息具体位置的映射关系

Index建立好以后,会save一次checkpoint,也就表示, checkpoint后的数据是可以被消费的辣,我们想一下,如果没有checkpoint的情况下会发生什么问题,如果flush完成之后宏机,index还没有建立,这个数据是不应该被消费的

最后当文件到达一定大小之后,需要建立一个新的segment文件来写入

Broker-写文件 Failover

我们之前说到了,建立一个新的文件,会随机挑选与副本数量相当的数据节点进行写入,那如果此时我们挑选节点中有一个出现了问题,导致不能正常写入了,我们应该怎么处理,是需要在这里等着这个节点恢复吗,当然不行,谁知道这个节点什么恢复,既然你不行,就把你换了,可以重新找正常的节点创建新的文件进行写入,这样也就保证了我们写入的可用性

Proxy

首先Consumer发送一个Fetch Request, 然后会有一个Wait流程,那么他的作用是什么呢,

想象一个Topic, 如果一直没有数据写入,那么,此时consumer就会一直发送Fetch Request,如果Consumer数量过多,BMQ的server端是打不住这个请求的

因此,我们设置了一个等待机制,如果没有fetch到指定大小的数据,那么proxy会等待指定的时间,再返回给用户侧,这样也就降低了fetch请求的10次数,经过我们的wait流程后,我们会到我们的Cache里面去找到是否有存在我们想要的数据,如果有直接返回,如果没有,再开始去存储系统当中寻找,

首先会Open这个文件,然后通过Index找到数据所在的具体位置,从这个位置开始读取数据

多机房部署

为什么需要多机房部署,其实对于一个高可用的服务,除了要防止单机故障所带来的的影响意外,也要防止机房级故障所带来的影响,比如机房断电,机房之间网络故障等等。那我们来看看BMQ的多机房部器是怎么做的 Proxy-> Broker -> Meta -> HDFS

BMQ-高级特性

泳道消息

Databus

Mirror

思考一下,我们是否可以通过多机房部署的方式,解决跨 Region 读写的问题?

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

Index

如果希望通过写入的 LogId、UserId 或者其他的业务字段进行消息的查询,应该怎么做?

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

Parquet

Apache Parquet是Hadoop生态圈中一种新型列式存储格式,它可以兼容 Hadoop 生态圈中大多数计算框架(Hadoop、Spark等),被多种查询引擎支持(Hive、Impala、Drill等)。

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

小结

  1. BMQ 的架构模型(解决 Kafka 存在的问题)

  2. BMQ 读写流程(Failover 机制,写入状态机)

  3. BMQ 高级特性(泳道、Databus、Mirror、Index、Parquet)

消息队列- RocketMQ

使用场景

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

RocketMQ 基本概念

  • 多了一个tag标签,在topic下面做进一步区分
  • 分区,ConsumerQueue
  • 多了一个生产者Producer Group
  • 集群控制器:NameServer

RocketMQ 架构

  • leader和follower是上升到了机器层,整个broker都是leader或者follower

存储模型

接下来我们来看看RocketMQ消息的存储模型,对于一个Broker来说所有的消息的会append到一个Commit Log上面,然后按照不同的Queue, 重新Dispatch到不同的Consumer中,这样Consumer就可以按照Queue进行拉取消费,但需要注意的是,这里的Consumer Queue所存储的并不是真实的数据,真实的数据其实只存在Commit Log中,这里存的仅仅是这个Queue所有消息在Commit Log上面的位置,相当于是这个Queue的一个密集索引

RocketMQ-高级特性

事务场景

事务消息(两阶段提交)

  • 1-2:正常生产
  • 3:执行本地事务逻辑
  • 4:生产者执行事务提交或者回滚
  • 5:如果miss了4个确认,回查
  • 6:查看本地事务状态
  • 7:基于6的结果,提交或者回滚

延迟发送

延迟消息

处理失败:消费重试和死信队列

图片有误,最后超过重试次数之后会发送到死信队列

小结

  1. RocketMQ 的基本概念(Queue,Tag)

  2. RocketMQ 的底层原理(架构模型、存储模型)

  3. RocketMQ 的高级特性(事务消息、重试和死信队列,延迟队列)