副本剖析(下)(十六)

Leader Epoch的介入

      上一节的内容所陈述的都是在正常情况下的 leader 副本与 follower 副本之间的同步过程,如果 leader 副本发生切换,那么同步过程又该如何处理呢?在 0.11.0.0 版本之前,Kafka 使用的是基于 HW 的同步机制,但这样有可能出现数据丢失或 leader 副本和 follower 副本数据不一致的问题。

8-9

 

        首先我们来看一下数据丢失的问题,如上图所示,Replica B 是当前的 leader 副本(用 L 标记),Replica A 是 follower 副本。参照上一节中的过程来进行分析:在某一时刻,B 中有2条消息 m1 和 m2,A 从 B 中同步了这两条消息,此时 A 和 B 的 LEO 都为2,同时 HW 都为1;之后 A 再向 B 中发送请求以拉取消息,FetchRequest 请求中带上了 A 的 LEO 信息,B 在收到请求之后更新了自己的 HW 为2;B 中虽然没有更多的消息,但还是在延时一段时间之后返回 FetchResponse,并在其中包含了 HW 信息;最后 A 根据 FetchResponse 中的 HW 信息更新自己的 HW 为2。

8-10

 

      可以看到整个过程中两者之间的 HW 同步有一个间隙,在 A 写入消息 m2 之后(LEO 更新为2)需要再一轮的 FetchRequest/FetchResponse 才能更新自身的 HW 为2。如上图所示,如果在这个时候 A 宕机了,那么在 A 重启之后会根据之前HW位置(这个值会存入本地的复制点文件 replication-offset-checkpoint)进行日志截断,这样便会将 m2 这条消息删除,此时 A 只剩下 m1 这一条消息,之后 A 再向 B 发送 FetchRequest 请求拉取消息。

8-11

 

        此时若 B 再宕机,那么 A 就会被选举为新的 leader,如上图所示。B 恢复之后会成为 follower,由于 follower 副本 HW 不能比 leader 副本的 HW 高,所以还会做一次日志截断,以此将 HW 调整为1。这样一来 m2 这条消息就丢失了(就算B不能恢复,这条消息也同样丢失)。

       对于这种情况,也有一些解决方法,比如等待所有 follower 副本都更新完自身的 HW 之后再更新 leader 副本的 HW,这样会增加多一轮的 FetchRequest/FetchResponse 延迟,自然不够妥当。还有一种方法就是 follower 副本恢复之后,在收到 leader 副本的 FetchResponse 前不要截断 follower 副本(follower 副本恢复之后会做两件事情:截断自身和向 leader 发送 FetchRequest 请求),不过这样也避免不了数据不一致的问题。

8-12

 

       如上图所示,当前 leader 副本为 A,follower 副本为 B,A 中有2条消息 m1 和 m2,并且 HW 和 LEO 都为2,B 中有1条消息 m1,并且 HW 和 LEO 都为1。假设 A 和 B 同时“挂掉”,然后 B 第一个恢复过来并成为 leader,如下图所示。

8-13

 

       之后 B 写入消息 m3,并将 LEO 和 HW 更新至2(假设所有场景中的 min.insync.replicas 参数配置为1)。此时 A 也恢复过来了,根据前面数据丢失场景中的介绍可知它会被赋予 follower 的角色,并且需要根据 HW 截断日志及发送 FetchRequest 至 B,不过此时 A 的 HW 正好也为2,那么就可以不做任何调整了,如下图所示。

