Kafka学习记录 (二) ----基础知识梳理2

kafka

博客内容东拼西凑,以下全文内容整理自极客时间专栏,非原创


一)消费者位移

我们来讨论一个问题:针对 Consumer Group,Kafka 是怎么管理位移的呢?你还记得吧,消费者在消费的过程中需要记录自己消费了多少数据,即消费位置信息。在 Kafka 中,这个位置信息有个专门的术语:位移(Offset)

位移看是上去是一个数值,比如消费到第1000条,offset = 1000 。其实对于 Consumer Group 而言,它是一组 KV 对,Key 是分区,V 对应 Consumer 消费该分区的最新位移。

1 . 对不不同版本的kafka维护offset方式

1.1 老版本:

老版本的 Consumer Group 把位移保存在 ZooKeeper 中。Apache ZooKeeper 是一个分布式的协调服务框架,Kafka 重度依赖它实现各种各样的协调管理。
将位移保存在 ZooKeeper 外部系统的做法,

优点:最显而易见的好处就是减少了 Kafka Broker 端的状态保存开销。
现在比较流行的提法是将服务器节点做成无状态的,这样可以自由地扩缩容,实现超强的伸缩性。
当 Consumer 重启后,它能自动从 ZooKeeper 中读取位移数据,从而在上次消费截止的地方继续消费。
缺点: ZooKeeper 其实并不适用于这种高频的写操作(Leader才能受理写请求等)

1.2 新版本:

将 Consumer 的位移数据作为一条条普通的 Kafka 消息,提交到 __consumer_offsets 中。可以这么说,__consumer_offsets 的主要作用是保存 Kafka 消费者的位移信息。

1.2.1 __consumer_offsets :位移主题

你可能会好奇,这个主题存的到底是什么格式的消息呢?所谓的消息格式,你可以简单地理解为是一个 KV 对。

Key 和 Value 分别表示消息的键值和消息体,在 Kafka 中它们就是字节数组而已(序列化)。

想象一下,如果让你来设计这个主题,你觉得消息格式应该长什么样子呢?我先不说社区的设计方案,我们自己先来设计一下。

首先从 Key 说起。一个 Kafka 集群中的 Consumer 数量会有很多,既然这个主题保存的是 Consumer 的位移数据,那么消息格式中必须要有字段来标识这个位移数据是哪个 Consumer 的,还记得之前我们说 Consumer Group 时提到的 Group ID 吗?

没错,就是这个字段,它能够标识唯一的 Consumer Group。

在这里插入图片描述
但是只保存 Group ID 就可以了吗?

别忘了,Consumer 提交位移是在分区层面上进行的,即它提交的是某个或某些分区的位移,那么很显然,Key 中还应该保存 Consumer 要提交位移的分区。

位移主题的 Key 中应该保存 3 部分内容:

<GroupId,TopicName ,PartitionInfo>

以上是位移消息的第一种消息格式,此外还有其他两种

  • 用于保存 Consumer Group 信息的消息。
    用来注册 Consumer Group
  • 用于删除 Group 过期位移甚至是删除 Group 的消息。
    一旦某个 Consumer Group 下的所有 Consumer 实例都停止了,而且它们的位移数据都已被删除时,Kafka 会向位移主题的对应分区写入 tombstone 消息,表明要彻底删除这个 Group 的信息。
1.2.1.1 __consumer_offsets 什么时候创建的?

通常来说,当 Kafka 集群中的第一个 Consumer 程序启动时,Kafka 会自动创建位移主题。

1.2.1.2 __consumer_offsets 分区数怎么设置?

这就要看 Broker 端参数 offsets.topic.num.partitions 的取值了。它的默认值是 50,因此 Kafka 会自动创建一个 50 分区的位移主题。

1.2.1.3 __consumer_offsets 副本数或备份因子是怎么控制的呢?

答案也很简单,这就是 Broker 端另一个参数 offsets.topic.replication.factor 要做的事情了。它的默认值是 3。

1.2.1.4 __consumer_offsets 什么时候提交的?

目前 Kafka Consumer 提交位移的方式有两种:自动提交位移手动提交位移

自动提交位移: Consumer 端有个参数叫 enable.auto.commit。

如果值是 true,则 Consumer 在后台默默地为你定期提交位移,提交间隔由一个专属的参数 auto.commit.interval.ms 来控制。

