1 引言
Kafka支持消息的冗余备份,可以设置对应主题的副本数(--replication-factor
可在创建主题的时候指定用于设置主题的副本数,offsets.topic.replication.factor
设置消费主题_consumer_offsets的
副本数,默认为3),每个副本包含的消息一样(但不是完全一致,可能从副本的数据较主副本稍微有些落后)。
每个分区的副本集合中会有一个副本被选举为主副本(leader),其他为从副本(follower),所有的读写请求由主副本对外提供,从副本负责将主副本的数据同步到自己所属分区,如果主副本所在分区宕机,则会重新选举出新的主副本对外提供服务。
Partition 与 Broker 对应关系:副本分配机制
2 ISR集合
ISR(In-Sync Replica)集合,表示目前可以用的副本集合,每个分区中的leader副本会维护此分区的ISR集合。这里的可用是指从副本的消息量与主副本的消息量相差不大,加入ISR集合中的副本必须满足以下几个条件:
- 副本所在节点需要与ZooKeeper维持心跳。
- 从副本的最后一条消息的offset需要与主副本的最后一条消息offset差值不超过设定阈值(
replica.lag.max.messages
)或者副本的LEO落后于主副本的LEO时长不大于设定阈值(replica.lag.time.max.ms
),官方推荐使用后者判断,并在新版本kafka0.10.0移除了replica.lag.max.messages
参数。
如果从副本不满足以上的任意条件,则会将其踢出ISR集合,当其再次满足以上条件之后又会被重新加入集合中。ISR的引入主要是解决同步副本与异步复制两种方案各自的缺陷(同步副本中如果有个副本宕机或者超时就会拖慢该副本组的整体性能;如果仅仅使用异步副本,当所有的副本消息均远落后于主副本时,一旦主副本宕机重新选举,那么就会存在消息丢失情况)。
3 HW&LEO
HW(High Watermark)是一个比较特殊的offset标记,消费端消费时只能拉取到小于HW的消息而HW及之后的消息对于消费者来说是不可见的,该值由主副本管理,当ISR集合中的全部从副本都拉取到HW指定消息之后,主副本会将HW值+1,即指向下一个offset位移,这样可以保证HW之前消息的可靠性。
LEO(Log End Offset)表示当前副本最新消息的下一个offset,所有副本都存在这样一个标记,如果是主副本,当生产端往其追加消息时,会将其值+1。当从副本从主副本成功拉取到消息时,其值也会增加。
3.1 从副本更新LEO与HW
从副本的数据来自主副本,通过向主副本发送fetch请求获取数据,从副本的LEO值会保存在两个地方:一个是自身所在的节点,一个是主副本所在的节点。自身节点保存LEO主要是为了更新自身的HW值,主副本保存从副本的LEO也是为了更新其HW。
当从副本每写入一条新消息就会增加其自身的LEO,主副本收到从副本的fetch请求,会先从自身的日志中读取对应数据,在数据返回给从副本之前会先去更新其保存的从副本LEO值。一旦从副本数据写入完成,就会尝试更新自己的HW值,比较LEO与fetch响应中主副本的返回HW,取最小值作为新的HW值。
3.2 主副本更新LEO与HW
主副本在日志写入时就会更新其自身的LEO值,与从副本类似。而主副本的HW值是分区的HW值,决定分区数据对应消费端的可见性,以下四种情况,主副本会尝试更新其HW值:
-
副本成为主副本:当某个副本成为主副本时,kafka会尝试更新分区的HW值。
-
broker出现奔溃导致副本被踢出ISR集合:如果有broker节点奔溃则会看是否影响对应分区,然后会去检查分区的HW值是否需要更新。
-
生成端往主副本写入消息时:消息写入会增加其LEO值,此时会查看是否需要修改HW值。
-
主副本接受到从副本的fetch请求时:主副本在处理从副本的fetch请求时会尝试更新分区HW值。
前面是去尝试更新HW,但是不一定会更新,主副本上保存着从副本的LEO值与自身的LEO值,这里会比较所有满足条件的副本LEO值,并选择最小的LEO值最为分区的HW值,其中满足条件的副本是指满足以下两个条件之一:
-
副本在ISR集合中
-
副本的LEO落后于主副本的LEO时长不大于设定阈值(replica.lag.time.max.ms,默认为10s)
3.3 数据丢失场景
前面提到如果仅仅依赖HW来进行日志截断以及水位的判断会存在问题,如上图所示,假定存在两个副本A、副本B,最开始A为主副本,B为从副本,且参数min.insync.replicas=1
,即ISR只有一个副本时也会返回成功:
-
初始情况为主副本A已经写入了两条消息,对应HW=1,LEO=2,LEOB=1,从副本B写入了一条消息,对应HW=1,LEO=1。
-
此时从副本B向主副本A发起fetchOffset=1请求,主副本收到请求之后更新LEOB=1,表示副本B已经收到了消息0,然后尝试更新HW值,
min(LEO,LEOB)=1
,即不需要更新,然后将消息1以及当前分区HW=1返回给从副本B,从副本B收到响应之后写入日志并更新LEO=2,然后更新其HW=1,虽然已经写入了两条消息,但是HW值需要在下一轮的请求才会更新为2。 -
此时从副本B重启,重启之后会根据HW值进行日志截断,即消息1会被删除。
-
从副本B向主副本A发送fetchOffset=1请求,如果此时主副本A没有什么异常,则跟第二步骤一样没有什么问题,假设此时主副本也宕机了,那么从副本B会变成主副本。
-
当副本A恢复之后会变成从副本并根据HW值进行日志截断,即把消息1丢失,此时消息1就永久丢失了。
3.4 数据不一致场景
如图所示,假定存在两个副本A、副本B,最开始A为主副本,B为从副本,且参数min.insync.replicas=1
,即ISR只有一个副本时也会返回成功:
-
初始状态为主副本A已经写入了两条消息对应HW=1,LEO=2,LEOB=1,从副本B也同步了两条消息,对应HW=1,LEO=2。
-
此时从副本B向主副本发送fetchOffset=2请求,主副本A在收到请求后更新分区HW=2并将该值返回给从副本B,如果此时从副本B宕机则会导致HW值写入失败。
-
我们假设此时主副本A也宕机了,从副本B先恢复并成为主副本,此时会发生日志截断,只保留消息0,然后对外提供服务,假设外部写入了一个消息1(这个消息与之前的消息1不一样,用不同的颜色标识不同消息)。
-
等副本A起来之后会变成从副本,不会发生日志截断,因为HW=2,但是对应位移1的消息其实是不一致的
4 leader epoch机制
HW值被用于衡量副本备份成功与否以及出现失败情况时候的日志截断依据可能会导致数据丢失与数据不一致情况,因此在新版的Kafka(0.11.0.0)引入了leader epoch概念。
leader epoch表示一个键值对<epoch, offset>,其中epoch表示leader主副本的版本号,从0开始编码,当leader每变更一次就会+1,offset表示该epoch版本的主副本写入第一条消息的位置。
比如<0,0>表示第一个主副本从位移0开始写入消息,<1,100>表示第二个主副本版本号为1并从位移100开始写入消息,主副本会将该信息保存在缓存中并定期写入到checkpoint文件中,每次发生主副本切换都会去从缓存中查询该信息,下面简单介绍下leader epoch的工作原理:
-
每条消息会都包含一个4字节的leader epoch number值
-
每个log目录都会创建一个leader epoch sequence文件用来存放主副本版本号以及开始位移。
-
当一个副本成为主副本之后,会在leader epoch sequence文件末尾添加一条新的记录,然后每条新的消息就会变成新的leader epoch值。
-
当某个副本宕机重启之后,会进行以下操作:
-
从leader epoch sequence文件中恢复所有的leader epoch。
-
向分区主副本发送LeaderEpoch请求,请求包含了从副本的leader epoch sequence文件中的最新leader epoch值。
-
主副本返回从副本对应LeaderEpoch的lastOffset,返回的lastOffset分为两种情况,一种是返回比从副本请求中leader epoch版本大1的开始位移,另外一种是与请求中的leader epoch相等则直接返回当前主副本的LEO值。
-
如果从副本的leader epoch开始位移大于从leader中返回的lastOffset,那么会将从副本的leader epoch sequence值保持跟主副本一致。
-
从副本截断本地消息到主副本返回的LastOffset所在位移处。
-
从副本开始从主副本开始拉取数据。
-
在获取数据时,如果从副本发现消息中的leader epoch值比自身的最新leader epoch值大,则会将该leader epoch 值写到leader epoch sequence文件,然后继续同步文件。
-
下面看下leader epoch机制如何避免前面提到的两种异常场景
4.1 数据丢失场景解决
- 如图所示,当从副本B重启之后向主副本A发送
offsetsForLeaderEpochRequest
,epoch主从副本相等,则A返回当前的LEO=2,从副本B中没有任何大于2的位移,因此不需要截断。 -
当从副本B向主副本A发送fetchoffset=2请求时,A宕机,所以从副本B成为主副本,并更新epoch值为<epoch=1, offset=2>,HW值更新为2。
-
当A恢复之后成为从副本,并向B发送fetcheOffset=2请求,B返回HW=2,则从副本A更新HW=2。
-
主副本B接受外界的写请求,从副本A向主副本A不断发起数据同步请求。
从上可以看出引入leader epoch值之后避免了前面提到的数据丢失情况,但是这里需要注意的是如果在上面的第一步,从副本B起来之后向主副本A发送offsetsForLeaderEpochRequest
请求失败,即主副本A同时也宕机了,那么消息1就会丢失,具体可见下面数据不一致场景中有提到。
4.2 数据不一致场景解决
- 从副本B恢复之后向主副本A发送
offsetsForLeaderEpochRequest
请求,由于主副本也宕机了,因此副本B将变成主副本并将消息1截断,此时接受到新消息1的写入。 -
副本A恢复之后变成从副本并向主副本A发送
offsetsForLeaderEpochRequest
请求,请求的epoch值小于主副本B,因此主副本B会返回epoch=1时的开始位移,即lastoffset=1,因此从副本A会截断消息1。 -
从副本A从主副本B拉取消息,并更新epoch值<epoch=1, offset=1>。
可以看出epoch的引入避免的数据不一致,但是两个副本均宕机,则还是存在数据丢失的场景,前面的所有讨论都是建立在min.insync.replicas=1
的前提下,因此需要在数据的可靠性与速度方面做权衡。
5 Kafka Replica 源码分析
- 副本同步机制原理-Kafka 源码解析之副本同步机制实现
- ReplicaManager 详解-Kafka 源码解析之 ReplicaManager 详解