消息队列|青训营笔记

159 阅读8分钟

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

一、本堂课重点内容:

消息队列前世今生

概述

服务处理能力有限 链路耗时长尾 日志如何处理

消息队列的一般使用场景及其功能

  • 解耦:使用消息队列缓存数据库操作,将存储服务与整个服务链路解耦,防止因为数据库导致系统崩溃。
  • 削峰:使用消息队列缓存庞大的请求,每次从中获取合理数量的请求进行处理,解决服务处理能力有限的问题。
  • 异步:对于服务链路较长的请求,但是又需要及时反馈结果给用户,所以将一些串行的链路,并联到消息队列上,解决链路耗时长尾的问题。
  • 日志处理:将日志直接写入消息队列,查看或处理时从消息队列中取出,防止程序日志丢失。

定义

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

发展历程

image.png

常见MQ

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

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

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

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

消息队列-Kafka

使用步骤

第一步:首先需要创建一个Kafka集群,但如果你是在字节工作,恭喜你这一步消息团队的小伙伴已经帮你完成了

第二步:需要在这个集群中创建一个Topic,并且设置好分片数量

第三步:引入对应语言的SDK,配置好集群和Topic等参数,初始化一个生产者,调用Send方法,将你的Hello World发送出去

第四步:引入对应语言的SDK,配置好集群和Topic等参数,初始化一个消费者,调用Poll方法,你将收到你刚刚发送的Hello World

基本概念

  • Topic:Kakfa中的逻辑队列,可以理解成每一个不同的业务场景就是一个不同的topic,对于这个业务来说,所有的数据都存储在这个topic中

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

  • Producer:顾名思义,也就是消息的生产端,负责将业务消息发送到Topic当中

  • Consumer:消息的消费端,负责消费已经发送到topic中的消息

  • Partition:通常topic会有多个分片,不同分片直接消息是可以并发来处理的,这样提高单个Topic的吞吐(也能容灾)

  • Offset : 对于每一个Partition来说,每一条消息都有一个唯一的Offset,消息在partition内的相对位置信息,并且严格递增
  • 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进行分配

  • 在每一个Broker,都分布着不同Topic的不同分片。

Kafka架构

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

Producer消息传递

  • 通过Batch批量发送,可以减少IO次数,从而加强发送能力

  • 通过压缩Batch,减少消息大小,防止带宽不够,目前支持Snappy、Gzip、LZ4、ZSTD压缩算法

Broker数据的存储

消息传递给Broker后,其如何存储到磁盘

消息文件结构

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

Broker顺序写

采用顺序写的方式进行写入(在末尾追加),以提高写入效率,减少寻道时间。

Broker消息读取

此时我们的消息写入到Broker的磁盘上了,那这些数据又该怎么被找到然后用来消费呢

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

Broker偏移索引文件

  • 偏移索引文件:文件名是文件中第一条消息的offset

  • 目标 : 寻找 offset = 28

  • 第一步,通过二分找到小于目标文件的最大索引文件

  • 第二步,再便利此文件找到目标offset

Broker时间戳索引文件

二分找到小于目标时间截最大的索引位置,在通过寻找 offset 的方式找到最终数据

Borker数据拷贝

传统数据拷贝

Broker零拷贝

  • Consumer从Broker中读取数据,通过sendfile的方式,将磁盘读到os内核缓冲区后,直接转到socket buffer进行网络发送

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

Consumer消息的分配

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

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

手动分配-Low Level

第一,手动分配,也就是Kafka中所说的Low Level消费方式进行消费,这种分配方式的一个好处就是启动比较快,因为对于每一个Consumer来说,启动的时候就已经知道了自己应该去消费哪个消费方式,就好比图中的Consumer Group1来说,Consumer1去消费Partition1,2,3 Consumer2,去消费456, Consumer3去消费78。这些Consumer再启动的时候就已经知道分配方案了,但这样这种方式的缺点又是什么呢,想象一下,如果我们的Consumer3挂掉了,我们的7,8分片是不是就停止消费了。又或者,如果我们新增了一台Consumer4,那是不是又需要停掉整个集群,重新修改配置再上线,保证Consumer4也可以消费数据,其实上面两个问题,有时候对于线上业务来说是致命的。

自动分配-High Level

所以Kafka也提供了自动分配的方式,这里也叫做High Level的消费方式,简单的来说,就是在我们的Broker集群中,对于不同的Consumer Group来讲,都会选取一台Broker当做Coordinator,而Coordinator的作用就是帮助Consumer Group进行分片的分配,也叫做分片的rebalance,使用这种方式,如果ConsumerGroup中有发生宕机,或者有新的Consumer加入,整个partition和Consumer都会重新进行分配来达到一个稳定的消费状态

Kafka缺点

  • 第一,因为有数据复制的问题,所以Kafka运维的时间成本和人力人本都不低

  • 第二,对于负载不均衡的场景,我们需要有一个较为复杂的解决方案进行数据迁移,从而来权衡IO升高的问题

除了以上两个问题以外,Kafka自身还存在其他的问题

  • 比如,Kafka没有自己的缓存,在进行数据读取的时候,只有Page Cache可以用,所以不是很灵活

  • 另外在前面的介绍当中,相信大家也了解到了,Kafka的Controller和Coordinator都是和Broker部署在一起的,Broker因为承载大量IO的原因,会导致Controller和Coordinator的性能下降,如果到一定程度,可能会影响整个集群的可用性

消息队列-BMQ

消息队列-RocketMQ

参考资料

后端专场 学习资料五

课程学习资料