自动提交位移有一个显著的优点,就是省事,你不用操心位移提交的事情,就能保证消息消费不会丢失。但这一点同时也是缺点。因为它太省事了,以至于丧失了很大的灵活性和可控性,你完全没法把控 Consumer 端的位移管理。

事实上,很多与 Kafka 集成的大数据框架都是禁用自动提交位移的,如 Spark、Flink 等。

手动提交位移: 即设置 enable.auto.commit = false。

一旦设置了 false,作为 Consumer 应用开发的你就要承担起位移提交的责任。Kafka Consumer API 为你提供了位移提交的方法,如 consumer.commitSync 等。当调用这些方法时,Kafka 会向位移主题写入相应的消息。

自动提交缺点

如果你选择的是自动提交位移,那么就可能存在一个问题:只要 Consumer 一直启动着,它就会无限期地向位移主题写入消息。我们来举个极端一点的例子。

假设 Consumer 当前消费到了某个主题的最新一条消息,位移是 100,之后该主题没有任何新消息产生,故 Consumer 无消息可消费了,所以位移永远保持在 100。
由于是自动提交位移,位移主题中会不停地写入位移 =100 的消息。
显然 Kafka 只需要保留这类消息中的最新一条就可以了,之前的消息都是可以删除的。
这就要求 Kafka 必须要有针对位移主题消息特点的消息删除策略,否则这种消息会越来越多,最终撑爆整个磁盘。

在这里插入图片描述

1.2.1.5 __consumer_offsets 的位移记录方式?

F:有个问题想请教一下,这个位移主题,Consumer是像消费其他主题的分区的内容一样去获取数据的话,那么这本身不也得有个位移,那这个位移又保存到哪里的呢?这样下去不就陷入了一个死循环了吗?要么就不是像正常的消费消息那样去从位移主题获取当前消费者对于某个主题的分区的位移?

A: 好问题!其实Kafka并不太关注__consumer_offsets消费的情况,不过Coordinator的确会在JVM中把所有分区当前已提交的最新位移缓存起来,并且通过这个缓存来决定哪个consumer当前消费到了哪个位移。

1.2.1.6 __consumer_offsets 的offset记录方式?

F:为什么位移主题写入消息时,不直接替换掉原来的数据,像 HashMap 一样呢?而是要堆积起来,另起线程来维护位移主题

A: 位移主题也是主题,也要遵循Kafka底层的日志设计思路,即append-only log


1.2.1.7 Kafka 是怎么删除位移主题中的过期消息的呢

对于同一个 Key 的两条消息 M1 和 M2,如果 M1 的发送时间早于 M2,那么 M1 就是过期消息。Compact 的过程就是扫描日志的所有消息,剔除那些过期的消息,然后把剩下的消息整理在一起。

在这里插入图片描述

图中位移为 0、2 和 3 的消息的 Key 都是 K1。Compact 之后,分区只需要保存位移为 3 的消息,因为它是最新发送的。Kafka 提供了专门的后台线程定期地巡检待 Compact 的主题,看看是否存在满足条件的可删除数据。这个后台线程叫 Log Cleaner。很多实际生产环境中都出现过位移主题无限膨胀占用过多磁盘空间的问题,如果你的环境中也有这个问题,我建议你去检查一下 Log Cleaner 线程的状态,通常都是这个线程挂掉了导致的。


二) Rebalance 参数设置参考

以下三个场景可能发生Rebalance:

  • 组成员数量发生变化
  • 订阅主题数量发生变化
  • 订阅主题的分区数发生变化

后面两个通常都是运维的主动操作,所以它们引发的 Rebalance 大都是不可避免的。接下来,我们主要说说因为组成员数量变化而引发的 Rebalance 该如何避免。

组成员数量发生变化:

如果 Consumer Group 下的 Consumer 实例数量发生变化,就一定会引发 Rebalance。这是 Rebalance 发生的最常见的原因。

Consumer 实例增加的情况很好理解,当我们启动一个配置有相同 group.id 值的 Consumer 程序时,实际上就向这个 Group 添加了一个新的 Consumer 实例。

通常来说,增加 Consumer 实例的操作都是计划内的,可能是出于增加 TPS 或提高伸缩性的需要。总之,它不属于我们要规避的那类“不必要 Rebalance” ,我们需要在意的是Consumer减少的情况;

