副本剖析(上)(十五)

本文深入探讨了Kafka的副本机制,包括副本的定义、角色以及如何通过多副本实现容灾和提高可用性。介绍了ISR(In-Sync Replicas)集合和HW(High Watermark)的概念,以及如何在异常情况下处理数据同步和失效副本。同时,讨论了ISR的收缩与扩展策略,以及如何维护数据一致性。此外,还提到了LEO(Log End Offset)和HW的关系,以及副本的同步过程。
摘要由CSDN通过智能技术生成

        Kafka 中采用了多副本的机制,这是大多数分布式系统中惯用的手法,以此来实现水平扩展、提供容灾能力、提升可用性和可靠性等。我们对此可以引申出一系列的疑问:Kafka 多副本之间如何进行数据同步,尤其是在发生异常时候的处理机制又是什么?多副本间的数据一致性如何解决,基于的一致性协议又是什么?如何确保 Kafka 的可靠性?Kafka 中的可靠性和可用性之间的关系又如何?

       本节开始从副本的角度切入来深挖 Kafka 中的数据一致性、数据可靠性等问题,主要包括副本剖析、日志同步机制和可靠性分析等内容。

副本剖析

       副本(Replica)是分布式系统中常见的概念之一,指的是分布式系统对数据和服务提供的一种冗余方式。在常见的分布式系统中,为了对外提供可用的服务,我们往往会对数据和服务进行副本处理。数据副本是指在不同的节点上持久化同一份数据,当某一个节点上存储的数据丢失时,可以从副本上读取该数据,这是解决分布式系统数据丢失问题最有效的手段。另一类副本是服务副本,指多个节点提供同样的服务,每个节点都有能力接收来自外部的请求并进行相应的处理。

       组成分布式系统的所有计算机都有可能发生任何形式的故障。一个被大量工程实践所检验过的“黄金定理”:任何在设计阶段考虑到的异常情况,一定会在系统实际运行中发生,并且在系统实际运行过程中还会遇到很多在设计时未能考虑到的异常故障。所以,除非需求指标允许,否则在系统设计时不能放过任何异常情况。

         Kafka 从 0.8 版本开始为分区引入了多副本机制,通过增加副本数量来提升数据容灾能力。同时,Kafka 通过多副本机制实现故障自动转移,在 Kafka 集群中某个 broker 节点失效的情况下仍然保证服务可用。这里先简要的整理下副本以及与副本相关的 AR、ISR、HW 和 LEO的概念:

  • 副本是相对于分区而言的,即副本是特定分区的副本。
  • 一个分区中包含一个或多个副本,其中一个为 leader 副本,其余为 follower 副本,各个副本位于不同的 broker 节点中。只有 leader 副本对外提供服务,follower 副本只负责数据同步。
  • 分区中的所有副本统称为 AR,而 ISR 是指与 leader 副本保持同步状态的副本集合,当然 leader 副本本身也是这个集合中的一员。
  • LEO 标识每个分区中最后一条消息的下一个位置,分区的每个副本都有自己的 LEO,ISR 中最小的 LEO 即为 HW,俗称高水位,消费者只能拉取到 HW 之前的消息。

        从生产者发出的一条消息首先会被写入分区的 leader 副本,不过还需要等待 ISR 集合中的所有 follower 副本都同步完之后才能被认为已经提交,之后才会更新分区的 HW,进而消费者可以消费到这条消息。

失效副本

        正常情况下,分区的所有副本都处于 ISR 集合中,但是难免会有异常情况发生,从而某些副本被剥离出 ISR 集合中。在 ISR 集合之外,也就是处于同步失效或功能失效(比如副本处于非存活状态)的副本统称为失效副本,失效副本对应的分区也就称为同步失效分区,即 under-replicated 分区。

        正常情况下,我们通过 kafka-topics.sh 脚本的 under-replicated-partitions 参数来显示主题中包含失效副本的分区时结果会返回空。比如我们来查看一下主题 topic-partitions 的相关信息:

[root@node1 kafka_2.11-2.0.0]# bin/kafka-topics.sh --zookeeper localhost: 2181/kafka --describe --topic topic-partitions --under-replicated-partitions

        读者可以自行验证一下,上面的示例中返回为空。紧接着我们将集群中的 brokerId 为2的节点关闭,再来执行同样的命令,结果显示如下:

