https://blog.csdn.net/jiayi_yao/article/details/124883964

1 简介

  在 kafka 中,高水位的作用主要是 2 个

    1)定义消息可见性,既用来告诉我们的消费者哪些消息是可以进行消费的;
    2)帮助 kafka 完成副本机制的同步。
  Kafka 分区下有可能有很多个副本用于实现冗余,从而进一步实现高可用。副本根据角色的不同可分为3种

    leader 副本:相应 clients 端读写请求的副本;
    Follower 副本:被动的备注 leader 副本的内容,不能相应 clients 端读写请求;
    ISR 副本: 包含了 leader 副本和所有与 leader 副本保持同步的 Followerer 副本。
  每个 kafka 副本对象都有两个重要的属性:LEO 和 HW。注意是所有的副本(leader + Follower)

    LEO:当前日志末端的位移(log end offset),记录了该副本底层日志(log)中下一条消息的位移值。
    HW:高水位值(High Watermark),对于同一个副本对象,其 HW 的值不会超越 LEO。
  我们假设下图是某个分区 leader 副本的高水位图

  在高水位线之下的为 已提交消息,在水位线之上的为 未提交消息,对于 已提交消息,我们的消费者可以进行消费,也就是图中 0-7 下标的消息。需要关注的是,位移值等于高水位的消息也属于未提交消息。也就是说,高水位上的消息是不能被消费者消费的。

  图中的日志末端位置,既我们所说的 LEO,他表示副本写入下一条消息的位移值。我们可以发现,位移值 15 的地方为虚框,这表示我们当前副本只有15条消息,位移值是从 0 到 14,下一条新消息的位移是 15。

  我们总说consumer无法消费未提交消息。这句话如果用以上名词来解读的话,应该表述为:consumer无法消费分区下leader副本中位移值大于分区HW的任何消息。这里需要特别注意分区HW就是leader副本的HW值。

  观察得知,对于同一个副本,我们的高水位值不会超越其 LEO 值

2 副本同步机制和HW及LEO的维护

  举一个例子,说明一下 Kafka 副本同步的全流程。

  该例子使用一个单分区且有两个副本的主题。

  当生产者发送一条消息时,Leader 和 Follower 副本的高水位是怎么被更新的

 

2.1 首先是初始状态

  

 

2.2 生产者发送消息到leader

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

  

  Leader 副本处理生产者消息的逻辑如下

    1)写入磁盘,更新 LEO = 1
    2)更新高水位HW
      当前的高水位为:0
      当前远程副本的LEO为:0
      HW = Math.max(0,0) = 0   [currentHW = max{currentHW, min(副本LEO-1, 副本LEO-2, ……,副本LEO-n)}]

   

 

2.3 follow同步

  Follow 副本尝试从 Leader 拉取消息,有消息可以拉取了,状态进一步变更为

  

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

    1)读取磁盘(页缓存)中的消息数据
    2)使用 Follower 副本发送消息请求中的位移值更新远程副本 LEO 值(Remote LEO)
      Remote LEO = fetchOffset = 0
    3)更新分区高水位值

      HW = Math.max(0,0) = 0


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

    1)写入消息到本地磁盘,更新 LEO 值为 1
    2)更新高水位
      获取 Leader 发送的高水位值:currentHW = 0
      更新高水位为:Math.mix(0,1) = 0    [min(leader的currentHW, currentLEO)]
    经过这一次拉取,我们的 Leader 和 Follower 副本的 LEO 都是 1,各自的高水位依然是0,没有被更新。

 

2.4 第二次同步

  没有新的消息

  

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

    1)读取磁盘(页缓存)中的消息数据
    2)使用 Follower 副本发送消息请求中的位移值更新远程副本 LEO 值(Remote LEO)
      Remote LEO = fetchOffset = 1
    3)更新分区高水位值
      LEO 值:Remote LEO = 1
      Leader 高水位值:currentHW = 0
      高水位值:HW = Math.max(0,1) = 1

  Follower 副本从 Leader 拉取消息的处理逻辑如下:

    1)写入消息到本地磁盘,更新 LEO 值(无变化)
    2)更新高水位

      LEO无变化还是1
      获取 Leader 发送的高水位值:currentHW = 1
      更新高水位为:HW = Math.min(1,1) = 1
    至此,一次完整的消息同步周期就结束了。事实上,Kafka 就是利用这样的机制,实现了 Leader 和 Follower 副本之间的同步。

