kafka系列一:开始

2,619 阅读14分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第7天,点击查看活动详情

Kafka由浅入深

一、前言

在项目中,我们常常需要用到消息队列中间件,主要用来解决应用耦合,异步消息,流量削锋等问题。实现高性能、高可用、可伸缩和最终一致性架构。使用较多的消息队列有kafka、RabbitMQ、ActiveMQ、RocketMQ。不同的消息队列都会有不同的特点和优势。本文将对 Kafka 进行浅析,并分享一套kafka重试方案。

二、消息系统介绍

2.1、什么是消息系统

消息系统是负责将消息数据从一个应用传递到另外一个应用的系统,应用只需关注于数据,无需关注数据在两个或多个应用间是如何传递的。分布式消息传递基于可靠的消息队列,在客户端应用和消息系统之间异步传递消息。目前有两种消息队列:1、点对点消息传递模式 2、发布-订阅消息传递模式

2.2、点对点传递模式

在点对点消息系统中,消息持久化到一个队列中。此时,将有一个或多个消费者消费队列中的数据。但是一条消息只能被消费一次。当一个消费者消费了队列中的某条数据之后,该条数据会从消息队列中删除。我的理解是点对点模式即只有一个消息队列专门为生产者和消费者服务,只能增加消费者或生产者数量,但是传输的内容无法进行分类,也无法将某条消息指定给某个消费者消费

image-20220629142933854

2.3、发布订阅消息传递模式

在发布-订阅消息系统中,消息被持久化到一个topic中。与点对点消息系统不同的是,消费者可以订阅一个或多个topic,消费者可以消费该topic中所有的数据。在发布-订阅消息系统中,消息的生产者称为发布者,消费者称为订阅者。其实每个topic都可以理解为一个队列,然后可以为这个队列指定具体的消费者。

image-20220629143042086

二、kafka浅析

2.1、概述

Kafka是最初由Linkedin公司开发,是一个分布式、分区的、多副本的、多订阅者,基于zookeeper协调的分布式日志系统(也可以当做MQ系统),常见可以用于web/nginx日志、访问日志,消息服务等等,Linkedin公司于2010年贡献给了Apache基金会并成为顶级开源项目。

2.2、主要应用场景

主要应用于大数据实时处理领域,例如基于Hadoop的批量处理系统,低延迟的实时系统、storm/Spark流式处理引擎、web/Nginx日志、访问日志、消息服务等

2.3、特点

  • 以时间复杂度为O(1)的方式提供消息持久化能力,即使对TB级以上数据也能保证常数时间的访问性能。

  • 高吞吐率。即使在非常廉价的商用机器上也能做到单机支持每秒100K条消息的传输。

  • 支持Kafka Server间的消息分区,及分布式消费,同时保证每个partition内的消息顺序传输。

  • 同时支持离线数据处理和实时数据处理。

  • Scale out:支持在线水平扩展。

2.4、术语解析

Kafka中发布订阅的对象是topic。我们可以为每类数据创建一个topic,把向topic发布消息的客户端称作producer,从topic订阅消息的客户端称作consumer。Producers和consumers可以同时从多个topic读写数据。一个kafka集群由一个或多个broker服务器组成,它负责持久化和备份具体的kafka消息

  • broker:一台kafka服务器就是一个broker。一个集群由多个broker节点组成。一个broker可以容纳多个topic,且拥有多个分区

    • 假设某topic有N个分区,集群中有M个broker。则分区与broker间的关系:
      • 若N>M 且N除以M是整数,则broker会平均存储这些分区
      • 若N>M 且N%M!=0, 则每个broker中的分区是不平均的(导致负载不均衡)
      • 若N<M 则会有N台broker服务器中具有一个分区,另外M-N台broker中是没有分区的
  • topic:可以理解为一个队列,一类消息,消息存放的目录即主题,如page view日志,click日志等均可以topic形式存在,生产者与消费者面向的都是一个topic,集群能到同时负责多个topic的分发

  • partition:topic物理上的分组,,一个topic可以分为多个partition,每个partition是一个有序的队列,分区之间是无序的。实现分区可拓展,一个非常大的topic可以分别到多个broker(即服务器)上

  • producer:消息生产者,就是向kafka broker发送消息的客户端

  • consumer:消息消费者,从kafka broker取消息的客户端

    1. 一个消费者可以消费多个topic主题
    2. 一个消费者可以消费同一个topic中的多个partition中的消息
    3. 一个分区中的消息允许多个无关的消费者同时消费
  • consumer group(CG)

    • consumer group 是kafka提供的可扩展且具有容错性的消费者机制。组内可以有多个消费者,它们共享一个公共的ID,即group ID。组内的所有消费者会协调在一起平均消费(对分区的消费是平均的,但对于消息的消费不一定是平均的)订阅主题的所有分区。
    • kafka可以保证在稳定状态下,一条消息只能被同一个consumer group中的一个consumer进行消费,当然,一个消息可以同时被多个consumer group消费。另外,kafka还可以保证,在稳定状态下每一个组内consumer中会消费某一个或多个指定的partition。
    • 组中consumer数量与partition数量的对应关系如图【2.4.1】
  • segment:partition物理上由多个segment组成,每个segment存着message消息

  • replica:副本,为保证集群中的每个节点发生故障时,该节点上的partition数据不丢失,且kafka仍然能继续工作,kafka提供了副本机制,一个topic的每个分区(partition)都有若干个副本,一个leader和若干个follower

  • leader:每个分区多个副本的‘主’,生产者发送数据的对象,以及消费者消费数据的对象都是leader

  • follower:每个分区多个副本的‘从’,实时从leader中同步数据,保持与leader数据同步,当leader发生故障,某个follower会成为新的follower

  • ISR:In-Sync Replicas,是指副本同步列表。(partition本身(leader)和对应数据备份partition(follower))

  • Partition Leader

    1. 每个partition有多个副本,其中有且仅有一个作为Leader,Leader是当前负责消息读写的partition。即所有读写操作只能发生于leader分区上。

    2. Partition Leader与Partition Follower是主备关系(是partition的状态,不是broler服务器的状态)

    3. leader宕机后,Broker controller会从Follower中选举出一个新的leader。

      注意,这个选举不是由zk完成的

  • Partition Follower:所有Follower都需要从Leader同步消息,Follower与Leader始终保持消息同步。