节点自然下线,被移除ConsumerGroup是正常的,让我们把注意力集中在哪些场景下,会被误判造成Consumer被下线

  • 第一类:未能及时发送心跳,导致 Consumer 被“踢出”Group 而引发的。因此,你需要仔细地设置 session.timeout.ms 和 heartbeat.interval.ms 的值。我在这里给出一些推荐数值,你可以“无脑”地应用在你的生产环境中。设置 session.timeout.ms = 6s。设置 heartbeat.interval.ms = 2s。要保证 Consumer 实例在被判定为“dead”之前,能够发送至少 3 轮的心跳请求,即 session.timeout.ms >= 3 * heartbeat.interval.ms。
  • 第二类:Consumer 消费时间过长导致的,比如消费线程写批处理sql等,max.poll.interval.ms 参数值的设置显得尤为关键。此外,长时间的FullGC也会出现这个情况。

session.timeout.ms:Consumer 实例都会定期地向 Coordinator 发送心跳请求 。
heartbeat.interval.ms:这个值设置得越小,Consumer 实例发送心跳请求的频率就越高。
max.poll.interval.ms 参数。它限定了 Consumer 端应用程序两次调用 poll 方法的最大时间间隔。它的默认值是 5 分钟。

三) TCP连接管理

1)Kafka 的 Java消费者管理 TCP 或 Socket 资源的机制

  • 消费者端主要的程序入口是 KafkaConsumer 类。

构建 KafkaConsumer 实例时是不会创建任何 TCP 连接的,也就是说,当你执行完 new KafkaConsumer(properties) 语句后,是没有 Socket 连接被创建出来。

  • 生产者端主要的程序入口是 KafkaConsumer 类。

KafkaProducer 在构建实例的时候,会在后台默默地启动一个 Sender 线程,这个 Sender 线程负责 Socket 连接的创建。

KafkaConsumer 创建socket的时机:

TCP 连接是在调用 KafkaConsumer.poll 方法时被创建的。
再细粒度地说,在 poll 方法内部有 3 个时机可以创建 TCP 连接。

1.发起 FindCoordinator 请求时。

还记得消费者端有个组件叫协调者(Coordinator)吗?

协调者(Coordinator): 它驻留在 Broker 端的内存中,负责消费者组的组成员管理和各个消费者的位移提交管理。

当消费者程序首次启动调用 poll 方法时,它需要向 Kafka 集群发送一个名为 FindCoordinator 的请求,希望 Kafka 集群告诉它哪个 Broker 是管理它的协调者。

不过,消费者应该向哪个 Broker 发送这类请求呢?
理论上任何一个 Broker 都能回答这个问题,也就是说消费者可以发送 FindCoordinator 请求给集群中的任意服务器。在这个问题上,社区做了一点点优化:消费者程序会向集群中当前 「负载最小」的那台 Broker 发送请求

「负载最小」 : 负载是如何评估的呢?其实很简单,就是看消费者连接的所有 Broker 中,谁的待发送请求最少。

2.连接协调者时。

Broker 处理完上一步发送的 FindCoordinator 请求之后,会返还对应的响应结果(Response),显式地告诉消费者哪个 Broker 是真正的协调者。

因此在这一步,消费者知晓了真正的协调者后,会创建连向该 Broker 的 Socket 连接。

只有成功连入协调者,协调者才能开启正常的组协调操作,比如加入组、等待组分配方案、心跳请求处理、位移获取、位移提交等。

3.消费数据时。

消费者会为每个要消费的分区创建与该分区「领导者副本」所在 Broker 连接的 TCP。

举个例子,假设消费者要消费 5 个分区的数据,这 5 个分区各自的领导者副本分布在 4 台 Broker 上,那么该消费者在消费时会创建与这 4 台 Broker 的 Socket 连接。

在这里插入图片描述
这里插入一个小话题,那就是消费者怎么知道自己的 Coordinator 在哪个 Broker 上,计算的过程非常简明,就是根据消费者组名的 HashCode 对 __consumer_offset 主题的分区数进行取余,代码如下:

def partitionFor(groupId: String): Int = Utils.abs(groupId.hashCode) % groupMetadataTopicPartitionCount

计算出的分区领导者副本所在的 Broker 就是对应 Coordinator 的位置


