Kafka leader副本选举与消息丢失场景讨论
如果某个broker挂了,leader副本在该broker上的分区就要重新进行leader选举。来简要描述下leader选举的过程
- 1.4.1 KafkaController会监听ZooKeeper的/brokers/ids节点路径,一旦发现有broker挂了,执行下面的逻辑。这里暂时先不考虑KafkaController所在broker挂了的情况,KafkaController挂了,各个broker会重新leader选举出新的KafkaController
-
1.4.2 leader副本在该broker上的分区就要重新进行leader选举,目前的选举策略是
- 1.4.2.1 优先从isr列表中选出第一个作为leader副本
-
1.4.2.2 如果isr列表为空,则查看该topic的unclean.leader.election.enable配置。
unclean.leader.election.enable:为true则代表允许选用非isr列表的副本作为leader,那么此时就意味着数据可能丢失,为false的话,则表示不允许,直接抛出NoReplicaOnlineException异常,造成leader副本选举失败。
- 1.4.2.3 如果上述配置为true,则从其他副本中选出一个作为leader副本,并且isr列表只包含该leader副本。
一旦选举成功,则将选举后的leader和isr和其他副本信息写入到该分区的对应的zk路径上。
- 1.4.3 KafkaController向上述相关的broker上发送LeaderAndIsr请求,将新分配的leader、isr、全部副本等信息传给他们。同时将向所有的broker发送UpdateMetadata请求,更新每个broker的缓存的metadata数据。
- 1.4.4 如果是leader副本,更新该分区的leader、isr、所有副本等信息。如果自己之前就是leader,则现在什么操作都不用做。如果之前不是leader,则需将自己保存的所有follower副本的logEndOffsetMetadata设置为UnknownOffsetMetadata,之后等待follower的fetch,就会进行更新
- 1.4.5 如果是follower副本,更新该分区的leader、isr、所有副本等信息
然后将日志截断到自己保存的highWatermarkMetadata位置,即日志的logEndOffsetMetadata等于了highWatermarkMetadata
最后创建新的fetch请求线程,向新leader不断发送fetch请求,初次fetch的offset是logEndOffsetMetadata。
上述重点就是leader副本的日志不做处理,而follower的日志则需要截断到highWatermarkMetadata位置。
至此,算是简单描述了分区的基本情况,下面就针对上述过程来讨论下kafka分区的高可用和一致性问题。
2 消息丢失
2.1 消息丢失的场景
哪些场景下会丢失消息?
- acks= 0、1,很明显都存在消息丢失的可能。
- 即使设置acks=-1,当isr列表为空,如果unclean.leader.election.enable为true,则会选择其他存活的副本作为新的leader,也会存在消息丢失的问题。
- 即使设置acks=-1,当isr列表为空,如果unclean.leader.election.enable为false,则不会选择其他存活的副本作为新的leader,即牺牲了可用性来防止上述消息丢失问题。
- 即使设置acks=-1,并且选出isr中的副本作为leader的时候,仍然是会存在丢数据的情况的:
s1 s2 s3是isr列表,还有其他副本为非isr列表,s1是leader,一旦某个日志写入到s1 s2 s3,则s1将highWatermarkMetadata提高,并回复了客户端ok,但是s2 s3的highWatermarkMetadata可能还没被更新,此时s1挂了,s2当选leader了,s2的日志不变,但是s3就要截断日志了,这时已经回复客户端的日志是没有丢的,因为s2已经复制了。
但是如果此时s2一旦挂了,s3当选,则s3上就不存在上述日志了(前面s2当选leader的时候s3已经将日志截断了),这时候就造成日志丢失了。
2.2 不丢消息的探讨
其实我们是希望上述最后一个场景能够做到不丢消息的,但是目前的做法还是可能会丢消息的。
丢消息最主要的原因是:
由于follower的highWatermarkMetadata相对于leader的highWatermarkMetadata是延迟更新的,当leader选举完成后,所有follower副本的截断到自己的highWatermarkMetadata位置,则可能截断了已被老leader提交了的日志,这样的话,这部分日志仅仅存在新的leader副本中,在其他副本中消失了,一旦leader副本挂了,这部分日志就彻底丢失了
这个截断到highWatermarkMetadata的操作的确太狠了,但是它的用途有一个就是:避免了日志的不一致的问题。通过每次leader选举之后的日志截断,来达到和leader之间日志的一致性,避免出现日志错乱的情况。
ZooKeeper和Raft的实现也有类似的日志复制的问题,那ZooKeeper和Raft的实现有没有这种问题呢?他们是如何解决的呢?
Raft并不进行日志的截断操作,而是会通过每次日志复制时的一致性检查来进行日志的纠正,达到和leader来保持一致的目的。不截断日志,那么对于已经提交的日志,则必然存在过半的机器上从而能够保证日志基本是不会丢失的。
ZooKeeper只有当某个follower的记录超出leader的部分才会截断,其他的不会截断的。选举出来的leader是经过过半pk的,必然是包含全部已经被提交的日志的,即使该leader挂了,再次重新选举,由于不进行日志截断,仍然是可以选出其他包含全部已提交的日志的(有过半的机器都包含全部已提交的日志)。ZooKeeper对于日志的纠正则是在leader选举完成后专门开启一个纠正过程。
kafka的截断到highWatermarkMetadata的确有点太粗暴了,如果不截断日志,则需要解决日志错乱的问题,即使不能够像ZooKeeper那样花大代价专门开启一个纠正过程,可以像Raft那样每次在fetch的时候可以进行不断的纠正。这一块还有待继续关注。