kafka中的HW和Leader Epoch

1,387 阅读16分钟

HW和LEO

  • HW是 High Watermark 的缩写,俗称高水位,它标识了一个特定的消息偏移量(offset),消费者只能拉取到这个 offset 之前的消息。HW 保证了 Kafka 集群中消息的一致性。确切地说,是保证了 Partition 的 Follower 与 Leader 间数 据的一致性。
  • LEO是 Log End Offset 的缩写,日志最后消息的偏移量。消息是被写入到 Kafka 的日志文件中的, 它标识当前日志文件中下一条待写入消息的 offset。

HW的作用

在 Kafka 中,高水位的作用主要有 2 个。

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

下面这张图展示了多个与高水位相关的 Kafka 术语。

image.png

我们假设这是某个分区 Leader 副本的高水位图。在分区高水位以下的消息被认为是已提交消息,反之就是未提交消息。消费者只能消费已提交消息,即图中位移小于 8 的所有消息。注意(这里不讨论 Kafka 事务,因为事务机制会影响消费者所能看到的消息的范围,它不只是简单依赖高水位来判断。它依靠一个名为 LSO(Log Stable Offset)的位移值来判断事务型消费者的可见性。)

注意:位移值等于高水位的消息也属于未提交消息。也就是说,高水位上的消息是不能被消费者消费的。

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

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

高水位更新机制

现在,我们知道了每个副本对象都保存了一组高水位值和 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,但它不会更新远程副本的高水位值,也就是我在图中标记为灰色的部分。

远程副本的主要作用是,帮助 Leader 副本确定其高水位,也就是分区高水位。

HW和LEO的更新策略如下:

image.png

关于Leader 副本保持同步的理解,其判断条件有两个:

  1. 该远程 Follower 副本在 ISR 中。
  2. 该远程 Follower 副本 LEO 值落后于 Leader 副本 LEO 值的时间,不超过 Broker 端参数 replica.lag.time.max.ms 的值。如果使用默认值的话,就是不超过 10 秒。

从Leader 副本和 Follower 副本两个维度,来总结一下高水位和 LEO 的更新机制。

Leader 副本

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

  1. 写入消息到本地磁盘。
  2. 更新分区高水位值。(也即Leader副本的高水位)
    i. 获取 Leader 副本所在 Broker 端保存的所有远程副本 LEO 值(LEO-1,LEO-2,……,LEO-n)。
    ii. 获取 Leader 副本高水位值:currentHW。
    iii. 更新 currentHW = max{currentHW, min(LEO-1, LEO-2, ……,LEO-n)}。 处理 Follower 副本拉取消息的逻辑如下:
  3. 读取磁盘(或页缓存)中的消息数据。
  4. 使用 Follower 副本发送请求中的位移值更新远程副本 LEO 值。
  5. 更新分区高水位值(具体步骤与处理生产者请求的步骤相同)。

Follower 副本

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

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

副本同步机制解析

举一个实际的例子,说明一下 Kafka 副本同步的全流程。该例子使用一个单分区且有两个副本的主题。

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

首先是初始状态。下面这张图中的 remote LEO 就是刚才的远程副本的 LEO 值。在初始状态时,所有值都是 0。

image.png

此时,Leader 副本成功将消息写入了本地磁盘,故 LEO 值被更新为 1。

Follower 再次尝试从 Leader 拉取消息。和之前不同的是,这次有消息可以拉取了,因此状态进一步变更为:

image.png follower 拉取到一条消息,带着消息和leader的 HW(0)&LEO(1)返回自身更新自己的LEO=1, 更新自己的HW=min(follower 自身 LEO(1) 和 leader HW(0))=0

这时,Follower 副本也成功地更新 LEO 为 1。此时,Leader 和 Follower 副本的 LEO 都是 1,但各自的高水位依然是 0,还没有被更新。它们需要在下一轮的拉取中被更新,如下图所示:

image.png Follower带着自己的 HW(0)&LEO(1) 去请求leader .此时leader 的HW更新为1,leader 保存的follower的LEO更新为1,带着leader 的 HW(1)&LEO(1)返回自身,更新自己的 HW&LEO