Consumer 何时关闭 TCP 连接?

和生产者类似,消费者关闭 Socket 也分为主动关闭和 Kafka 自动关闭。

主动关闭 :指你显式地调用消费者 API 的方法去关闭消费者,具体方式就是手动调用 KafkaConsumer.close() 方法,或者是执行 Kill 命令,不论是 Kill -2 还是 Kill -9;

自动关闭:是由消费者端参数 connection.max.idle.ms 控制的,该参数现在的默认值是 9 分钟,即如果某个 Socket 连接上连续 9 分钟都没有任何请求“过境”的话,那么消费者会强行“杀掉”这个 Socket 连接。


四)消费者组消费进度监控

对于 Kafka 消费者来说,最重要的事情就是监控它们的消费进度了,或者说是监控它们消费的滞后程度。这个滞后程度有个专门的名称:消费者 Lag 或 Consumer Lag

所谓滞后程度,就是指消费者当前落后于生产者的程度。比方说,Kafka 生产者向某主题成功生产了 100 万条消息,你的消费者当前消费了 80 万条消息,那么我们就说你的消费者滞后了 20 万条消息,即 Lag 等于 20 万。

通常来说,Lag 的单位是消息数,而且我们一般是在主题这个级别上讨论 Lag 的,但实际上,Kafka 监控 Lag 的层级是在分区上的。如果要计算主题级别的,你需要手动汇总所有主题分区的 Lag,将它们累加起来,合并成最终的 Lag 值。

对消费者而言,Lag 应该算是最最重要的监控指标了。它直接反映了一个消费者的运行情况。一个正常工作的消费者,它的 Lag 值应该很小,甚至是接近于 0 的,这表示该消费者能够及时地消费生产者生产出来的消息,滞后程度很小。反之,如果一个消费者 Lag 值很大,通常就表明它无法跟上生产者的速度,最终 Lag 会越来越大,从而拖慢下游消息的处理速度。

更可怕的是,由于消费者的速度无法匹及生产者的速度,极有可能导致它消费的数据已经不在操作系统的页缓存中了。这样的话,消费者就不得不从磁盘上读取它们,这就进一步拉大了与生产者的差距,进而出现马太效应,即那些 Lag 原本就很大的消费者会越来越慢,Lag 也会越来越大。

应该怎么监控它呢?简单来说,有 3 种方法。

  1. 使用 Kafka 自带的命令行工具 kafka-consumer-groups 脚本。
  2. 使用 Kafka Java Consumer API 编程。
  3. 使用 Kafka 自带的 JMX 监控指标。

以上三种方式,推荐使用第三种;
//todo 三种方法使用demo;


五)Kafka 的副本机制(Replicate)

数据副本一般往往具备以下三个优点:

  1. 提供数据冗余。即使系统部分组件失效,系统依然能够继续运转,因而增加了整体可用性以及数据持久性。
  2. 提供高伸缩性。支持横向扩展,能够通过增加机器的方式来提升读性能,进而提高读操作吞吐量。
  3. 改善数据局部性。允许将数据放入与用户地理位置相近的地方,从而降低系统延时。

数据冗余 : Kafka对于副本的处理,就是基于这个角度。
提供高申诉性: rdbms 读写分离 ,读库对外提供服务基于这个角度。
改善数据局部性:这里有点像IDC 或者 CDN 的感觉。

副本机制依然是 Kafka 设计架构的核心所在,它也是 Kafka 确保系统高可用和消息高持久性的重要基石。

我们之前谈到过,Kafka 是有主题概念的,而每个主题又进一步划分成若干个分区。
副本的概念实际上是在分区层级下定义的,每个分区配置有若干个副本。

所谓副本(Replica),本质就是一个只能追加写消息的提交日志。

根据 Kafka 副本机制的定义,同一个分区下的所有副本保存有相同的消息序列,这些副本分散保存在不同的 Broker 上,从而能够对抗部分 Broker 宕机带来的数据不可用。

在实际生产环境中,每台 Broker 都可能保存有各个主题下不同分区的不同副本,因此,单个 Broker 上存有成百上千个副本的现象是非常正常的。

在这里插入图片描述
主题 1 分区 0 的 3 个副本分散在 3 台 Broker 上,其他主题分区的副本也都散落在不同的 Broker 上,从而实现数据冗余。

副本角色

