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

151 阅读11分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的第2篇笔记

Kafka

消息队列应用场景

  • 解耦
  • 削峰
  • 异步
  • 日志处理

1.解耦

假设一个场景:用户需要搜索一个商品并且点击一个商品。在整个请求链路中需要对用户的行为进行记录,如果记录发生故障(删库跑路)整个请求就会被卡住,无法继续往下推进。

image-20220530142816676

使用消息队列来进行解耦:把存储服务从原服务中拆分,达到解耦目标,再使用消息队列进行服务间的通信

image-20220530143153307

2.削峰

一个高并发业务如何处理巨大的请求量呢,处理订单需要时间,而发起订单的请求量又极为庞大

image-20220530143427543

使用消息队列来进行削峰:在发起订单与处理订单之间利用消息队列建立一个缓冲区。处理订单每次只从缓冲区获取10各请求进行处理。对请求流量进行削峰

image-20220530143657399

3.异步

在处理一个请求时,业务链路中有一部分处理极为耗时,导致整个请求响应变慢

image-20220530144020034

利用消息队列实现异步处理:在处理耗时的业务时,把该部分业务逻辑单独划分出来,通过消息队列来进行通信,主业务通知副业务后可以先返回响应,达到异步处理的效果。从而规避主业务同步链路的耗时问题

image-20220530144345054

4.日志处理

在服务器处理业务时都需要进行日志记录。把日志存储在本地容易服务器宕机的影响而丢失。

实际在线上的环境中,通常把日志先存入消息队列,然后由消息队列把日志存储到对应的搜索引擎(ES)或者日志中间件(Kibana,分析与展示)

日志处理更类似于前三点的一个应用示例

日志不通过本地存储,而通过消息队列转接到日志中间件处理:解耦

如果日志处理是请求链路中比较耗时的一部分,引入消息队列就可以起到异步的作用

image-20220530145239545

什么是消息队列

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

image-20220530145433519

消息队列-Kafka

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

1.使用场景

  • 业务日志:每个服务都会有自己的日志信息,可以交由kafka处理
  • Metrics数据:程序运行状态、如qps、cpu利用率、内存占用率等等
  • 用户行为数据:例如搜索、点赞、评论、收藏

image-20220530150937575

2.使用步骤

①创建kafka集群

②在集群中创建topic,并设置其分区数量

③根据kafka提供的sdk编写上游生产者逻辑和下游消费者逻辑

3.基本概念

image-20220530151441426

Cluster(Broker机器节点)

物理集群,每个集群中可以建立多个不同的topic

topic(业务主题)

逻辑队列,不同的业务场景可以建立不同的topic。topic内部含义多个Partition分区

Producer(生产者)

生产者,负责将业务消息发送到topic中

Consumer(消费者)

消费者,负责消费topic中的消息

ConsumerGroup(消费者组)

消费者组,不同组的Consumer消费进度互不干涉

Partition(分区)

topic中分区的概念,不同Partition内的消息可以被异步处理

Offset(唯一ID)

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

image-20220530152140703

Replica(partition副本)

partition下的副本数概念。对于每个partition来说,它有多个副本。不同replica具有不同的角色(leader、follower)。

通常同一partition的多个replica分布在集群中的不同机器上,来达到我们系统容灾的作用

  • leader:对外提供写入和读取(生产和消费)
  • follower:不断同leader上拉取数据,努力的保证和leader数据一致

ISR:In-Sync-Replica: kafka中的一个特性。对于follower而言,它和leader的数据差距是有一个配置信息的。如果follower大于了这个差距,就会被踢出ISR

很老的kafka版本通过offset衡量差距,现版本通过时间衡量差距

ISR的设计目的: 当partition的leader所在机器发生宕机时,我们就可以从ISR中重写选择一个副本让它称为leader。保证kafka的高可用

image-20220530153603754

4.kafka架构

BrokerController:计算分片方案,计算每个topic-partition的leader、follower分区在哪个broker上,再告诉各个broker

image-20220530154252314

Zookeeper:和Controller配合,去负责存储一些集群的元数据信息、分区的一些分配信息、consumer消费到的位点信息等等

image-20220530154532268

5.一条消息的自述

从一条消息的视角,看看kafka为什么能支撑这么高的吞吐量

如果生产者每次发送一条消息,则IO次数会很大,无法实现百万级的吞吐量

Producer-批量发送

生产者一次发送多条消息,减少发送次数,提高吞吐量

image-20220530154949319

如果消息量很大,网络带宽不够用了呢?

Producer-数据压缩

通过压缩,减少批量数据的大小。解决网络带宽不够用的问题。目前支持Snappy(默认)、Gzip、LZ4、ZSTD(各性能优秀,推荐)压缩算法

image-20220530155450083

Broker-数据存储-顺序写

存储结构

之前提到每个broker上都会分配一些partition的副本(leader/follower),而副本最终都会通过日志的形式记录在磁盘上

对于一个日志的副本Log来说不可能存储在一个文件上:因为消息是有一个过期机制的,过期的消息是需要清除的。

因此,对于一个副本的日志来说,它最后都要被切分成多个日志段Segment(有序)。而每个Segment实际主要对应三个文件:

  • .log(日志文件):对应消息的数据
  • .index(offet索引文件):offset和数据在.log文件中具体位置的映射
  • .timeindex(时间戳索引文件):时间戳和数据在.log文件中具体位置的映射

文件的命名主要是用每个Segment中的第一条消息的offset作为文件名