2.4.1、leader的选举

当partition leader宕机后,broker controller 会从ISR中选一个follower成为新的 partition leader。但是,若ISR中没有其它副本怎么办?可以通过unclean.leader.election.enable的取值来设置leader选举的范围。

  • false

    1. 必须等待 ISR 列表中有副本活过来才进行新的选举。该策略可靠性有保证,但可用性低
  • true

    1. 在ISR中没有副本的情况下可以选择任何一个没有宕机的partition(OSR)主机中该topic的partition副本作为新的leader,该策略可用性高,但可靠性没保证。
    2. OSR里的partition follower 是因为与前任的partition leader 通讯出问题(可能是leader自己出问题),follower才被移出ISO,但该partition follower 与其他 follower 通讯可能没问题
2.4.2、consumer group(CG)

image-20220629155951539

image-20220629160003730

image-20220629160013920

image-20220629160023560

2.5、消息发送的可靠性机制(ACK机制)

生产者向kafka发送消息时,可以选择需要的可靠性级别。通过acks参数的值进行设置

  • 为0值时

    1. 异步发送,生产者向kafka发送消息而不需要kafka反馈成功ack。该方式效率最高,但是可靠性最低。其可能会存在消息丢失的情况
    2. 在传输过程中丢失:由于网络原因,生产者发送的消息根本就没有到达kafka,但生产者不知道,其会一直生产并发送消息给kafka。这种情况可能会出现大量的消息丢失
    3. 在broker中丢失:当broker的缓存满时正准备给partition leader中写入时,此时到达的新消息会丢失
  • 为1值时

    1. 同步发送,默认值。生产者发送消息给kafka,broker的partition leader 在接收到消息后马上发送成功ack,生产者收到后知道消息发送成功,然后会再发送消息。如果一直未收到kafka的ack,则生产者会任务消息发送失败,会重发消息

    2. 该方式不能保证消息发送成功。该发生仍然会出现消息丢失的情况。例如,当partition leader 收到消息后马上向producer 发送接收成功ack,但此时在partition follower 还没有同步数据时,该partition leader 宕机,此时写入到原来leader中的消息就丢失了,因为这条消息对于producer来说,是发送成功了,但对于剩余的partition follower 来说 根本就不存在过这条消息数据

  • 为-1值时

    1. 同步发送。生产者发送消息给kafka,kafka收到消息后要等到 ISR 列表中的所有副本都同步消息完成后,才向生产者发送成功ack。如果一直未收到kafka的ack,则认为消息发送失败,会自动重新发送消息
    2. 该方式很少会出现消息丢失的情况。但其存在消息重复接收的情况。为了解决重复接收问题,kafka允许为消息指定唯一标识,并允许用户自定义去重机制(自己代码实现去重)

2.6、follower同步机制

2.5、重复消费问题及解决方案

最常见的重复消费有两种

  • 同一个consumer 重复消费
    1. 当 consumer(消费者)由于消费能力较低而引发了消费超时的时候,则可能会形成重复消费。
    2. 在时间到达时刚好某数据消费完毕,正准备提交offset 但还没有提交时,时间到了,此时就会产生重复消费问题。

其解决方案是:延长offset提交时间。

  • 不同的consumer 重复消费
  1. 当consumer(消费者)消费了消息但还未提交offset时宕机,则这些已被消费过的消息会被重复消费。

其解决法案就是将自动提交改为手动提交。

2.5、Kafka的结构

image-20220629142607424

  • 其实可以注意到leader的分配是根据topic分区来的,每个broker都有leader和follower

2.6、kafka为提高吞吐量而做的一些设计

  • 将从生产端、服务端和消费者端,三端的设计进行剖析。比较浅白,待后续深入。