既然分区下能够配置多个副本,而且这些副本的内容还要一致,那么很自然的一个问题就是:我们该如何确保副本中所有的数据都是一致的呢?

特别是对 Kafka 而言,当生产者发送消息到某个主题后,消息是如何同步到对应的所有副本中的呢?针对这个问题,最常见的解决方案就是采用基于领导者(Leader-based)的副本机制。Apache Kafka 就是这样的设计。

第一,副本分类

领导者副本(Leader Replica)追随者副本(Follower Replica)

每个分区在创建时都要选举一个副本,称为领导者副本,其余的副本自动称为追随者副本。

第二,追随者副本(Follower Replica)

在 Kafka 中,追随者副本是不对外提供服务的。这就是说,任何一个追随者副本都不能响应消费者和生产者的读写请求。

所有的请求都必须由领导者副本来处理,或者说,所有的读写请求都必须发往领导者副本所在的 Broker,由该 Broker 负责处理。所以注定做不到想rdbms中呢样的读写分离。

追随者副本不处理客户端请求,它唯一的任务就是从领导者副本异步拉取消息,并写入到自己的提交日志中,从而实现与领导者副本的同步。

在这里插入图片描述

读写分离的引申 [2023年02月19日更新下述内容]

读写分离是关于 Kafka 副本管理的一个热点话题,Kafka 目前是支持消费从副本消息数据的,KIP-392 的提案就是关于这个机制。

但是我们读取的从副本所在的 Broker 也是另一个分区的领导者副本所在的位置,大多数场景下使用这个功能只会导致热点 Broker 的出现,并承担数据同步延迟的代价,并不能达到我们减轻领导者副本负载的目的。

提案改进这个小功能点主要是为了解决跨数据中心/机架部署的场景下,尽可能从本地数据中心消费数据,降低网络通讯的成本开销,提高数据吞吐量

另外一点需要注意的是,Broker 的分区副本同步只能从领导者副本消费消息进行拉取,无法从其他从副本获取数据,支持读写分离的是客户端消费者。 (思考pulsarGEO)

第三,选举

当领导者副本挂掉了,或者说领导者副本所在的 Broker 宕机时,Kafka 依托于 ZooKeeper 提供的监控功能能够实时感知到,并立即开启新一轮的领导者选举,从追随者副本中选一个作为新的领导者。

老 Leader 副本重启回来后,只能作为追随者副本加入到集群中。

呢么,为什么这么设计?

  1. 方便实现“Read-your-writes”。所谓 Read-your-writes,顾名思义就是,当你使用生产者 API 向 Kafka 成功写入消息后,马上使用消费者 API 去读取刚才生产的消息。举个例子,比如你平时发微博时,你发完一条微博,肯定是希望能立即看到的,这就是典型的 Read-your-writes 场景。

如果允许追随者副本对外提供服务,由于副本同步是异步的,因此有可能出现追随者副本还没有从领导者副本那里拉取到最新的消息,从而使得客户端看不到最新写入的消息。


  1. 方便实现单调读(Monotonic Reads)。什么是单调读呢?就是对于一个消费者用户而言,在多次消费消息时,它不会看到某条消息一会儿存在一会儿不存在。如果允许追随者副本提供读服务,那么假设当前有 2 个追随者副本 F1 和 F2,它们异步地拉取领导者副本数据。

倘若 F1 拉取了 Leader 的最新消息而 F2 还未及时拉取,那么,此时如果有一个消费者先从 F1 读取消息之后又从 F2 拉取消息,它可能会看到这样的现象:第一次消费时看到的最新消息在第二次消费时不见了,这就不是单调读一致性。但是,如果所有的读请求都是由 Leader 来处理,那么 Kafka 就很容易实现单调读一致性。


In-sync Replicas(ISR)

ISR 概念介绍

我们刚刚反复说过,追随者副本不提供服务,只是定期地异步拉取领导者副本中的数据而已。既然是异步的,就存在着不可能与 Leader 实时同步的风险。

呢么要回答这个问题,首先第一点就是:

Kafka 要明确地告诉我们,追随者副本到底在什么条件下才算与 Leader 同步。

基于这个想法,Kafka 引入了 In-sync Replicas,也就是所谓的 ISR 副本集合。
ISR 中的副本都是与 Leader 同步的副本,相反,不在 ISR 中的追随者副本就被认为是与 Leader 不同步的。那么,到底什么副本能够进入到 ISR 中呢?

