术语
- Leader Epoch:一个32位单调递增的数字,代表每一个Leader副本时代,存储于每一条消息。
- Leader Epoch Start Offset:每一个Leader副本时代的第一条消息的位移。
- Leader Epoch Sequence File: 一个序列文件,每一个Leader副本时代,Leader Epoch Start Offset的变化记录存储在改文件中。
- Leader Epoch Request: follower副本通过该请求获取Leader副本的第一条消息的位移,如果Leader副本所在分区不存在序列文件,获取的是Leader副本的Log End Offset。
动机
在原来的副本同步(复制)机制中,使用HW高水位来进行日志的截断,某些情况下会导致数据的丢失和数据不一致问题。
场景1:消息丢失
kafka中的复制协议大体有两个阶段,第一阶段:follower副本从leader副本同步数据,它取到了m2这条消息。第二阶段:在下一轮的RPC调用中follower会确认收到了m2这条消息,假定其它的follower副本也都确认成功收到了这条消息,Leader副本才会更新其高水位HW,并且会在follower再次从Leader副本同步获取数据的时候会把这个高水位值放在请求响应中回传给follower副本。由此可以看出,leader副本控制着高水位HW的进度,并且会在随后的RPC调用中回传给follower副本。
复制协议还包含一个阶段,就是在follower副本初始化的时候,follower副本会先根据其记录的高水位HW来进行日志的阶段,用来保证数据的同步,然后再去同步Leader副本,这一步会出现一个bug情况:在同步的过程,如果由于follower变成了leader副本,由于日志的截断操作,可能会导致消息的丢失。
让我们来看个例子:
假设我们有两个broker,broker A和broker B,当前broker B所在的副本是Leader副本,follower副本A从leader B获取到消息m2,但是还没有向leader B确认m2已提交(第二阶段还未发生,第二阶段follower副本会确认提交m2,并且更新自己的高水位HW)。在此时,follower副本A重启了,重启以后初始化的时候,又会先根据其记录的高水位HW来进行日志的阶段,用来保证数据的同步,然后再去同步Leader副本B获取数据。好巧的是,此时leader副本B所在的broker又不幸宕机了,follower副本A经过选举变成了Leader副本A,那这个m2消息就永久的丢失了。
这个消息丢失问题的根源就是follower副本额外花费了一轮的RPC来更新自己的高水位值。在这个二阶段的间隙,如果遇到了leader副本的更换,follower会在执行日志截断时丢失数据。考虑几种简单的方案来解决这个问题,一种是leader副本等待follower副本更新完其高水位HW然后再去更新leader自己所管理的高水位HW。但是这会额外增加一轮RPC调用;一种是在从leader副本同步获取数据之前不执行日志的阶段操作,这应该有效,但是它又会出现别的问题。请看下面这个例子
场景2:不同副本上的消息错乱
假设还是存在上面那两个副本,由于停电导致两个副本都停掉了。不幸的是,不同机器上的日志会出现错乱不一致的问题,甚至在最坏的情况下,副本同步会卡住。
有一个潜在的问题是kafka是异步刷盘的,这意味着,每一次crash以后,不同分区副本上存在着任意数量的消息。机器恢复以后,任何一个副本都可能成为leader副本。如果成为leader副本所在的机器上存储的消息是最少的,那么就会丢失一些数据。
当follower副本B从leader副本A同步获取数据m2时,两个broker都宕机了,follower副本B所在的机器先重启成功并且follower副本B成为leader副本A,并且开始接受新的消息m3并且更新了自己的高水位。后来broker A也重启了,其所在的副本变成了follower副本A,follower副本A初始化开始根据Leader的高水位执行日志的截断操作,因为此时两副本的高水位值一样,follower副本A不需要截断,最终导致两个副本上的消息出现了错乱不一致的问题。
解决方案
我们通过引入Leader Epoch的概念来解决这两个问题为每一个Leader副本时代分配一个标识符,然后由领导将其添加到每个消息中。每个副本都会保留一个 [LeaderEpoch => StartOffset] 向量,用来标识消息在领导者时代的变化。当follower副本需要截断日志时,这个向量会替代高位水作为其截断操作的参照数据。follower副本会从leader副本所有的Leader Epoch向量集合中获取一个合适的leader epoch,用来截断那些在leader副本中不存在的数据。领导者副本可以根据Leader Epoch有效的告诉追随者副本需要截断到哪个偏移量。
我们可以通过这个leader epoch方案来
解决场景1的问题:
在场景1中,当follower副本A重启以后,它会向leader副本B发送一个LeaderEpochRequest请求,来获取自身所处的leader epoch最新的偏移量是多少,因为followerA和Leader副本B所处的时代相同(leader epoch编码都是0),Leader副本B会返回自己的LEO,也就是2给follower副本A。请注意,与高水印不同的是,follower副本上的offset值是0,follower副本不会截断任何消息,m2得以保留不会丢失。当followerA选为leader的时候就保留了所有已提交的日志,日志丢失的问题得到解决。
解决场景2的问题:
开始的时候副本A是leader副本A,当两个broker在崩溃后重启后,brokerB先成功重启,follower副本B 成为Leader副本B。它会开启一个新的领导者纪元LE1,开始接受消息 m3。然后brokerA又成功重启,此时副本A很自然成为follower副本A,接着它会向leader B发送一个LeaderEpoch request请求,用来确定自己应该处于哪个领导者时代,leader B会返回LE1时代的第一个位移,这里返回的值是1(也就是m3所在的位移)。follower B收到这个响应以后会根据这个位移1来截断日志,它知道了应该遗弃掉m2,从位移1开始同步获取日志。