在新一轮的拉取请求中,由于位移值是 0 的消息已经拉取成功,因此 Follower 副本这次请求拉取的是位移值 =1 的消息。Leader 副本接收到此请求后,更新远程副本 LEO 为 1,然后更新 Leader 高水位为 1。做完这些之后,它会将当前已更新过的高水位值 1 发送给 Follower 副本。Follower 副本接收到以后,也将自己的高水位值更新成 1。至此,一次完整的消息同步周期就结束了。事实上,Kafka 就是利用这样的机制,实现了 Leader 和 Follower 副本之间的同步。

这种HW和LEO更新策略有个很明显的问题,即follower的HW更新需要follower的2轮fetch中的leader返回才能更新,而Leader的HW已更新。

HW机制如何导致数据丢失

image.png 1. follower故障 follower发生故障后会被临时踢出ISR,待该follower恢复后,follower会读取本地磁盘记录的上次的HW,并将log文件高于HW的部分截取掉,从HW开始向leader进行同步。等该follower的LEO大于等于该Partition的HW,即follower追上leader之后,就可以重新加入ISR了。

如图所示,第三个副本同步的时候挂掉了,等其重后,会按照之前的HW重新同步.(即13,14,15不要了)。
2.leader故障 leader发生故障之后,会从ISR中选出一个新的leader,之后,为保证多个副本之间的数据一致性,其余的follower会先将各自的log文件高于HW的部分截掉,然后从新的leader同步数据。

如果leader发生故障,如果第二个副本选为leader,则第三副本,会将其高于HW的数据截取。以第二个为准进行同步。

如果第三个副本选为leader,则第二个以第三个为准,开始从HW进行同步,不能管如何,都会丢失数据。但是保证了数据的一致性。

注意:这只能保证副本之间的数据一致性,并不能保证数据不丢失或者不重复。

Leader Epoch

依托于高水位,Kafka 既界定了消息的对外可见性,又实现了异步的副本同步机制。不过,我们还是要思考一下这里面存在的问题。

从上面的分析中,我们知道,Follower 副本的高水位更新需要一轮额外的拉取请求才能实现。如果把上面那个例子扩展到多个 Follower 副本,情况可能更糟,也许需要多轮拉取请求。也就是说,Leader 副本高水位更新和 Follower 副本高水位更新在时间上是存在错配的。这种错配是很多“数据丢失”或“数据不一致”问题的根源。基于此,社区在 0.11 版本正式引入了 Leader Epoch 概念,来规避因高水位更新错配导致的各种不一致问题。

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

  1. Epoch。一个单调增加的版本号。每当副本领导权发生变更时,都会增加该版本号。小版本号的 Leader 被认为是过期 Leader,不能再行使 Leader 权力。
  2. 起始位移(Start Offset)。Leader 副本在该 Epoch 值上写入的首条消息的位移。

所谓Leader epoch实际上是一对值:<epoch, offset>:

  1. epoch表示Leader的版本号,从0开始,Leader变更过1次,epoch+1
  2. offset对应于该epoch版本的Leader写入第一条消息的offset。因此假设有两对值:
<0, 0>
<1, 120>

则表示第一个Leader从位移0开始写入消息;共写了120条[0, 119];而第二个Leader版本号是1,从位移120处开始 写入消息。

  1. Leader broker中会保存这样的一个缓存,并定期地写入到一个 checkpoint 文件中。
  2. 当Leader写Log时它会尝试更新整个缓存:如果这个Leader首次写消息,则会在缓存中增加一个条目;否则就不做更新。
  3. 每次副本变为Leader时会查询这部分缓存,获取出对应Leader版本的位移,则不会发生数据不一致和丢失的情况。

HW丢失

在解释Leader Epoch 是如何防止数据丢失的。先说一下HW是怎么造成数据丢失,我们在上面说过了,再具体说一下:请先看下图。

image.png 开始时,副本 A 和副本 B 都处于正常状态,A 是 Leader 副本。某个使用了默认 acks 设置(ack=1)的生产者程序向 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 回来后,此时的HW是2,由于副本的HW不能比Leader的高,需要执行相同的日志截断操作,即将高水位调整为与 B 相同的值,也就是 1。这样操作之后,位移值为 1 的那条消息就从这两个副本中被永远地抹掉了。这就是这张图要展示的数据丢失场景。

