Kafka 副本一致性深入理解|LEO,ISR,HW ,leader epoch

497 阅读7分钟

副本一致性概念

ISR

分区中所有的副本称为AR(Assigned Replicas)。所有与leader副本保持一定程度同步的副本(包括leader副本)组成ISR(In-Sync Replicas),也就是说 ISR 是 AR 的一个子集

所有与leader副本保持一定程度同步,同步期间内follower副本相对于leader副本而言会有一定程度的滞后。所谓“一定程度的同步”是指一定时间范围内从leader拉取消息成功的follower节点都可以进入ISR,这个范围可以通过参数进行配置默认是10s。leader副本负责维护和跟踪ISR集合中所有follower副本的滞后状态,当follower副本落后太多或失效时,leader副本会把它从ISR集合中剔除。

HW

HW(highwater mark):高水位。作用是大于等于 HW 的offset消息对于消费者是不能消费的。高水位由leader副本负责管理,高水位的取值是所有副本(leader副本和follower副本)LEO的最小值。下图反映了HW和LEO的关系。leader副本负责更新HW,会选取三个副本中最小的LEO作为HW,同时follower副本在向leader副本拉取消息时,会把HW也返回给Follower,Follower副本也会维持一个HW的值,目的是防止leader挂了以后,follower选上leader时有HW可用。

follower 什么时候会得到要更新的 HW 呢?

Kafka 中的复制协议大体有两个阶段。第一阶段:follower副本从leader副本同步数据,它取到了偏移量=4这条消息。第二阶段:在下一轮的RPC调用中follower会确认收到了偏移量=4这条消息,假定其他的follower副本也都确认成功收到了这条消息,Leader副本才会更新其高水位HW,并且会在follower再次从Leader副本同步获取数据的时候把这个高水位值放在请求响应中回传给follower副本。由此可以看出,leader副本控制着高水位HW的进度,并且会在随后的RPC调用中回传给follower副本。

follower的HW的更新比较复杂,我画了张图,你可以参考:

如上图所示,一共有5个步骤。

第一步:初始阶段,这时候leader和follower的LEO和HW都指向LEO=4的位置。

第二步:这是生产者向Leader发送了一个偏移量为4的消息,于是LEO更新为5,而HW还是4,因为follower的LEO并没有发生改变。

第三步:这时候follower定时发出请求要拉取LEO=4的消息,follower收到消息后把LEO改为5。

第四步:follower再次定时发出请求要拉取偏移量为5的消息。leader知道follower把LEO=4的消息更新成功了,于是leader的HW为5。

第五步:Leader没有偏移量为5的消息但是把HW=4返回给了follower,但是于是follower的HW改为5。

这时候,Leader和Follower完全同步完成。

你可能发现了,Leader的HW=4是在follower发出拉取LEO=5的请求时才更新的。为什么要这样设计呢?

为什么 Leader 的 HW 更新是在下一次 Follower 拉取 Leader 的请求时更新的?

原因是为了节省网络开销,因为消息系统的并发量很高,如果follower副本每次拉取leader副本都还要给leader副本返回拉取是否成功的响应,那么网络开销太高了。但是Leader必须要知道Follower是否同步成功到哪个偏移量了,这样才能更新HW。于是做了个折中的方案:在下次拉取请求的信息里面带上上次拉取成功的信息。比如上图中的第四步拉取LEO=5的请求Leader收到后会知道偏移量=4的消息follower已经成功接收到了,于是更新HW。

如果HW和LEO不一致的Follower被选为leader会发生什么?

Follower会根据HW来截断日志文件,一般LEO会大于HW。为什么要截断呢?因为Follower的信息有限,根本无法判断其他的follower的LEO也更新了,除了从leader获得HW根本没有别的办法确定,如果不截断会造成与其他副本数据不一致。也就是说,HW也是用来保证数据一致性的。

也就是说Leader和Follower的HW同步有一个间隙,这样会造成什么问题呢?

造成的问题一:丢失消息

第一步:也就是Follower要带着LEO=5的请求去拉取消息时,Leader更新了HW,但这时还没返回给Follower新的HW。

第二步:Follower重启,会根据HW截断日志文件。

第三步:Leader也挂了,原来的Follower经过选举成为了新的Leader。

第四步:这时原来的Leader又恢复了,成为了新的Follower,这时发现比Leader的HW高,所以又进行了一次截断。这样偏移量=4的消息就没了。

总结:不该截断的时候截断了。

截断会发生在两个场景:

  1. follower副本重启发现HW和LEO不一致时,截断大于等于HW的日志。
  2. follower副本从Leader副本拉取Leader的HW比follower副本上的HW小,截断大于等于Leader的HW的日志。

造成的问题二:消息错乱

第一步:生产者向leader副本发送偏移量为4的消息m4,同时Follower副本来拉取消息,leader收到出Follower要拉取偏移量为4的消息,于是把HW设置为5。

第二步:两个机器同时挂了,Follower并没有收到移量为4的消息m4。

第三步:两个机器同时重启,Follower先恢复这时leader还没恢复,Follower变为Leader,同时生产者向新的Leader发送了偏移量为4的消息m5。

第四步:这时候原来的Leader变为Follower,然后做截断,但是HW=LEO就不做任何截断。

这时候两个副本间的数据就不一致了。

总结:该截断的时候没有截断。

解决方案:引入 leader epoch

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

  • (0, 0)
  • (1, 120)

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

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

在场景1中,当follower重启以后,它会向leader发送一个LeaderEpochRequest请求,来获取自身所处的leader epoch最新的LEO是多少,因为follower和Leader所处的时代相同(leader epoch编码都是0),Leader会返回自己的LEO,也就是5给follower副本。请注意,与高水位不同的是,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开始同步获取日志。

如果想详细学习请看 掘金小册《Kafka 源码精讲》 你将获得:

  • 全面学 Kafka 各个组件,系统掌握 Kafka 的原理;
  • 系统学习 Kafak 的轮子组件,如内存缓冲池,基于 NIO 的通信模块;
  • Kafka 高可靠分布式设计;
  • Kafka 底层文件存储设计;