Kafka复习(二):控制器、Kafka副本机制、为什么Kafka不支持主从读写分离、高水位和Leader Epoch、Kafka重平衡

四、控制器

控制器组件(Controller)主要作用是在ZooKeeper的帮助下管理和协调整个Kafka集群。集群中任意一台Broker都能充当控制器角色,在运行过程中,只能有一个Broker成为控制器。控制器是重度依赖ZooKeeper的

1、控制器的选举

Broker在启动时,会尝试去ZooKeeper中创建/controller临时节点,第一个成功创建/controller临时节点的Broker会被指定为控制器

ZooKeeper中还有一个与控制器有关的持久节点/controller_epoch,节点中存放的是一个整型的controller_epoch值。controller_epoch值用于记录控制器发生变更的次数,也可以称之为控制器纪元。controller_epoch的初始值为1,即集群中的第一个控制器的纪元为1,当控制器发生变更时,每选出一个新的控制器就将该字段值加1。每个和控制器交互的请求都会携带controller_epoch这个字段,如果请求的controller_epoch值小于内存中的controller_epoch值,则认为这个请求是向已经过期的控制器发送的请求,那么这个请求会被认定为无效的请求。如果请求的controller_epoch值大于内存中的controller_epoch值,那么说明已经有新的控制器当选了(也就是说接收到这种请求的broker已经不再是控制器了)。Kafka通过controller_epoch来保证控制器的唯一性,进而保证相关操作的一致性

2、控制器的作用

1)、主题管理(创建、删除、增加分区)

当执行kafka-topics脚本时,大部分的后台工作都是控制器来完成的

2)、分区重分配

kafka-reassign-partitions脚本提供的对已有主题分区进行细粒度的分配功能

3)、Preferred领导者选举

Preferred领导者选举主要是Kafka为了避免部分Broker负载过重而提供的一种换Leader的方案

4)、集群成员管理(新增Broker、Broker主动关闭、Broker宕机)

控制器组件会利用Watch机制检查ZooKeeper的/brokers/ids节点下的子节点数量变更。当有新Broker启动后,它会在/brokers下创建专属的znode节点。一旦创建完毕,ZooKeeper会通过Watch机制将消息通知推送给控制器,这样,控制器就能自动地感知到这个变化。进而开启后续新增Broker作业

侦测Broker存活性则是依赖于ZooKeeper临时节点。每个Broker启动后,会在/brokers/ids下创建一个临时的znode。当Broker宕机或主机关闭后,该Broker与ZooKeeper的会话结束,这个znode会被自动删除。同理,ZooKeeper的Watch机制将这一变更推送给控制器,控制器就能知道有Broker关闭或宕机了

5)、数据服务

控制器上保存了最全的集群元数据信息,其他所有Broker会定期接收控制器发来的元数据更新请求,从而更新其内存中的缓存数据

3、控制器故障转移

故障转移指的是,当运行中的控制器突然宕机或意外终止时,Kafka能够快速地感知到,并立即启用备用控制器来代替之前失败的控制器

控制器故障转移

最开始时,Broker0是控制器。当Broker0宕机后,ZooKeeper通过Watch机制感知/controller临时节点被删除。之后,所有存活的Broker开始竞选新的控制器身份。Broker3最终赢得了选举,成功地在ZooKeeper上重建了/controller节点。之后,Broker3会从ZooKeeper中读取集群元数据信息,并初始化到自己的缓存中

4、控制器内部设计原理

在0.11版对控制器的底层设计进了重构。最大的改进是:把多线程的方案改成了单线程加事件队列的方案

控制器内部设计原理

单线程+队列的实现方式:社区引入了一个事件处理线程,统一处理各种控制器事件,然后控制器将原来执行的操作全部建模成一个个独立的事件,发送到专属的事件队列中,供此线程消费

单线程不代表之前提到的所有线程都被干掉了,控制器只是把缓存状态变更方面的工作委托给了这个线程而已

第二个改进:将之前同步操作ZooKeeper全部改为异步操作。ZooKeeper本身的API提供了同步写和异步写两种方式。同步操作ZooKeeper,在有大量主题分区发生变更时,ZooKeeper容易成为系统的瓶颈

五、可靠性保证

1、Kafka副本机制详解

副本的概念实际上是在分区层级下定义的,每个分区配置有若干个副本。同一分区下的所有副本保存有相同的消息序列,这些副本分散保存在不同的Broker上,从而能够对抗部分Broker宕机带来的数据不可用

Kafka副本机制详解