8-14

 

     如此一来 A 中保留了 m2 而 B 中没有,B 中新增了 m3 而 A 也同步不到,这样 A 和 B 就出现了数据不一致的情形。

       为了解决上述两种问题,Kafka 从 0.11.0.0 开始引入了 leader epoch 的概念,在需要截断数据的时候使用 leader epoch 作为参考依据而不是原本的 HW。leader epoch 代表 leader 的纪元信息(epoch),初始值为0。每当 leader 变更一次,leader epoch 的值就会加1,相当于为 leader 增设了一个版本号。

       与此同时,每个副本中还会增设一个矢量 <LeaderEpoch => StartOffset>,其中 StartOffset 表示当前 LeaderEpoch 下写入的第一条消息的偏移量。每个副本的 Log 下都有一个 leader-epoch-checkpoint 文件,在发生 leader epoch 变更时,会将对应的矢量对追加到这个文件中。在讲述 v2 版本的消息格式时就提到了消息集中的 partition leader epoch 字段,而这个字段正对应这里讲述的 leader epoch。

      下面我们再来看一下引入 leader epoch 之后如何应付前面所说的数据丢失和数据不一致的场景。首先讲述应对数据丢失的问题,如下图所示,这里只是多了 LE(LeaderEpoch 的缩写,当前 A 和 B 中的 LE 都为0)。

8-15

 

8-16

      同样A发生重启,之后A不是先忙着截断日志而是先发送OffsetsForLeaderEpochRequest请求给B(OffsetsForLeaderEpochRequest请求体结构如上图所示,其中包含A当前的LeaderEpoch值),B作为目前的leader在收到请求之后会返回当前的LEO(LogEndOffset,注意图中LE0和LEO的不同),与请求对应的响应为OffsetsForLeaderEpochResponse,对应的响应体结构可以参考下面第一张图,整个过程可以参考下面第二张图。

8-17

 

8-18

 

      如果 A 中的 LeaderEpoch(假设为 LE_A)和 B 中的不相同,那么 B 此时会查找 LeaderEpoch 为 LE_A+1 对应的 StartOffset 并返回给 A,也就是 LE_A 对应的 LEO,所以我们可以将 OffsetsForLeaderEpochRequest 的请求看作用来查找 follower 副本当前 LeaderEpoch 的 LEO。

       如上图所示,A 在收到2之后发现和目前的 LEO 相同,也就不需要截断日志了。之后 B 发生了宕机,A 成为新的 leader,那么对应的 LE=0 也变成了 LE=1,对应的消息 m2 此时就得到了保留,这是原本所不能的,如下图所示。之后不管 B 有没有恢复,后续的消息都可以以 LE1 为 LeaderEpoch 陆续追加到 A 中。

8-19

 

        下面我们再来看一下 leader epoch 如何应对数据不一致的场景。如下图所示,当前 A 为 leader,B 为 follower,A 中有2条消息 m1 和 m2,而 B 中有1条消息 m1。假设 A 和 B 同时“挂掉”,然后 B 第一个恢复过来并成为新的 leader。

8-20

 

      之后 B 写入消息 m3,并将 LEO 和 HW 更新至2,如下图所示。注意此时的 LeaderEpoch 已经从 LE0 增至 LE1 了。

8-21

 

      紧接着 A 也恢复过来成为 follower 并向 B 发送 OffsetsForLeaderEpochRequest 请求,此时 A 的 LeaderEpoch 为 LE0。B 根据 LE0 查询到对应的 offset 为1并返回给 A,A 就截断日志并删除了消息 m2,如下图所示。之后 A 发送 FetchRequest 至 B 请求来同步数据,最终A和B中都有两条消息 m1 和 m3,HW 和 LEO都为2,并且 LeaderEpoch 都为 LE1,如此便解决了数据不一致的问题。

8-22

 