总结概括步骤:
第一阶段:A是Leader,此时A的HW是2,B的HW是1,此时B携带自身的额LEO拉取A中的消息,此时B重启,B还没有从A中拉取消息更新自身的HW时发生了重启。
第二阶端:B重启后发现LEO和本身的HW不一致,则根据HW(这个值会存入本地的replication-offset-checkpoint)进行日志截断,这时会将M2的消息进行删除。此时的B中只有M1这条消息。从A中拉取消息
第三阶端:B从A中拉取消息时,此时A宕机,说明Leader挂了,选举出B为新的Leader,HW就是1。
第四阶端:A主机恢复,此时的A则是一个Follower副本,此时的HW是2,由于副本的HW不能比Leader的高,还会做一次日志截断,此时的HW调整为1。那么M2的消息就会丢失,即使A没有恢复正常同样M2消息还是丢失了。

Leader Epoch 如何不丢失

如何利用 Leader Epoch 机制来规避这种数据丢失。请看下图

image.png

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

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

总结概括步骤:
第一阶段:A是Leader,此时A的HW是2,B的HW是1,此时B携带自身的额LEO拉取A中的消息,此时B重启,B还没有从A中拉取消息更新自身的HW时发生了重启。
第二阶段:Follower 副本 B 重启回来后,需要向 A 发送一个特殊的请求去获取 Leader 的 LEO 值。在这个例子中,该值为 2。当获知到 Leader LEO=2 后,B 发现该 LEO 值不比它自己的 LEO 值小,而且缓存中也没有保存任何起始位移值 > 2 的 Epoch 条目,因此 B 无需执行任何日志截断操作。这是对高水位机制的一个明显改进,即副本是否执行日志截断不再依赖于高水位进行判断。
第三阶段:副本 A 宕机了,B 成为 Leader。此时B的LEO为2,HW为2
第四阶段:当 A 重启回来后,执行与 B 相同的逻辑判断,发现也不用执行日志截断,至此位移值为 1 的那条消息M2在两个副本中均得到保留。后面当生产者程序向 B 写入新消息时,副本 B 所在的 Broker 缓存中,会生成新的 Leader Epoch 条目:[Epoch=1, Offset=2]。之后,副本 B 会使用这个条目帮助判断后续是否执行日志截断操作。这样,通过 Leader Epoch 机制,Kafka 完美地规避了这种数据丢失场景。

只需要知道每个副本都引入了新的状态来保存自己当leader时开始写入的第一条消息的offset以及leader版本。这样在恢复的时候完全使用这些信息而非HW来判断是否需要截断日志。

HW数据不一致

image.png

分为四个阶端,灰色背景的为Leader
第一阶段:A节点的HW和LEO都是2,B的HW是1,LEO是2此时正要去Leader拉取数据,A/B两个节点都都宕机
第二阶端:B恢复后发现LEO和本身的HW不一致,则根据HW(这个值会存入本地的replication-offset-checkpoint)进行日志截断,这时会将M2的消息进行删除。此时的B中只有M1这条消息
第三阶端:此时B是Leader追加消息M3同步更新HW和LEO都是2
第四阶端:此时A恢复后自动成为Follower,此时的HW是2,从B(Leader)拉取消息时返回给A的HW同样是2,这样A不用做任何操作,此时我们发现A中有M1、M2然而B中却是M1和M3。

leader epoch规避数据不一致

image.png

image.png

image.png

第一阶段:A节点的HW和LEO都是2,B的HW和LEO是1,此时正要去Leader拉取数据,A/B两个节点都都宕机
第二阶段:B重启后成为leader,其并不需要依据HW截断,所以其0保留,当接收生产者发来的消息时m3,更新epoch<1,1>.此时leader epoch已从le0更新为le1.
第三阶端:此时A回复成功后成为Follwer并向 B 发送 OffsetsForLeaderEpochRequest 请求,此时 A 的 LeaderEpoch 为 le0。B 根据 le0 查询到对应的 LEO 为1并返回给 A,A 就截断日志并删除了消息 m2.
第四阶端: A 发送 FetchRequest 至 B 请求来同步数据,最终A和B中都有两条消息 m1 和 m3,HW 和 LEO都为2,并且 LeaderEpoch 都为 LE1,如此便解决了数据不一致的问题。