kafka里的HW(High Watermark)是与消息offset相关的一个概念,本文会从下面这四个方面来展开。

  1. HW的作用
  2. HW的更新机制
  3. 副本同步机制解析
  4. leader epoch

HW的作用

在kafka中,HW的作用主要有2个:

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

image.png

假设这是某个分区Leader副本的HW图,我们可以清楚地看到‘已提交消息’与‘未提交消息’,在展开之前先解释下何为已提交消息:

当kafka的若干个broker成功地接收到一条消息并写入到日志文件后,它们会告诉生产者这条消息已成功提交,那么这条消息就是‘已提交消息’。注意这里使用的是若干个broker,这个是由使用者配置决定的,使用者可以配置只要有一个broker成功保存该消息就算是已提交,也可以配置令所有broker都成功保存该消息才算是已提交。

回到HW上来,在上图中已提交的消息才会对消费者可见,才会被消费者拉取消费。位移值等于高水位的消息也属于未提交消息,即高水位上的消息是不能被消费者消费的。

图中还有一个日志末端的概念,Log End Offset(LEO)。它表示副本写入下一要消息的位移值。注意,数字 15 所在的方框是虚线,这就说明,这个副本当前只有 15 条消息,位移值是从 0 到 14,下一条新消息的位移是 15。显然,介于高水位和 LEO 之间的消息就属于未提交消息。这也从侧面告诉了我们一个重要的事实,那就是:同一个副本对象,其高水位值不会大于 LEO 值

高水位和LEO是副本对象的两个重要属性。kafka所有副本都有对应的高水位和LEO值,而不仅仅是leader副本。只不过leader副本比较特殊,kafka使用leader副本的高水位来定义所在分区的高水位。换句话说,分区的高水位就是其leader副本的高水位。

高水位更新机制

我们知道每个副本都保存了一组HW值和LEO值,实际上,在Leader副本所在的broker上,还保存了其他follower副本的LEO值。
image.png
上图中,我们可以看到,Broker 0上保存了某分区leader副本和所有follower副本的LEO值,而Broker 1上仅仅保存了该分区的某个follower副本,kafka将Broker 0上保存的这些follower副本又称为远程副本(Remote Replica)。kafka副本机制在运行过程中,会更新Broker 1上follower副本的高水位和LEO的值,同时也会更新Broker 0上的leader副本的高水位和LEO以及所有远程副本的LEO,但它不会更新远程副本的高水位值,也就是图中标记为灰色的部分。

为什么要在Broker 0上保存这些远程副本呢?其实,它们的主要作用是帮助leader副本确定其高水位,也就是分区高水位。下图是各副本高水位值与LEO更新机制:
image.png

这里先解释下何为与leader副本保持同步,判断有两个条件:

  1. 该远程follower副本在ISR(副本同步队列)中
  2. 该远程follower副本LEO值落后于leader副本LEO值的时间,不超过broker端参数replica.lag.time.max.mx的值(默认值为10s,如果一个 follower 在这个时间内没有发送任何 fetch 请求或者在这个时间内没有追上 leader 当前的 log end offset,那么将会从 isr 中移除)。

下面分别从leader副本和follower副本两个维度来总结一下高水位和LEO的更新机制。

leader副本

处理生产者请求的逻辑如下:

  1. 写入消息到本地磁盘
  2. 更新分区高水位值。步骤有:1)获取leader副本所在broker端保存的所有远程副本LEO值;2)获取leader副本高水位值currentHW;3)更新currentHW=min(currentHW,所有远程副本LEO值)

处理远程follower副本逻辑如下:

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

follower副本

从leader拉取消息的处理逻辑如下:

  1. 写入消息到本地磁盘
  2. 更新LEO值
  3. 更新高水位值。步骤有:1)获取leader发送的高水位值currentHW。2)获取2里的LEO值:currentLEO。3)更新高水位为min(currentHW,currentLEO).

简单的说就是:

  • Leader端:取Leader端和Follower端的Leo的集合的最小值
  • Follower端:取Leader端返回给Follower端的HW的

HW的更新步骤还可以参考:Kafka HW及Epoch

副本同步机制

