多副本机制

副本是分布式系统中对数据服务提供的一种冗余方式。为了对外提供可用的服务,往往会对数据服务进行副本处理。

  • 数据副本:在不同的节点持久化同一份数据,当某个节点存储的数据丢失时,可以从副本中读取数据,这是分布式系统解决数据丢失问题的最有效的手段。
  • 服务副本:多个节点提供相同的服务,每个节点都有能力接收外部的请求并进行相应的处理。

Kafka 从 0.8 版本开始为分区引入了多副本机制,通过增加副本数量提升数据容灾能力。同时,Kafka通过多副本机制实现了故障自动转移,在 Kafka 集群中的某个节点失效的情况下仍然保证服务可用。从生产者发出的一条消息,首先会被写入分区的 Leader 副本,然后需要等待 ISR 集合中的所有 Follower 副本同步完成之后才能被认为已经提交,接着更新分区的 HW,进而消费者可以消费到这条消息。

副本:相对于分区而言的,即副本是特定分区的副本。一个分区包含一个或者多个副本,其中一个为 Leader 副本,其余为 Follower 副本,各个副本位于不同的 broker 节点上。只有 Leader 副本对外提供服务,Follower 副本只负责与 Leader 副本进行数据同步。

AR:分区中的所有副本的统称。

ISR:所有与 Leader 副本保持同步状态的副本集合(Leader 副本也是 ISR 集合的一员)。

LEO:标识每个分区的最后一条消息的下一个位置,分区的每个副本都有自己的 LEO。

HW:ISR 中最小的 LEO,俗称高水位,消费者只能拉取到 HW 之前的消息。

失效副本

处于失效状态(同步失效或者功能失效)的副本会被剥离出 ISR 集合,失效副本对应的分区称为失效分区,即 under-replicated 分区

可以通过如下脚本命令查看失效分区:

bin/kafka-topics.sh --bootstrap-server 10.211.55.6:9092,10.211.55.7:9092,10.211.55.8:9092 --describe --topic thing1 --under-replicated-partitions
  • 功能失效:比如副本下线。

  • 同步失效:当 ISR 集合中的一个 Follower 副本滞后 Leader 副本的时间超过 broker 端参数 – replica.lag.time.max.ms(默认10000)就会判定为同步失效。 比如 Follower 副本的 I/O 开销过大导致 Follower 副本同步速度太慢,在一段时间内都无法追赶上 Leader 副本;频繁的 Full GC 导致进程卡住,在一段时间内没有向 Leader 副本发送同步请求。

ISR的伸缩与扩充

当检测到 ISR 集合中有失效副本时,就会收缩 ISR 集合。

ISR的收缩过程如下图:

1、Kafka启动的时候会开启两个与 ISR 相关的定时任务 – “isr-expiration”、“isr-change-propagation”。

2、“isr-expiration” 任务 每隔 replica.lag.time.max.ms / 2 (默认5000ms)检测每个分区是否需要缩减其 ISR 集合,当检测到 ISR 集合中有失效副本时,就会收缩 ISR 集合。如果某个分区的 ISR 集合发生变更,则将变更后的数据记录到 ZooKeeper 的 /brokers/topics//partition//state 节点中。此外,还会将变更后的记录缓存到 isrChangeSet 中。

3、“isr-change-propagation” 任务 每隔 2500ms 检查 isrChangeSet,如果发现 isrChangeSet 中有 ISR 集合的变更记录,则在 ZooKeeper 的 “/isr_change_notification” 路径下创建一个以 “isr_change” 开头的持久顺序节点,并将 isrChangeSet 中的信息保存到这个节点中。

4、Kafka的控制器为 “/isr_change_notification” 添加一个 Watcher,当这个节点有子节点发生变化时,会触发 Watcher 的处理,通知控制器更新元数据并向它管理的 Kafka Broker 节点发送更新元数据的请求,最后删除 /isr_change_notification 路径下已经处理过的子节点。

Note:为了避免频繁触发 Watcher 的处理影响 Kafka 控制器、其它broker节点、ZooKeeper 的性能,Kafka 添加了限定条件,当检测到分区的 ISR 集合发生变化时,还需要检查以下两个条件:

(1)上一次 ISR 集合发生变化距离现在已经超过5s。

(2)上一次写入 ZooKeeper 的时间距离现在已经超过60s。

满足以上两个条件之一才可以将 ISR 集合的变化写入 ZooKeeper 中。