上图是一个有3台Broker的Kafka集群上的副本分布情况,主题1每个分区的3个副本都分散在3台Broker上,从而实现数据冗余

1)、基于领导者的副本机制
基于领导者的副本机制
  1. 在Kafka中,副本分为两类:领导者副本(Leader Replica)和追随者副本(Follower Replica)。每个分区在创建时都要选举一个副本,称为领导者副本,其余的副本自动成为追随者副本
  2. 在Kafka中,任何一个追随者副本都不能响应消费者和生产者的读写请求,所有的请求必须由领导者副本来处理。追随者副本的唯一任务就是从领导者副本异步拉取消息,并写入自己的提交日志中,从而实现与领导者副本的同步
  3. 当领导者副本挂掉了,Kafka依托于ZooKeeper提供的监控功能能够实时感知到,并立即开启新一轮的领导者选举,从追随者副本中选一个作为新的领导者。老Leader副本重启回来后,只能作为追随者副本加入到集群中
2)、为什么Kafka不支持主从读写分离?

主从分离与否没有绝对的优劣,它仅仅是一种架构设计,各自有适用的场景

  1. Redis和MySQL都支持主从读写分离,这和它们的使用场景有关。对于那种读操作很多而写操作相对不频繁的负载类型而言,采用读写分离是非常不错的方案——可以通过添加很多Follower实现横向扩展,提升读操作性能。反观Kafka,它的主要场景还是在消息引擎而不是以数据存储的方式对外提供读服务,通常涉及频繁地生产消息和消费消息,这不属于典型的读多写少场景,因此读写分离方案在这个场景下并不太适合

  2. Kafka副本机制使用的是异步消息拉取,因此存在Leader和Follower之间的不一致性。如果要采用读写分离,必然要处理Leader和Follower之间的数据不一致问题

  3. 主写从读可以均摊一定的负载却不能做到完全的负载均衡,比如对于数据写压力很大而读压力很小的情况,从节点只能分摊很少的负载压力,而绝大多数压力还是在主节点上。而在Kafka中却可以达到很大程度上的负载均衡,而且这种均衡是在主写主读的架构上实现的

为什么Kafka不支持主从读写分离

如上图所示,在Kafka集群中有3个分区,每个分区有3个副本,正好均匀地分布在3个Broker上,灰色阴影的代表Leader副本,非灰色阴影的代表Follower副本,虚线表示Follower副本从Leader副本上拉取消息。当生产者写入消息的时候都写入Leader副本,对于上图而言,每个Broker都有消息从生产者流入;当消费者读取消息的时候也是从Leader副本中读取的,每个Broker都有消息流出到消费者。每个Broker上的读写负载都是一样的,Kafka可以通过主写主读实现主写从读实现不了的负载均衡,同样可以达到负载均衡的效果,没必要刻意实现主写从读增加代码实现的复杂程度

3)、ISR

分区中的所有副本统称为AR,而ISR是指与Leader副本保持同步状态的副本(包括Leader)集合,OSR是指与Leader副本同步滞后过多的副本。AR=ISR+OR

在ISR集合之外的副本统称为失效副本,失效副本对应的分区称为同步失效分区,即under-replicated

ISR

当ISR集合中的一个Follower副本滞后Leader副本的时间超过Broker参数replica.lag.time.max.ms指定的值(默认10s)时则判定为同步失败,需要将该Follower副本剔除出ISR集合

当Follower副本将Leader副本LEO之前的日志全部同步时,则认为该Follower副本已经追赶上Leader副本,此时更新该副本的lastCaughtUpTimeMs标识

如果增加了副本因子,那么新增的副本在赶上Leader副本之前都是处于失效状态的。如果一个Follower副本由于某些原因而下线,之后又上线,在追赶上Leader副本之前也处于失效状态

Kafka在启动的时候会开启两个与ISR相关的定时任务,分别为isr-expiration和isr-change-propagation

isr-expiration任务会周期性地检测每个分区是否需要缩减其ISR集合,也就是定期检查当前时间与副本的lastCaughtUpTimeMs差值是否大于参数replica.lag.time.max.ms指定的值,周期是replica.lag.time.max.ms的一半(默认5s)

当ISR集合发生变更时还会将变更后的记录缓存到isrChangeSet中,isr-change-propagation任务会周期性(2.5s)地检查isrChangeSet,如果发现isrChangeSet中有ISR集合的变更记录,那么它会在ZooKeeper的/isr_change_notification路径下创建一个以isr_change_开头的持久顺序节点,并将isrChangeSet中的信息保存到这个节点中。Kafka控制器为/isr_change_notification添加了一个Watcher,当这个节点有子节点发生变化时会触发Watcher的动作,以此通知控制器更新相关元数据信息并向它管理的Broker节点发送更新元数据的请求,最后删除/isr_change_notification路径下已经处理过的节点