[root@node1 kafka_2.11-2.0.0]# bin/kafka-topics.sh --zookeeper localhost:2181/ kafka --describe --topic topic-partitions --under-replicated-partitions 
     Topic: topic-partitions	Partition: 0	Leader: 1	Replicas: 1,2,0	Isr: 1,0
     Topic: topic-partitions	Partition: 1	Leader: 0	Replicas: 2,0,1	Isr: 0,1
     Topic: topic-partitions	Partition: 2	Leader: 0	Replicas: 0,1,2	Isr: 0,1

       可以看到主题 topic-partitions 中的三个分区都为 under-replicated 分区,因为它们都有副本处于下线状态,即处于功能失效状态。

       前面提及失效副本不仅是指处于功能失效状态的副本,处于同步失效状态的副本也可以看作失效副本。怎么判定一个分区是否有副本处于同步失效的状态呢?Kafka 从 0.9.x 版本开始就通过唯一的 broker 端参数 replica.lag.time.max.ms 来抉择,当 ISR 集合中的一个 follower 副本滞后 leader 副本的时间超过此参数指定的值时则判定为同步失败,需要将此 follower 副本剔除出 ISR 集合,具体可以参考下图。replica.lag.time.max.ms 参数的默认值为10000。

8-1

 

       具体的实现原理也很容易理解,当 follower 副本将 leader 副本 LEO(LogEndOffset)之前的日志全部同步时,则认为该 follower 副本已经追赶上 leader 副本,此时更新该副本的 lastCaughtUpTimeMs 标识。Kafka 的副本管理器会启动一个副本过期检测的定时任务,而这个定时任务会定时检查当前时间与副本的 lastCaughtUpTimeMs 差值是否大于参数 replica.lag.time.max.ms 指定的值。

         千万不要错误地认为 follower 副本只要拉取 leader 副本的数据就会更新 lastCaughtUpTimeMs。试想一下,当 leader 副本中消息的流入速度大于 follower 副本中拉取的速度时,就算 follower 副本一直不断地拉取 leader 副本的消息也不能与 leader 副本同步。如果还将此 follower 副本置于 ISR 集合中,那么当 leader 副本下线而选取此 follower 副本为新的 leader 副本时就会造成消息的严重丢失。

Kafka 源码注释中说明了一般有两种情况会导致副本失效:

  • follower 副本进程卡住,在一段时间内根本没有向 leader 副本发起同步请求,比如频繁的 Full GC。
  • follower 副本进程同步过慢,在一段时间内都无法追赶上 leader 副本,比如 I/O 开销过大。

       在这里再补充一点,如果通过工具增加了副本因子,那么新增加的副本在赶上 leader 副本之前也都是处于失效状态的。如果一个 follower 副本由于某些原因(比如宕机)而下线,之后又上线,在追赶上 leader 副本之前也处于失效状态。

        在 0.9.x 版本之前,Kafka 中还有另一个参数 replica.lag.max.messages(默认值为4000),它也是用来判定失效副本的,当一个 follower 副本滞后 leader 副本的消息数超过 replica.lag.max.messages 的大小时,则判定它处于同步失效的状态。它与 replica.lag.time.max.ms 参数判定出的失效副本取并集组成一个失效副本的集合,从而进一步剥离出分区的 ISR 集合。

       不过这个 replica.lag.max.messages 参数很难给定一个合适的值,若设置得太大,则这个参数本身就没有太多意义,若设置得太小则会让 follower 副本反复处于同步、未同步、同步的死循环中,进而又造成 ISR 集合的频繁伸缩。而且这个参数是 broker 级别的,也就是说,对 broker 中的所有主题都生效。以默认的值4000为例,对于消息流入速度很低的主题(比如 TPS 为10),这个参数并无用武之地;而对于消息流入速度很高的主题(比如 TPS 为20000),这个参数的取值又会引入 ISR 的频繁变动。所以从 0.9.x 版本开始,Kafka 就彻底移除了这一参数,相关的资料还可以参考KIP16

       具有失效副本的分区可以从侧面反映出 Kafka 集群的很多问题,毫不夸张地说:如果只用一个指标来衡量 Kafka,那么同步失效分区(具有失效副本的分区)的个数必然是首选。

ISR的伸缩

         Kafka 在启动的时候会开启两个与ISR相关的定时任务,名称分别为“isr-expiration”和“isr-change-propagation”。isr-expiration 任务会周期性地检测每个分区是否需要缩减其 ISR 集合。这个周期和 replica.lag.time.max.ms 参数有关,大小是这个参数值的一半,默认值为 5000ms。当检测到 ISR 集合中有失效副本时,就会收缩 ISR 集合。如果某个分区的 ISR 集合发生变更,则会将变更后的数据记录到 ZooKeeper 对应的 /brokers/topics/<topic>/partition/<parititon>/state 节点中。节点中的数据示例如下:

{"controller_epoch":26,"leader":0,"version":1,"leader_epoch":2,"isr":[0,1]}

         其中 controller_epoch 表示当前 Kafka 控制器的 epoch,leader 表示当前分区的 leader 副本所在的 broker 的 id 编号,version 表示版本号(当前版本固定为1),leader_epoch 表示当前分区的 leader 纪元,isr 表示变更后的 ISR 列表。

         除此之外,当 ISR 集合发生变更时还会将变更后的记录缓存到 isrChangeSet 中,isr-change-propagation 任务会周期性(固定值为 2500ms)地检查 isrChangeSet,如果发现 isrChangeSet 中有 ISR 集合的变更记录,那么它会在 ZooKeeper 的 /isr_change_notification 路径下创建一个以 isr_change_ 开头的持久顺序节点(比如 /isr_change_notification/isr_change_0000000000),并将 isrChangeSet 中的信息保存到这个节点中。

        Kafka 控制器为 /isr_change_notification 添加了一个 Watcher,当这个节点中有子节点发生变化时会触发 Watcher 的动作,以此通知控制器更新相关元数据信息并向它管理的 broker 节点发送更新元数据的请求,最后删除 /isr_change_notification 路径下已经处理过的节点。

        频繁地触发 Watcher 会影响 Kafka 控制器、ZooKeeper 甚至其他 broker 节点的性能。为了避免这种情况,Kafka 添加了限定条件,当检测到分区的 ISR 集合发生变化时,还需要检查以下两个条件:

  1. 上一次 ISR 集合发生变化距离现在已经超过5s。
  2. 上一次写入 ZooKeeper 的时间距离现在已经超过60s。

满足以上两个条件之一才可以将 ISR 集合的变化写入目标节点。

有缩减对应就会有扩充,那么 Kafka 又是何时扩充 ISR 的呢?

       随着 follower 副本不断与 leader 副本进行消息同步,follower 副本的 LEO 也会逐渐后移,并最终追赶上 leader 副本,此时该 follower 副本就有资格进入 ISR 集合。追赶上 leader 副本的判定准则是此副本的 LEO 是否不小于 leader 副本的 HW,注意这里并不是和 leader 副本的 LEO 相比。ISR 扩充之后同样会更新 ZooKeeper 中的 /brokers/topics/<topic>/partition/<parititon>/state 节点和 isrChangeSet,之后的步骤就和 ISR 收缩时的相同。

      当 ISR 集合发生增减时,或者 ISR 集合中任一副本的 LEO 发生变化时,都可能会影响整个分区的 HW。

        如下图所示,leader 副本的 LEO 为9,follower1 副本的 LEO 为7,而 follower2 副本的 LEO 为6,如果判定这3个副本都处于 ISR 集合中,那么这个分区的 HW 为6;如果 follower3 已经被判定为失效副本被剥离出 ISR 集合,那么此时分区的 HW 为 leader 副本和 follower1 副本中 LEO 的最小值,即为7。

8-2

 

冷门知识:很多读者对 Kafka 中的 HW 的概念并不陌生,但是却并不知道还有一个 LW 的概念。LW 是 Low Watermark 的缩写,俗称“低水位”,代表 AR 集合中最小的 logStartOffset 值。副本的拉取请求(FetchRequest,它有可能触发新建日志分段而旧的被清理,进而导致 logStartOffset 的增加)和删除消息请求(DeleteRecordRequest)都有可能促使LW的增长。

LEO与HW

      对于副本而言,还有两个概念:本地副本(Local Replica)和远程副本(Remote Replica),本地副本是指对应的 Log分配在当前的 broker 节点上,远程副本是指对应的Log分配在其他的 broker 节点上。在 Kafka 中,同一个分区的信息会存在多个 broker 节点上,并被其上的副本管理器所管理,这样在逻辑层面每个 broker 节点上的分区就有了多个副本,但是只有本地副本才有对应的日志。