kafka的索引文件采用的是稀疏索引的方式,从而节省查找时间和空间

image-20220530160914064

顺序写

所谓顺序写就是创建出一个文件之后,不会像数据库这种对内容中间进行写入或修改。而是对于每一条消息都采用末尾添加的方式写入文件中,从而实现存储的顺序性,减少磁盘寻道时间,提高写入效率。

Broker-数据消费-找到数据

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

1.通过二分法找到小于offet的最大Segment

image-20220530162654397

2.在索引文件(稀疏索引)中通过二分法找到目标offset所在的batch位置。再在batch内部遍历找到目标offset

.log文件中数据是以一个个batch存储起来的,从而实现稀疏索引

image-20220530163321962

Broker-零拷贝

读出:sendfile的系统调用:磁盘数据拷贝到内核空间后就可以直接发送给网卡,而不经过应用空间和socket缓存。减少了三次数据拷贝

写入:mmap+write

image-20220530163710654

Consumer-消息的接收

因为consumer每个group都相互独立,互不影响。因此每个group都需要拉取目标topic中所有partition。group中每个consumer应该拉取目标topic下的哪些partition,如何分配,是我们接下来需要讨论的问题

手动分配

在代码中就写定每个consumer应该拉取哪个/哪些partition

①如果运行时某个consumer挂掉了,则其负责的partition数据流将无人处理。这是我们无法忍受的问题:不能自动容灾

②当运行时发现consumer的消费能力不够,需要再增加consumer,但原有consumer不会让出负责的partition,此时就无法动态的完成partition的重新分配

自动分配

针对不同的consumer group,broker集群会为其选择一台broker作为它的Coordinator(协调员)

Coordinator的作用是帮助我们的group中的每一位consumer自动分配partition。这个过程在kafka中也叫做Rebalance

image-20220530165123691

具体Coordinator如何实现自动分配的呢?

①Consumer group的每一个consumer随机的(根据broker集群负载情况,选择负载轻的broker节点)从broker集群中选择一个节点询问本group的Coordinator是哪个broker

②consumer访问Coordinator节点,并请求加入Coordinator所管理的consumer group中

③Coordinator从收到的consumer请求中选择一个consumer作为leader,用来计算consumer-partition分配策略

为什么需要consumer group自己计算分配策略呢。确实kafka有默认的分配策略,但是为了满足不同业务的需要,就把分配策略的任务交给了consumer group自己实现

④leader consumer把分配方案发送给Coordinator,由Coordinator同步告知给其他consumer

⑤后续各consumer按一定时间间隔给Coordinator发送心跳。如果出现了Coordinator没有收到某个consumer的心跳,则Coordinator会认为该consumer已经宕机,把它从group中踢出。再重新走一遍上面的流程实现partition的分配

image-20220530170540332

小结

kafka提高吞吐量的功能(Feature):

Producer:批量发送、数据压缩

Broker:顺序写、消息索引、零拷贝

Consumer:Rebalance

6.kafka的缺点

1.kafka节点重启问题

当一个broker节点重启时,其内部对应的partition leader分区将会失效,因此该partition将会从ISR中选择一个follower作为新的leader。新的leader开始写入数据,broker重启完成后,需要以follower的形式先追赶新leader数据一段时间(因为重启的过程中,新leader一直在写入数据)。追赶完成后,旧leader需要回切为新leader(保持负载均衡)。

这里为什么需要回切呢?假设不回切的话,1号节点重启,leader转移到2号节点,2号节点重启,leader转到3号节点,以此类推,假设100各节点中99个重启,则最后所有partition的leader都集中到100节点上,使得该节点读写压力剧增,违背了负载均衡的原则

在上述的整个过程走下来,将会是分钟级别的操作。假设broker集群足够大,同时需要重启的节点又足够多,此时每个节点都只能串行启动(保证所有partition的可用性),时间成本将会以天甚至周为代价。这使得替换、缩容、扩容的运维成本大大增加

为什么每个节点只能串行启动呢?假设两个节点并行重启,而正好某个partition只有两个副本且在这两个节点上。则此时这个partition就会不可用

重启:选新leader,关机—>开机,旧leader追赶—>回切—>串行重启

2.负载不均衡

某些partition可能数据量很多,导致leader所在的broker节点系统负载很高。

为了降低该broker节点的负载,我们只能选择把该broker上的其他partition(leader/follower)迁移到其他broker上,迁移就会涉及到数据复制的问题,而数据复制又会带来I/O升高。所以就变成了一个死循环,我们为了解决I/O升高问题,又引入了新的I/O升高的问题。这个时候我们就需要权衡两台机器的I/O,设计出一个极其复杂的负载均衡策略,从而增加了运维成本和难度

小结

经过上面的分析,可以看出kafka具有以下方面的问题

1.运维成本高

2.对于负载不均衡的场景,解决方案复杂

3.没有自己的缓存,完全依赖纯文件系统的Page Cache(灵活性不如我们自己根据具体业务实现的缓存机制)

4.Controller和Coordinator和Broker在同一进程中,大量I/O会造成其性能下降

Controller负责partition的分片策略,Coordinator负责消费者-partition的分配策略。它们两个的职责实际上都是选择一台broker机器,让它同时担当了Controller和Coordinator的角色。假设Coordinator的I/O升高造成Controller的性能下降,则对整个kafka集群将是一个灾难性的问题。因为Controller挂了,则整个集群都有可能不可用了