4)、Unclean领导者选举

unclean.leader.election.enable参数的默认值为flase,如果设置为true就意味着当Leader下线时候可以从非ISR集合中选举出新的Leader,选举这种副本的过程称为Unclean领导者选举

开启Unclean领导者选举可能会造成数据丢失,但好处是,它使得分区Leader副本一直存在,不至于停止对外提供服务,因此提升了高可用性。反之,禁止Unclean领导者选举的好处在于维护了数据的一致性,避免了消息丢失,但牺牲了高可用性

2、高水位和Leader Epoch

1)、高水位的作用

在Kafka中,高水位的作用有2个:

  1. 定义消息可见性,即用来标识分区下的哪些消息我是可以被消费者消费的
  2. 帮助Kafka完成副本同步
高水位的作用

在分区高水位以下的消息被认为是已提交消息,反之就是未提交消息。消费者只能消费已提交消息,即图中位移小于8的所有消息

LEO(日志末端位移)表示副本写入下一条消息的位移值。介于高水位和LEO之间的消息就属于未提交消息。同一个副本的高水位值不会大于LEO值

Kafka所有副本都有对应的高水位和LEO值,分区的高水位就是其Leader副本的高水位

2)、高水位更新机制

每个副本对象都保存了一组高水位和LEO值,但在Leader副本所在的Broker上还保存了其他Follower副本的LEO值

高水位更新机制

Broker0上保存了某分区的Leader副本和所有Follower副本的LEO值,Broker1上仅仅保存了该分区的某个Follower副本。Broker0上保存的这些Follower副本称为远程副本(Remote Replica),主要作用是帮助Leader副本确定其高水位,也就是分区高水位。Kafka副本机制在运行过程中,会更新Broker1上Follower副本的高水位和LEO值,同时也会更新Broker0上Leader副本的高水位和LEO以及所有远程副本的LEO,但不会更新远程副本的高水位值,也就是图中标记为灰色的部分

LEO和高水位的更新时机

更新对象更新时机
Broker1上Follower副本LEOFollower副本从Leader副本拉取消息,写入到本地磁盘,会更新其LEO值
Broker0上Leader副本LEOLeader副本接收到生产者发送的消息,写入到本地磁盘,会更新期LEO值
Broker0上远程副本LEOFollower副本从Leader副本拉取消息时,会告诉Leader副本从哪个位移处开始拉取。Leader副本会使用这个位移值来更新远程副本的LEO
Broker1上Follower副本高水位Follower副本成功更新完LEO之后,会比较其LEO值与Leader副本发来的高水位值,并用两者的较小值去更新自己的高水位
Broker0上Leader副本高水位主要有两个更新时机:一个是Leader副本更新其LEO之后;另一个是更新完远程副本LEO之后。具体算法是:取Leader副本和所有与Leader同步的远程副本LEO中的最小值

Leader副本处理生产者请求逻辑

  1. 写入消息到本地磁盘

  2. 更新分区高水位值

    1)获取Leader副本所在Broker端保存的所有远程副本LEO值(LEO-1,LEO-2,……,LEO-n)

    2)获取Leader副本高水位值:currentHW

    3)更新currentHW=max{currentHW,min(LEO-1, LEO-2, ……, LEO-n)}

Leader副本处理Follower副本拉取消息的逻辑

  1. 读取磁盘(或页缓存)中的消息数据
  2. 使用Follower副本发送请求中的位移值更新远程副本LEO值
  3. 更新分区高水位值(步骤与Leader副本处理生产者请求逻辑相同)

Follower副本从Leader拉取消息的逻辑

  1. 写入消息到本次磁盘

  2. 更新LEO值

  3. 更新高水位值

    1)获取Leader发送的高水位值:currentHW

    2)获取步骤2中更新过的LEO值:currentLEO

    3)更新高水位值为min(currentHW, currentLEO)

3)、副本同步机制解析

当生产者向一个单分区且有两个副本的主题发送一条消息时,Leader和Follower副本的高水位更新流程如下:

初始状态时,所有值都是0

高水位更新机制

当生产者给主题分区发送一条消息后:

高水位更新机制

此时,Leader副本成功将消息写入了本地磁盘,LEO值被更新为1

Follower再次尝试从Leader拉取消息。这次有消息可以拉取了

高水位更新机制