看个例子,说明一下kafka副本同步的全流程,该例子使用一个单分区且有两个副本的主题。
当生产者发送一条消息时,Leader 和 Follower 副本对应的高水位是怎么被更新的呢?我给出了一些图片,我们一一来看。
首先是初始状态,下面这张图中的 remote LEO 就是刚才的远程副本的 LEO 值。在初始状态时,所有值都是 0。
image.png
当生产者给主题分区发送一条消息后,状态变更为:
image.png
此时,leader副本成功将消息写入了本地磁盘,故LEO值被更新为1.
follower再次尝试从leader拉取消息。和之前不同的是,这次可以拉取到消息了,因此状态进一步变更为:
image.png
这时,follower副本也成功更新LEO为1.此时,leader和follower副本的LEO都是1,但各自的高水位依然是0,它们需要在下一轮的拉取中被更新,如下图:
image.png
在新一轮的拉取请求中,由于位移值是0的消息已经被拉取成功,因此follower副本这次请求拉取的是位移值=1的消息。leader副本接收到此请求后,更新远程副本LEO为1,然后更新leader高水位1,做完这些之后,它会将当前已更新过的高水位值1发送给follower副本,follower副本接收到后,也将自己的高水位值更新成1.至此,一次完整的消息同步周期就结束了。事实上,kafka就是利用这样的机制,实现了leader与follower副本之间的同步。

leader epoch

根据上文的分析,依据高水位,kafka既界定了消息的对外可见性,又实现了异步的副本同步机制,不过,我们还要思考一下这里存在的问题。

从刚才的分析中,我们知道,follower副本的高水位更新需要一轮额外的拉取请求才能实现。如果将上面那个例子扩展到多个follower副本,情况会比较糟糕,那会需要多轮请求拉取。也就是说,leader副本高水位更新和follower副本高水位更新在时间上是存在错配的,副本在初始化过程中会先使用HW高水位来进行日志的截断,数据丢失或数据不一致问题的根源基于此,kafka在 0.11 版本正式引入了 Leader Epoch 概念,来规避因高水位更新错配导致的各种不一致问题。

所谓leader epoch,我们大致可以认为是leader版本。它由两部分数据组成。

  1. epoch。一个单调增加的版本号。每当副本领导权发生变更时,都会增加该版本号,小版本号的leader被认为是过期epoch,不能再行使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的起始位移,避免数据丢失和不一致的情况。

看个例子。它展示的是leader epoch是如何防止数据丢失的。
我们先看如果单纯依赖高水位是怎么造成数据丢失的。
image.png

这里解释下,开始时,副本A和副本B都处于正常状态,A是leader副本。某个使用了默认acks设置(默认为1,leader写成功即可)的生产者程序向A发送了两条消息,A 全部写入成功,此时kafka会通知生产者两条消息全部发送成功。
现在我们假设leader和follower都写入了这两条消息,而且leader副本的高水位也已经更新,但follower副本高水位还没更新(follower高水位的更新与leader端有时间错配).倘若此时副本B 所在的broker宕机,当它重启回来后,副本B会执行日志截断操作,将LEO值调整为之前的高水位值,也就是1,这就是说,位移值为1的那条消息被副本B从磁盘中删除,此时副本B的底层磁盘文件中只保存有1条消息,即位移值为0的那条消息。

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

严格来说,这个场景发生的前提是Broker 端参数 min.insync.replicas 设置为 1。此时一旦消息被写入到 Leader 副本的磁盘,就会被认为是“已提交状态”,但现有的时间错配问题导致 Follower 端的高水位更新是有滞后的。如果在这个短暂的滞后时间窗口内,接连发生 Broker 宕机,那么这类数据的丢失就是不可避免的。

现在,我们来看下如何利用 Leader Epoch 机制来规避这种数据丢失。我依然用图的方式来说明。

image.png

场景和之前大致是类似的,只不过引用 Leader Epoch 机制后,Follower 副本 B 重启回来后,需要向 A 发送一个特殊的请求去获取 Leader 的 LEO 值。在这个例子中,该值为 2。当获知到 Leader LEO=2 后,B 发现该 LEO 值不比它自己的 LEO 值小,而且缓存中也没有保存任何起始位移值大于2 的 Epoch 条目(<1,2>里的start offset为2),因此 B 无需执行任何日志截断操作。这是对高水位机制的一个明显改进,即副本是否执行日志截断不再依赖于高水位进行判断

现在,副本 A 宕机了,B 成为 Leader。同样地,当 A 重启回来后,执行与 B 相同的逻辑判断,发现也不用执行日志截断,至此位移值为 1 的那条消息在两个副本中均得到保留。后面当生产者程序向 B 写入新消息时,副本 B 所在的 Broker 缓存中,会生成新的 Leader Epoch 条目:[Epoch=1, Offset=2]。之后,副本 B 会使用这个条目帮助判断后续是否执行日志截断操作。这样,通过 Leader Epoch 机制,Kafka 完美地规避了这种数据丢失场景。

参考的文章:深入浅出kafka原理-6-kafka副本同步leader epoch机制

KIP-101 - Alter Replication Protocol to use Leader Epoch rather than High Watermark for Truncation


步履不停
38 声望13 粉丝

好走的都是下坡路