为什么不支持读写分离

      在 Kafka 中,生产者写入消息、消费者读取消息的操作都是与 leader 副本进行交互的,从而实现的是一种主写主读的生产消费模型。数据库、Redis 等都具备主写主读的功能,与此同时还支持主写从读的功能,主写从读也就是读写分离,为了与主写主读对应,这里就以主写从读来称呼。Kafka 并不支持主写从读,这是为什么呢?

        从代码层面上来说,虽然增加了代码复杂度,但在 Kafka 中这种功能完全可以支持。对于这个问题,我们可以从“收益点”这个角度来做具体分析。主写从读可以让从节点去分担主节点的负载压力,预防主节点负载过重而从节点却空闲的情况发生。但是主写从读也有2个很明显的缺点:

  1. 数据一致性问题。数据从主节点转到从节点必然会有一个延时的时间窗口,这个时间窗口会导致主从节点之间的数据不一致。某一时刻,在主节点和从节点中 A 数据的值都为 X,之后将主节点中 A 的值修改为 Y,那么在这个变更通知到从节点之前,应用读取从节点中的 A 数据的值并不为最新的 Y,由此便产生了数据不一致的问题。
  2. 延时问题。类似 Redis 这种组件,数据从写入主节点到同步至从节点中的过程需要经历网络→主节点内存→网络→从节点内存这几个阶段,整个过程会耗费一定的时间。而在 Kafka 中,主从同步会比 Redis 更加耗时,它需要经历网络→主节点内存→主节点磁盘→网络→从节点内存→从节点磁盘这几个阶段。对延时敏感的应用而言,主写从读的功能并不太适用。

        现实情况下,很多应用既可以忍受一定程度上的延时,也可以忍受一段时间内的数据不一致的情况,那么对于这种情况,Kafka 是否有必要支持主写从读的功能呢?

       主写从读可以均摊一定的负载却不能做到完全的负载均衡,比如对于数据写压力很大而读压力很小的情况,从节点只能分摊很少的负载压力,而绝大多数压力还是在主节点上。而在 Kafka 中却可以达到很大程度上的负载均衡,而且这种均衡是在主写主读的架构上实现的。我们来看一下 Kafka 的生产消费模型,如下图所示。

8-23

 

       如上图所示,在 Kafka 集群中有3个分区,每个分区有3个副本,正好均匀地分布在3个 broker 上,灰色阴影的代表 leader 副本,非灰色阴影的代表 follower 副本,虚线表示 follower 副本从 leader 副本上拉取消息。当生产者写入消息的时候都写入 leader 副本,对于上图中的情形,每个 broker 都有消息从生产者流入;当消费者读取消息的时候也是从 leader 副本中读取的,对于上图中的情形,每个 broker 都有消息流出到消费者。

      我们很明显地可以看出,每个 broker 上的读写负载都是一样的,这就说明 Kafka 可以通过主写主读实现主写从读实现不了的负载均衡。有以下几种情况(包含但不仅限于)会造成一定程度上的负载不均衡:

  1. broker 端的分区分配不均。当创建主题的时候可能会出现某些 broker 分配到的分区数多而其他 broker 分配到的分区数少,那么自然而然地分配到的 leader 副本也就不均。
  2. 生产者写入消息不均。生产者可能只对某些 broker 中的 leader 副本进行大量的写入操作,而对其他 broker 中的 leader 副本不闻不问。
  3. 消费者消费消息不均。消费者可能只对某些 broker 中的 leader 副本进行大量的拉取操作,而对其他 broker 中的 leader 副本不闻不问。
  4. leader 副本的切换不均。在实际应用中可能会由于broker宕机而造成主从副本的切换,或者分区副本的重分配等,这些动作都有可能造成各个 broker 中 leader 副本的分配不均。

      对此,我们可以做一些防范措施。针对第一种情况,在主题创建的时候尽可能使分区分配得均衡,好在Kafka中相应的分配算法也是在极力地追求这一目标,如果是开发人员自定义的分配,则需要注意这方面的内容。对于第二和第三种情况,主写从读也无法解决。对于第四种情况,Kafka提供了优先副本的选举来达到leader副本的均衡,与此同时,也可以配合相应的监控、告警和运维平台来实现均衡的优化。

      在实际应用中,配合监控、告警、运维相结合的生态平台,在绝大多数情况下 Kafka 都能做到很大程度上的负载均衡。总的来说,Kafka 只支持主写主读有几个优点:可以简化代码的实现逻辑,减少出错的可能;将负载粒度细化均摊,与主写从读相比,不仅负载效能更好,而且对用户可控;没有延时的影响;在副本稳定的情况下,不会出现数据不一致的情况。为此,Kafka 又何必再去实现对它而言毫无收益的主写从读的功能呢?这一切都得益于 Kafka 优秀的架构设计,从某种意义上来说,主写从读是由于设计上的缺陷而形成的权宜之计。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值