这时,Follower副本也成功地更新LEO为1。此时,Leader和Follower副本的LEO都是1,但各自的高水位依然是0,还没有被更新。需要在下一轮的拉取中被更新

高水位更新机制

在新一轮的拉取请求中,由于位移值是0的消息已经拉取成功,因此Follower副本这次请求拉取的是位移值=1的消息。Leader副本接收到此请求后,更新远程副本LEO为1,然后更新Leader高水位为1。然后将当前已更新过的高水位值1发送给Follower副本。Follower副本接收到以后,也将自己的高水位值更新为1。至此,一次完整的消息同步周期就结束了

4)、高水位和Leader Epoch

Follower副本的高水位更新需要一轮额外的拉取请求才能实现,Leader副本高水位更新和Follower副本高水位更新在时间上是存在错配的,这种错配是很多数据丢失或数据不一致问题的根源。Kafka在0.11版本引入了Leader Epoch的概念避免因高水位更新错配导致的各种不一致问题

Leader Epoch可以认为是Leader版本。由两部分数据组成:

  1. Epoch:一个单调增加的版本号。每当副本领导权发生变更时,都会增加该版本号。小版本号的Leader被认为是过期Leader,不能再行使Leader权力
  2. 起始位移(Start Offset):Leader副本在该Epoch值上写入的首条消息的位移

现在有两个Leader Epoch<0, 0><1, 120>,第一个Leader Epoch表示版本号是0,这个版本号的Leader从位移0开始保存消息,一共保存了120条消息。之后,Leader发生了变更,版本号增加到1,新版本的起始位移是120

Kafka Broker会在内存中为每个分区都缓存Leader Epoch数据,同时还会定期地将这些信息持久化到一个checkpoint文件中。当Leader副本写入消息到磁盘时,Broker尝试更新这部分缓存。如果该Leader是首次写入消息,那么Broker会向缓存中增加一个Leader Epoch条目,否则就不做更新。每次有Leader变更时,新的Leader副本会查询这部分缓存,取出对应的Leader Epoch的起始位移

单纯依赖高水位是怎么造成数据丢失的

高水位更新机制

开始时,副本A和副本B都处于正常状态,A是Leader副本。某个使用了默认acks设置的生产者向A发送了两条消息,A全部写入成功,此时Kafka会通知生产者两条消息全部发送成功

假设Leader副本和Follower副本都写入了这两条消息,而且Leader副本的高水位也已经更新了,但Follower副本高水位还未更新。此时副本B所在的Broker宕机,当它重启回来后,副本B会执行日志截断操作,将LEO值调整为之前的高水位值,也就是1。此时副本B只保存有1条消息,即位移值为0的那条消息

当执行完截断操作后,副本B开始从A拉取消息,此时副本A所在的Broker宕机了,那么只能让副本B称为新的Leader,当A回来后,需要执行相同的日志截断操作,即将高水位调整为与B相同的值,也就是1。这样操作之后,位移值为1的那条消息就从这两个副本中被永远地抹掉了,也就是数据丢失场景

如何利用Leader Epoch机制来规避这种数据丢失

高水位更新机制

引入Leader Epoch机制后,Follower副本B重启回来后,需要向A发送一个特殊的请求去获取Leader的LEO值。在这个例子中,该值为2。当获取到Leader LEO=2后,B发现该LEO值不比它自己的LEO值小,而且缓存中也没有保存任何起始位移值>2的Epoch条目,因此B无需执行任何日志截断操作,副本是否执行日志截断不再依赖于高水位进行截断

现在,副本A宕机了,B成为Leader。当A重启回来后,执行与B相同的逻辑判断,发现也不用执行日志截断,至此位移值为1的那条消息在两个副本中均得到保留。后面当生产者向B写入新消息时,副本B所在的Broker缓存中,会生成新的Leader Epoch条目:[Epoch=1, Offset=2]

六、重平衡流程解析

重平衡的作用是让消费者组内所有的消费者实例就消费哪些主题分区达成一致

在Rebalance过程中,所有Consumer实例共同参与,在协调者组件的帮助下,完成主体分区的分配。但是,在整个Rebalance过程中,所有实例都不能消费任何消息,因此它对Consumer的TPS影响很大

1、通知机制

Kafka消费者需要定期地发送心跳请求到Broker端的协调者,以表明它还存活着。重平衡的通知机制正是通过心跳线程来完成的。当协调者决定开启新一轮重平衡后,它会将REBALANCE_IN_PROGRESS封装进心跳请求的响应中,发还给消费者实例。当消费者实例发现心跳响应中包含了REBALANCE_IN_PROGRESS,就能立马知道重平衡开始了