8-3

 

      如上图所示,某个分区有3个副本分别位于 broker0、broker1 和 broker2 节点中,其中带阴影的方框表示本地副本。假设 broker0 上的副本1为当前分区的 leader 副本,那么副本2和副本3就是 follower 副本,整个消息追加的过程可以概括如下:

  1. 生产者客户端发送消息至 leader 副本(副本1)中。
  2. 消息被追加到 leader 副本的本地日志,并且会更新日志的偏移量。
  3. follower 副本(副本2和副本3)向 leader 副本请求同步数据。
  4. leader 副本所在的服务器读取本地日志,并更新对应拉取的 follower 副本的信息。
  5. leader 副本所在的服务器将拉取结果返回给 follower 副本。
  6. follower 副本收到 leader 副本返回的拉取结果,将消息追加到本地日志中,并更新日志的偏移量信息。

       了解了这些内容后,我们再来分析在这个过程中各个副本 LEO 和 HW 的变化情况。下面的示例采用同上图中相同的环境背景,如下图(左)所示,生产者一直在往 leader 副本(带阴影的方框)中写入消息。某一时刻,leader 副本的 LEO 增加至5,并且所有副本的 HW 还都为0。

8-4 8-5

 

       之后 follower 副本(不带阴影的方框)向 leader 副本拉取消息,在拉取的请求中会带有自身的 LEO 信息,这个 LEO 信息对应的是 FetchRequest 请求中的 fetch_offset。leader 副本返回给 follower 副本相应的消息,并且还带有自身的 HW 信息,如上图(右)所示,这个 HW 信息对应的是 FetchResponse 中的 high_watermark。

        此时两个 follower 副本各自拉取到了消息,并更新各自的 LEO 为3和4。与此同时,follower 副本还会更新自己的 HW,更新 HW 的算法是比较当前 LEO 和 leader 副本中传送过来的HW的值,取较小值作为自己的 HW 值。当前两个 follower 副本的 HW 都等于0(min(0,0) = 0)。

接下来 follower 副本再次请求拉取 leader 副本中的消息,如下图(左)所示。

8-6 8-7

 

       此时 leader 副本收到来自 follower 副本的 FetchRequest 请求,其中带有 LEO 的相关信息,选取其中的最小值作为新的 HW,即 min(15,3,4)=3。然后连同消息和 HW 一起返回 FetchResponse 给 follower 副本,如上图(右)所示。注意 leader 副本的 HW 是一个很重要的东西,因为它直接影响了分区数据对消费者的可见性。

两个 follower 副本在收到新的消息之后更新 LEO 并且更新自己的 HW 为3(min(LEO,3)=3)。

       在一个分区中,leader 副本所在的节点会记录所有副本的 LEO,而 follower 副本所在的节点只会记录自身的 LEO,而不会记录其他副本的 LEO。对 HW 而言,各个副本所在的节点都只记录它自身的 HW。变更本节第3张图,使其带有相应的 LEO 和 HW 信息,如下图所示。leader 副本中带有其他 follower 副本的 LEO,那么它们是什么时候更新的呢?leader 副本收到 follower 副本的 FetchRequest 请求之后,它首先会从自己的日志文件中读取数据,然后在返回给 follower 副本数据前先更新 follower 副本的 LEO。

8-8

 

         在第1节中,Kafka 的根目录下有 cleaner-offset-checkpoint、log-start-offset-checkpoint、recovery-point-offset-checkpoint 和 replication-offset-checkpoint 四个检查点文件,除了在第4节中提及了 cleaner-offset-checkpoint,其余章节都没有做过多的说明。

         recovery-point-offset-checkpoint 和 replication-offset-checkpoint 这两个文件分别对应了 LEO 和 HW。Kafka 中会有一个定时任务负责将所有分区的 LEO 刷写到恢复点文件 recovery-point-offset-checkpoint 中,定时周期由broker端参数 log.flush.offset.checkpoint.interval.ms 来配置,默认值为60000。

       还有一个定时任务负责将所有分区的 HW 刷写到复制点文件 replication-offset-checkpoint 中,定时周期由 broker 端参数 replica.high.watermark.checkpoint.interval.ms 来配置,默认值为5000。

        log-start-offset-checkpoint 文件对应 logStartOffset(注意不能缩写为 LSO,因为在 Kafka 中 LSO 是 LastStableOffset 的缩写),这个在第4节中就讲过,在 FetchRequest 和 FetchResponse 中也有它的身影,它用来标识日志的起始偏移量。各个副本在变动 LEO 和 HW 的过程中,logStartOffset 也有可能随之而动。Kafka也有一个定时任务来负责将所有分区的 logStartOffset 书写到起始点文件 log-start-offset-checkpoint 中,定时周期由 broker 端参数 log.flush.start.offset.checkpoint.interval.ms 来配置,默认值为60000。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值