2.6.1、生产端
  • 消息发送流程

image-20220629162538298

  • 生产者产生一条消息大概分为7个步骤,看起来很长,但可以划分为3个大模块:
    1. KafkaProducer主线程
      • 主要负责消息的产生、序列化、分区、压缩等发送前的准备工作
    2. RecordAccumulator缓存
    3. Sender子线程
      • 负责从RecordAccumulator缓存中取出消息,然后调用网络底层组件完成消息的发送

image-20220629143856214

  • 其实我们从图中很明显可以看出,生产者对消息的处理流程主要分为消息创建和消息发送,为什么kafka设计将两个步骤放在不同的线程并且通过缓存进行解耦,而不是将两个步骤放在一个线程处理呢?假如我们将两个步骤放到一个线程中去处理会发生什么呢,我们可以看到消息创建是依赖于远程数据库/缓存,这个步骤是有可能因为网络不通等原因而发生阻塞的,同时消息发送也依赖于生产端与服务端网络的通信情况,两者其中之一发生阻塞,则会全部阻塞。然后通过缓存进行解耦的方式就可以使两个步骤独立开来,从而提高生产端的吞吐量

image-20220629144421757

  • 消息发送

    底层kselector封装java nio组件,消息发送到kafka服务端都是批量发送,每次都以producer batch(图中缓存区的集合)为单位发送,即一次发送的消息可能不止一条,这样设计减少了网络请求的次数,提升网络读写效率,进而提高了吞吐量

  • 知识补充

    producer有同步和异步两种发送方式,但底层都是通过sender进行消息发送,区别在于异步发送,producer放入缓存就从send() 方法中返回,不在意子线程是否将消息发送出去,而同步发送则等待子线程发送完成再返回

2.6.2、服务端

kafka服务端有四个设计特点,从而保证服务端的吞吐量

  • 使用了reactor 设计模式

在系统设计上使用了reactor 设计模式,这块不太了解,随便写下,大概逻辑也是把功能区拆分为4个大模块,通过功能解耦及设置缓存区来提高吞吐量

  1. Acceptor线程类:负责创建和客户端的连接
  2. processor线程类:网络事件处理层
  3. requestChannel:请求和响应的缓冲层
  4. KafkaRequestHandler和KafkaApis:真正的业务逻辑处理层
  • 思考:为什么要这样设计?

    连接的创建和网络读写事件的处理解耦,可以避免阻塞问题,提高吞吐量。然后缓冲层再进入业务处理层,这样可以避免高并发场景下,业务线程工作过于饱和而导致超时的情况。业务逻辑层(KafkaApis) 处理完后会把响应放入对应的Processor线程里的响应集合里而不是直接业务逻辑层响应客户端,这样也实现了业务线程和网络操作线程的解耦

image-20220629150045222

  • 顺序写

    kafka写日志文件时,用的是追加消息的形式,只在文件尾部顺序写消息,同时只在文件头部读消息,消息队列不涉及修改消息,所以不需要随机写。这样设计即使是传统的磁盘吞吐量也会很大,因为操作系统对顺序写和顺序读有优化

  • 页缓存

    把缓存当磁盘用,避免频繁读写磁盘。在读取或写入文件时,判断数据是否在内存中,若在内存中则直接将内存中的返回给进程,若不在则读磁盘文件,同时会多读一些连续的磁盘页放到内存中。因为kafka是顺序读写,所以命中率会很高,可以极大减少磁盘的访问次数,从而提高服务端吞吐量

  • 0拷贝

    • 以消费者读消息为例子,假如不使用零拷贝,经历了以下步骤:
      1. CPU要从用户态切换到内核态
      2. DMA将数据从磁盘拷贝到内核缓存区
      3. 将数据从内核缓存区拷贝到应用程序
      4. CPU从内核态切换到用户态
      5. 应用程序调用send() 方法,CPU从用户态切换到内核态,将数据发送到SocketBuffer
      6. DMA(内核)将SocketBuffer中的数据发送到网卡
      7. CPU从内核态切换到用户态

    总共经历4次的数据拷贝和4次的CPU上下文切换

    image-20220629150856379

    • 使用零拷贝

      CPU不参与拷贝数据的工作(拷贝工作都由DMA完成)

      节省大量的CPU周期,减少两次CPU在用户态和内核态之间的切换

image-20220629151137987

2.6.3、消费端

消费端提升吞吐量主要的方式就是通过解耦,减少io阻塞,提高整体的吞吐率

image-20220629154218349

image-20220629154418568

三、总结

  1. 生产端

    通过消息压缩、消息缓存批量发送、异步解耦等方面提升吞吐量

  2. 服务端

    网络层使用Reactor设计提升网络层的吞吐、顺序写、页缓存、零拷贝

  3. 消费端

    通过线程异步解耦的方式提升了拉取消息的效率,进而提升消费者的吞吐量

最后的最后

看到看到这了,不来个赞吗~