当 Follower 副本不断与 Leader 副本进行数据同步,并最终追赶上 Leader 副本(当前 Follower 副本的 LEO 大于等于 Leader 副本的 HW)时,Follower 副本就有资格进入 ISR 集合。ISR 集合的扩充过程与收缩过程相似,这里不再展开分析。

副本更新LEO、HW的过程

生产者往 Leader 副本写入消息,消息被追加到 Leader 副本的本地日志,并且会更新 LEO。

之后 Follower 副本向 Leader 副本拉取消息,并且带有自身的 LEO 信息。

Leader 副本读取本地日志,返回给 Follower 副本对应的消息,并且带有自身的 HW 信息。

Follower 副本收到 Leader 副本返回的消息,会将消息追加到本地日志中,并且更新 LEO、HW。

Follower 副本更新 HW 的算法:比较当前 LEO 与 Leader 副本返回的 HW 的值,取较小值作为自己的 HW

Follower 副本再次请求拉取 Leader 副本中的消息,并且带有更新后的 HW 的值。

Leader 副本收到 Follower 的请求后,选择最小的 LEO 作为 HW。

Follower 副本收到 Leader 副本返回的消息,会接着将消息追加到本地日志中,并且更新 LEO、HW。

Leader Epoch 的引入

在 0.11.0.0 版本之前,Kafka 使用的是基于 HW 的同步方式。这种方式可能会出现 数据丢失 或者 Leader 副本和 Follower 副本数据不一致 的问题。

Follower 副本在更新 LEO 为 2 和 更新 HW 为 2 之间存在一轮的 FetchRequest/FetchResponse。如果在这个过程中,Follower 副本宕机了,那么重启后会根据之前记录的 HW(读取 replication-offset-checkpoint 文件)进行日志截断,那么会导致 b 这条消息被删除。然后 Follower 副本再向 Leader 副本拉取消息,如果此时 Leader 副本宕机并且 Follower 副本被选举为新的 Leader,接着原来的 Leader 副本恢复后就会成为 Follower 副本,由于 Follower 副本的 HW 不能比 Leader 副本的 HW 高,所以还会进行一次日志截断,由此 b 这条消息就丢失了。或者原来的 Leader 副本无法恢复,b 这条消息也是会丢失的。

如果 min.insync.replicas=1 的场景下,上述两个副本处于挂掉状态,Replica B 先恢复并成为 Leader 副本,接着写入消息 c 并更新 LEO、HW 为 2。此时 Replica A 也恢复过来了,成为 Follower 副本并且需要根据 HW 截断日志以及向 Leader 副本拉取数据,由于此时 Replica A 的 HW 也是 2,所以可以不做任何调整。如此一来,Replica A 和 Replica B 就会出现数据不一致的问题。

为了解决上述两个问题,Kafka 从0.11.0.0开始引入了 Leader Epoch 的概念,在需要截断数据的时候使用 Leader Epoch 作为参考依据。Leader Epoch 初始值为 0,代表 Leader 的纪元,每次 Leader 变更,该值都会加一。每个副本在本地的 leader-epoch-checkpoint 文件中记录 StartOffset> 信息。

  • 解决数据丢失问题:Follower 副本重启,先发送请求(包含 Leader Epoch 值)给 Leader 副本,如果 Leader 副本收到请求后发现当前的 Leader Epoch 与 Follower 传过来的 Leader Epoch 一致,则返回当前的 LEO;如果不一致,则 Leader 副本会查找 Follower 传过来的 Leader Epoch + 1 对应的 StartOffset 返回给 Follower 副本。Follower 副本根据 Leader 副本返回的 StartOffset 判断是否需要截断日志。

StartOffset:当前 Leader Epoch 下的写入的第一条消息的偏移量

  • 解决数据不一致的问题:还是在 min.insync.replicas=1 的场景下,Replica A、Replica B 都处于挂掉的状态,Replica B 先恢复通过选举成为 Leader并更新 LE,然后写入 c 这条消息并更新 LEO、HW。这时 Replica A 恢复过来成为 Follower,向 Replica B 发送请求并携带自身的 LE。Replica B 接收到请求后,比较两者的 LE 发现不一致,然后返回 Replica B 当前 LE 对应的 StartOffset。Replica A 比较 Replica B 返回的 StartOffset 与自己的 LEO ,判断是否需要日志截断。