消费者端参数heartbeat.interval.ms参数(默认是3秒)设置了心跳的间隔时间,同时也是控制重平衡通知的频率

2、消费者组状态机

Kafka设计了一组消费者组状态机来帮助协调者完成整个重平衡流程

状态含义
Empty组内没有任何成员,但消费者组可能存在已提交的位移数据,而且这些位移尚未过期
Dead组内没有任何成员,但组的元数据信息已经在协调者端被删除。协调者组件保存着当前向它注册过的所有组信息,所谓的元数据信息就类似于这个注册信息
PreparingRebalance消费者组准备开启重平衡,此时所有成员都要重新请求加入消费者组
CompletingRebalance消费者组下所有成员已经加入,各个成员正在等待分配方案。该状态在老一点的版本中被称为AwaitingSync,它和CompletingRebalance是等价的
Stable消费者组的稳定状态。这状态表明了重平衡已经完成,组内各成员能够正常消费数据了

状态机的各个状态流转如下图:

在这里插入图片描述

消费者组启动时,最开始是Empty状态,当重平衡过程开启后,为PreparingRebalance状态等待成员加入,之后变更到CompletingRebalance状态等待分配方案,最后流转到Stable状态完成重平衡

当有新成员加入或已有成员退出时,消费者组的状态从Stable直接跳到PreparingRebalance状态,此时,所有现存成员就必须重新申请加入组。当所有成员都退出组后,消费者组状态变更为Empty

Kafka定期自动删除过期位移的条件就是,组要处于Empty状态。因此,如果你的消费者组停掉了很长时间(超过7天),那么Kafka很可能就把该组的位移数据删除了

3、消费者端重平衡流程

在消费者端,重平衡分为两个步骤:分别是加入组和等待领导者消费者(Leader Consumer)分配方案。这两个步骤分别对应两类特定的请求:JoinGroup请求和SyncGroup请求

当组内成员加入组时,它会向协调者发送JoinGroup请求。在该请求中,每个成员都要将自己订阅的主题上报,这样协调者就能收集到所有成员的订阅信息。一旦收集了全局成员的JoinGroup请求后,协调者会从这些成员中选择一个担任这个消费者组的领导者

通常情况,第一个发送JoinGroup请求请求的成员自动成为领导者。领导者消费者的任务是收集所有成员的订阅信息,然后根据这些信息,制定具体的分区消费分配方案

选出领导者之后,协调者会把消费者组订阅信息封装进JoinGroup请求的响应体中,然后发给领导者,由领导者统一做出分配方案后,进入到下一步:发送SyncGroup请求

领导者向协调者发送SyncGroup请求,将刚刚做出的分配方案发给协调者。其他成员也会向协调者发送SyncGroup请求,只不过请求体中并没有实际的内容。这一步的主要目的是让协调者接收分配方案,然后统一以SyncGroup响应的方式分发给所有成员,这样组内所有成员就都知道自己改消费哪些分区了

在这里插入图片描述

JoinGroup请求的主要作用是将组内成员订阅信息发送给领导者消费者,待领导者制定好分配方案后,重平衡流程进入到SyncGroup请求阶段

在这里插入图片描述

SyncGroup请求的主要目的是让协调者把领导者制定的分配方案下发给各个组内成员。当所有成员都成功接收到分配方案后,消费者组进入Stable状态,即开始正常的消费工作

4、Broker端重平衡场景剖析

1)新成员入组

在这里插入图片描述

当协调者收到新的JoinGroup请求后,它会通过心跳请求响应的方式通知组内现有的所有成员,强制它们开启新一轮的重平衡

2)组成员主动离组

在这里插入图片描述

消费者实例所在线程或进程调用close()方法主动通知协调者它要退出。这个场景涉及第三类请求:LeaveGroup请求。协调者收到LeaveGroup请求后,依然会以心跳响应的方式通知其他成员

3)组成员崩溃离组

在这里插入图片描述

崩溃离组是指消费者实例出现严重故障,突然宕机导致的离组。崩溃离组是被动的,协调者通常需要等待一段时间才能感知,这段时间一般是由消费者端参数session.timeout.ms控制的

4)组成员提交位移

在这里插入图片描述

正常情况下,每个组内成员都会定期汇报位移给协调者。当重平衡开启时,协调者会给予成员一段缓冲时间,要求每个成员必须在这段时间内快速地上报自己的位移信息,然后再开启正常的JoinGroup/SyncGroup请求发送

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

邋遢的流浪剑客

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值