3 同步机制存在的问题

  依托于高水位,我们不仅向外界定义了消息的可见性,又实现了副本的同步机制。

  这种副本同步机制会有什么缺点呢

 

3.1 数据丢失

 

  蓝色:已落磁盘的数据
  黄色:无任何数据
  当我们的副本进行第二次同步时,假如在 Follower 副本从 Leader 拉取消息的处理逻辑这里,我们的副本B重启了机器。

  等到副本B 重启成功后,副本B 会执行日志截断操作(根据高水位的数值进行截断),将 LEO 值调整为之前的高水位值,也就是 1。位移值为1的那条消息被副本 B 从磁盘中删除,此时副本 B 的底层磁盘文件中只保存有 1 条消息,即位移值为 0 的那条消息。

  当执行完截断日志的操作后,副本B开始从副本A拉取消息,进行正常的消息同步。这时候副本A重启了,我们会让我们的副本B成为 Leader。

  当副本A重启成功时,会自动向 Leader 看齐,此时,当 A 回来后,需要执行相同的日志截断操作,即将高水位调整为与 B 相同的值,也就是 1。

  这样操作之后,位移值为 1 的那条消息就从这两个副本中被永远地抹掉了

3.2 数据不一致

  当我们的副本B想要同步副本A的消息时,这个时候,副本A和副本B都发生了重启的操作。

  我们的副本B先启动成功,成功当选 Leader,这个时候我们的生产者会将数据发送到副本B中,也就是图中的 1。

  等到副本A启动成功时,会与 Leader 副本进行同步,发现 Leader副本的 LEO 和 HW 都为1,这个时候,副本A不需要进行任何操作。

  我们观察结果,可以看到,我们副本A的数据和副本2的数据发生了不一致的现象

 

4 Leader Epoch

4.1 簡介

  造成上述两个问题的根本原因在于HW值被用于衡量副本备份的成功与否以及在出现failture时作为日志截断的依据

  但HW值的更新是异步延迟的,特别是需要额外的FETCH请求处理流程才能更新,故这中间发生的任何崩溃都可能导致HW值的过期。

  鉴于这些原因,Kafka 0.11引入了leader epoch来取代HW值。Leader端多开辟一段内存区域专门保存leader的epoch信息,这样即使出现上面的两个场景也能很好地规避这些问题。

  所谓leader epoch实际上是一对值:(epoch,offset)。epoch表示leader的版本号,从0开始,当leader变更过1次时epoch就会+1,而offset则对应于该epoch版本的leader写入第一条消息的位移。         因此假设有两对值:

    (0, 0)则表示leader从位移0开始写入消息;共写了120条[0, 119]。

    (1, 120)则表示leader版本号是1,从位移120处开始写入消息。

  leader broker中会保存这样的一个缓存,并定期地写入到一个checkpoint文件中。

  当leader写底层log时它会尝试更新整个缓存——如果这个leader首次写消息,则会在缓存中增加一个条目;否则就不做更新。

  而每次副本重新成为leader时会查询这部分缓存,获取出对应leader版本的位移

 

4.2 epoch和副本同步

  node宕机并且恢复之后,并不会去截断自己的日志,而是先发送 一个 OffsetsForLeaderEpochRequest 请求给到 leader 副本,leader 副本收到请求之后返回当前的 LEO。

    1)如果 follower 副本的 leaderEpoch 和 leader 副本的 epoch 相同, leader 的 LEO 只可能大于或者等于 follower副本的 LEO 值,所以这个时候不会发生截断,正常同步即可。

    2)如果 follower 副本和 leader 副本的 epoch 值不同,那么 leader 副本会查找 follower 副本传过来的 epoch+1 在本地文件中存储的 StartOffset 返回给 follower 副本,也就是follow存储的leader的下一个 leader 副本的 LEO

  通过epoch的offset和自己的leo进行比较,leo<offset,则不会发生日志截断,否则,发生日志截断。

  可以解决上面情况2数据不一致的问题