首先要明确的是,Leader 副本天然就在 ISR 中。也就是说,ISR 不只是追随者副本集合,它必然包括 Leader 副本。甚至在某些情况下,ISR 只有 Leader 这一个副本。

ISR 组内的副本与leader保持同步的replica集合,我们要保证不丢消息,首先要保证ISR的存活(至少有一个备份存活),并且消息提交成功。这样再从ISR中获取的副本,就是当前最新的副本;

ISR 属性介绍

broker offset 大致分为:base offsethigh watemark(HW)log end offset(LEO)这个几个概念非常重要,要是搞不清的话,后面的内容基本上就乱了。

base offset:起始位移,replica中第一天消息的offset

HW:replica高水印值,副本中最新一条已提交消息的位移。leader 的HW值也就是实际已提交消息的范围,每个replica都有HW值,但仅仅leader中的HW才能作为标示信息。什么意思呢,就是说当按照参数标准成功完成消息备份(成功同步给follower replica后)才会更新HW的值,代表消息理论上已经不会丢失,可以认为“已提交”。

LEO:日志末端位移,也就是replica中下一条待写入消息的offset,注意哈,是下一条并且是待写入的,并不是最后一条。这个LEO个人感觉也就是用来标示follower的同步进度的。

ISR 集合加入与收缩

够进入到 ISR 的追随者副本要满足一定的条件。至于是什么条件,我先卖个关子,我们先来一起看看下面这张图。

在这里插入图片描述

你觉得哪个追随者副本与 Leader 不同步?答案是,要根据具体情况来定。

这个标准就是 Broker 端参数 replica.lag.time.max.ms 参数值

这个参数的含义是 Follower 副本能够落后 Leader 副本的最长时间间隔,当前默认值是 10 秒。

follower从leader拿到消息后会更新一个名为_lastCaughtUpTimeMs的字段。每当要检查follower是否out of ISR时就会用当前时间减去这个字段值去和replica.lag.time.max.ms 比较

这就是说,只要一个 Follower 副本落后 Leader 副本的时间不连续超过 10 秒,那么 Kafka 就认为该 Follower 副本与 Leader 是同步的,即使此时 Follower 副本中保存的消息明显少于 Leader 副本中的消息。

我们在前面说过,Follower 副本唯一的工作就是不断地从 Leader 副本拉取消息,然后写入到自己的提交日志中。如果这个同步过程的速度持续慢于 Leader 副本的消息写入速度,那么在 replica.lag.time.max.ms 时间后,此 Follower 副本就会被认为是与 Leader 副本不同步的,因此不能再放入 ISR 中。

此时,Kafka 会自动收缩 ISR 集合,将该副本“踢出”ISR。值得注意的是,倘若该副本后面慢慢地追上了 Leader 的进度,那么它是能够重新被加回 ISR 的。这也表明,ISR 是一个动态调整的集合,而非静态不变的。

Kafka在启动的时候会开启两个任务,一个任务用来定期地检查是否需要缩减或者扩大ISR集合,这个周期是replica.lag.time.max.ms的一半,默认5000ms。当检测到ISR集合中有失效副本时,就会收缩ISR集合,当检查到有Follower的HighWatermark追赶上Leader时,就会扩充ISR。

除此之外,当ISR集合发生变更的时候还会将变更后的记录缓存到isrChangeSet中,另外一个任务会周期性地检查这个Set,如果发现这个Set中有ISR集合的变更记录,那么它会在zk中持久化一个节点。

Kafka对ISR的管理,最终都会反馈到Zookeeper节点上 ,
PATH : /brokers/topics/[topic]/partitions/[partition]/state

然后因为Controllr在这个节点的路径上注册了一个Watcher,所以它就能够感知到ISR的变化,并向它所管理的broker发送更新元数据的请求。最后删除该路径下已经处理过的节点。

Unclean 领导者选举(Unclean Leader Election)

既然 ISR 是可以动态调整的,那么自然就可以出现这样的情形:ISR 为空。因为 Leader 副本天然就在 ISR 中,如果 ISR 为空了,就说明 Leader 副本也“挂掉”了,Kafka 需要重新选举一个新的 Leader。

可是 ISR 是空,此时该怎么选举新 Leader 呢?

以 Broker 端参数 unclean.leader.election.enable 控制是否允许 Unclean 领导者选举。

Kafka 把所有不在 ISR 中的存活副本都称为非同步副本
通常来说,非同步副本落后 Leader 太多,因此,如果选择这些副本作为新 Leader,就可能出现数据的丢失。

毕竟,这些副本中保存的消息远远落后于老 Leader 中的消息。
在 Kafka 中,选举这种副本的过程称为 Unclean 领导者选举。

缺点:开启 Unclean 领导者选举可能会造成数据丢失,
优点:好处是,它使得分区 Leader 副本一直存在,不至于停止对外提供服务,因此提升了高可用性。

反之亦然;

如果你听说过 CAP 理论的话,你一定知道,一个分布式系统通常只能同时满足一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)中的两个。显然,在这个问题上,Kafka 赋予你选择 C 或 A 的权利。


六)请求是怎么被处理的?

无论是 Kafka 客户端还是 Broker 端,它们之间的交互都是通过“请求 / 响应”的方式完成的。比如,客户端会通过网络发送消息生产请求给 Broker,而 Broker 处理完成后,会发送对应的响应给到客户端。

Apache Kafka 自己定义了一组请求协议,用于实现各种各样的交互操作。

比如常见的 PRODUCE 请求是用于生产消息的FETCH 请求是用于消费消息的METADATA 请求是用于请求 Kafka 集群元数据信息的

  1. 顺序处理请求

每个请求都必须等待前一个请求处理完毕才能得到处理

while (true) {
      Request request = accept(connection);
      handle(request);
}

  1. 每个请求使用单独线程处理

完全采用异步的方式,开销过大


	while (true) {
            Request = request = accept(connection);
            Thread thread = new Thread(() -> { handle(request);});
            thread.start();
	}
  1. Reactor 模式

Reactor 模式是事件驱动架构的一种实现方式,特别适合应用于处理多个客户端并发向服务器端发送请求的场景。

Reactor 模式的架构如下图所示:
在这里插入图片描述

从这张图中,我们可以发现,多个客户端会发送请求给到 Reactor。Reactor 有个请求分发线程 Dispatcher,也就是图中的 Acceptor,它会将不同的请求下发到多个工作线程中处理。在这个架构中,Acceptor 线程只是用于请求分发,不涉及具体的逻辑处理,非常得轻量级,因此有很高的吞吐量表现。而这些工作线程可以根据实际业务处理需要任意增减,从而动态调节系统负载能力。

如果我们来为 Kafka 画一张类似的图的话,那它应该是这个样子的:

在这里插入图片描述
显然,这两张图长得差不多。Kafka 的 Broker 端有个 SocketServer 组件,类似于 Reactor 模式中的 Dispatcher,它也有对应的 Acceptor 线程和一个工作线程池,只不过在 Kafka 中,这个工作线程池有个专属的名字,叫网络线程池。

Kafka 提供了 Broker 端参数 num.network.threads,用于调整该网络线程池的线程数。
其默认值是 3,表示每台 Broker 启动时会创建 3 个网络线程,专门处理客户端发送的请求。

这里跟想的还真不一样,本以为Server端是基于netty搞的,竟然是jdk直接实现的。

呢么,当网络线程接收到请求后,它是怎么处理的呢?你可能会认为,它顺序处理不就好了吗?实际上,Kafka 在这个环节又做了一层异步线程池的处理,我们一起来看一看下面这张图。

在这里插入图片描述
当网络线程拿到请求后,它不是自己处理,而是将请求放入到一个共享请求队列中。Broker 端还有个 IO 线程池,负责从该队列中取出请求,执行真正的处理。如果是 PRODUCE 生产请求,则将消息写入到底层的磁盘日志中;如果是 FETCH 请求,则从磁盘或页缓存中读取消息。

跟之前Netty学习中的一张图特别相似,贴在一起做记忆:
在这里插入图片描述

细心的你一定发现了请求队列和响应队列的差别:
请求队列是所有网络线程共享的,而响应队列则是每个网络线程专属的。

这么设计的原因就在于,Dispatcher 只是用于请求分发而不负责响应回传,因此只能让每个网络线程自己发送 Response 给客户端,所以这些 Response 也就没必要放在一个公共的地方。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值