参考文章:
1.《深入理解Kafka- 核心设计与实践原理》朱忠华
2. Kafka设计解析(二十一)Kafka水位(high watermark)与leader epoch的讨论
https://www.cnblogs.com/warehouse/p/9545429.html
本文针对的是 Kafka 0.11 + 之后的 Kafka ,由于之前的 Kafka 可能出现丢数,数据不一致的问题,建议升级 Kafka 版本。
最近由于之前学习了 Zookeeper 的 ZAB , Raft 协议, Paxos 原型协议,最近对Kafka 的副本策略 也做了一个研究。Kafka 的副本策略 以及 同步方案 与 Raft , Zab 有者相似之处。
我们来看下 Kafka 的 副本相关的概念:
AR ( Assigned Replicas) : 分区中所有的副本都称为 AR
ISR (In-Sync-Replicas) : ISR 是 AR 集合中的一个子集。 ISR 描述的是 各个分区 的 与 主 Leader 保持同步的 Follower.
注意: 我们这里要知道什么是保持同步的Follower.
在Kafka 较为低的版本(Kafka 0.9.*)有两个参数影响 ISR 的判断:
replica.lag.max.messages 默认为 4000 (废弃)
参数解释:描述了最多容忍 Follower 落后于 Leader 多少消息
废弃原因:由于4000 是一个经验值,对于大的流量,反而会显得比较小。而对于小的流量,QPS 为 50 record/ s ,这个参数值又显得不够合理,故该值被废弃掉;
replica.lag.time.max.ms 默认为 10000 单位 ms
参数解释: 最多容忍Follower 落后 Leader 多长时间,判断依据 : 当 follower 副本 LEO (LogEndOffset) 之前的日志全部同步时 (副本的LEO 与 Leader LEO 相同),则认为该 follower 副本已经追赶上 leader 副本,此时更新该副本的lastCaughtUpTimeMS 标识。
OSR 指的是 (Out-of-sync Replicas), 与Leader 滞后过多的副本,如何判断滞后,请参考之前的ISR
我们知道了 AR, ISR , OSR 的概念,那么3者之间有什么关系呢?
AR = ISR + OSR
HW 是 High Watermark 的缩写,俗称高水位,水印,它标识了一个特定的消息偏移量(Offset ),消费者只能拉取到这个 Offset 之前的消息。
注意 : HW 标识的是已经确认的消息的下一条消息。
LEO 是 Log End Offset 的缩写, 它标识了当前日志文件中 下一条待写入消息的 Offset .
注意 :LEO 标识的是下一条待写入消息的 Offset
注意这里我们了解下副本失效的两种情况:
1.Follower 副本进程卡住,在一段时间内没有向Leader 副本发起同步请求,比如 频繁 Full GC.
2.Follower 副本进程同步过慢,在一段时间内都无法追赶上 leader 副本,比如 I/O 开销过大。
通过kafka 自带的工具:kafka-topics.sh , 我们可以很方便知道当前的 AR , ISR , OSR ,Leader , Follower 这些信息。
下面给出一个示例,演示如何查看信息:
假设查找的 topic : TD_ADAPTER_TRAFFIC
命令:
[cloudera-scm@dmp-job001 ~]$ kafka-topics --topic TD_ADAPTER_TRAFFIC --describe --zookeeper dmp-big006.b1.bj.jd:2181,dmp-big006.b1.bj.jd:2181,dmp-big006.b1.bj.jd:2181/kafka
打印的信息:
Topic:TD_ADAPTER_TRAFFIC PartitionCount:6 ReplicationFactor:3 Configs:
Topic: TD_ADAPTER_TRAFFIC Partition: 0 Leader: 75 Replicas: 75,73,74 Isr: 74,75,73
Topic: TD_ADAPTER_TRAFFIC Partition: 1 Leader: 73 Replicas: 73,74,75 Isr: 75,74,73
Topic: TD_ADAPTER_TRAFFIC Partition: 2 Leader: 74 Replicas: 74,75,73 Isr: 75,74,73
Topic: TD_ADAPTER_TRAFFIC Partition: 3 Leader: 75 Replicas: 75,73,74 Isr: 75,74,73
Topic: TD_ADAPTER_TRAFFIC Partition: 4 Leader: 73 Replicas: 73,74,75 Isr: 75,74,73
Topic: TD_ADAPTER_TRAFFIC Partition: 5 Leader: 74 Replicas: 74,75,73 Isr: 75,74,73
首先可以看到配置的副本数是 3
对分区0 来说 ,AR 集合是 {75,73,74 } ISR 集合是 {74,75,73},
那么根据 AR = ISR + OSR 可以知道对于 Partition : 0 来说 ,没有 OSR ,
我们也可以直接查看是否有 OSR, 命令如下:
kafka-topics --topic TD_ADAPTER_TRAFFIC --describe --under-replicated-partitions --zookeeper dmp-big006.b1.bj.jd:2181,dmp-big006.b1.bj.jd:2181,dmp-big006.b1.bj.jd:2181/kafka
注意,我们增加了一个参数 --under-replicated-partitions
参数含义: --under-replicated-partitions if set when describing topics, only show under replicated partitions
可知,我们查找的是在做同步的 AR , 也就是 OSR
正常情况下,是没有输出的:
[cloudera-scm@dmp-job001 ~]$ kafka-topics --topic TD_ADAPTER_TRAFFIC --describe --under-replicated-partitions --zookeeper dmp-big006.b1.bj.jd:2181,dmp-big006.b1.bj.jd:2181,dmp-big006.b1.bj.jd:2181/kafka
SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/opt/cloudera/parcels/KAFKA-3.0.0-1.3.0.0.p0.40/lib/kafka/libs/slf4j-log4j12-1.7.25.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/opt/cloudera/parcels/KAFKA-3.0.0-1.3.0.0.p0.40/lib/kafka/libs/slf4j-log4j12-1.7.5.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [org.slf4j.impl.Log4jLoggerFactory]
我们查看下一个线上的Topic 的信息
[cloudera-scm@dmp-job001 ~]$ kafka-topics --topic TD_ADAPTER_TRAFFIC --describe --zookeeper dmp-big006.b1.bj.jd:2181,dmp-big006.b1.bj.jd:2181,dmp-big006.b1.bj.jd:2181/kafka
SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/opt/cloudera/parcels/KAFKA-3.0.0-1.3.0.0.p0.40/lib/kafka/libs/slf4j-log4j12-1.7.25.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/opt/cloudera/parcels/KAFKA-3.0.0-1.3.0.0.p0.40/lib/kafka/libs/slf4j-log4j12-1.7.5.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [org.slf4j.impl.Log4jLoggerFactory]
Topic:TD_ADAPTER_TRAFFIC PartitionCount:6 ReplicationFactor:3 Configs:
Topic: TD_ADAPTER_TRAFFIC Partition: 0 Leader: 75 Replicas: 75,73,74 Isr: 74,75,73
Topic: TD_ADAPTER_TRAFFIC Partition: 1 Leader: 73 Replicas: 73,74,75 Isr: 75,74,73
Topic: TD_ADAPTER_TRAFFIC Partition: 2 Leader: 74 Replicas: 74,75,73 Isr: 75,74,73
Topic: TD_ADAPTER_TRAFFIC Partition: 3 Leader: 75 Replicas: 75,73,74 Isr: 75,74,73
Topic: TD_ADAPTER_TRAFFIC Partition: 4 Leader: 73 Replicas: 73,74,75 Isr: 75,74,73
Topic: TD_ADAPTER_TRAFFIC Partition: 5 Leader: 74 Replicas: 74,75,73 Isr: 75,74,73
我们可以看到各个分区的Leader, 下面我们讲解下各个分区的Leader 是如何选举产生的。
我们讲解下,对于分区 0
AR 为 【75,73,74】, ISR 为 【 74,75,73】
Leader选举分为以下几种情况:
情况一 : 当创建分区或者分区上线的时候都下雨要执行Leader的选举动作,对应的选举策略为 OfflinePartitionLeaderElectionStrategy.
选举的思路 :
按照AR集合中副本的顺序查找第一个存活的副本,并且这个副本在ISR集合中。
注意:这是是根据AR的顺序而不是ISR的顺序进行选举的。
除了ISR外,也可以从OSR 中选举Leader (unclean.leader.election.enable), 不推荐,会造成数据丢失
如果ISR没有可用的副本,那么此时还要再检查一下 unclean.leader.election.enable 参数,默认为 false. 如果这个参数配置为 true, 那么表示允许从非 ISR 列表中,选举Leader. 从AR 列表中找到的第一个存活的副本即为Leader.
情况二:当分区进行重分配,也需要执行Leader 的选举动作,选举策略为 ReassignPartitionLeaderElectionStrategy.
选举思路:从重分配的AR 列表中找到第一个存活的副本,并且这个副本在目前的ISR 列表中
情况三: 当发生优先副本选举时,PreferredReplicaPartitionLeaderElectionStrategy
直接将优先副本设置为Leader, AR几何中的第一个副本即为优先副本
情况四:当某节点被优雅的关闭时 (执行 ControlledShutdown) ,该节点的副本都会下线,于此对应的分区需要执行 Leader 选举。选举策略 ControlledShutdownPartitionLeaderElectionStrategy
AR列表中找到第一个存活的副本,并且这个副本在目前的 ISR 列表中,还要确保这个副本不处于正在被关闭的节点上。
Kafka 中,生产者 写入消息,消费者读取消息操作 都是与Leader 副本进行交互的,实现的是 一种 主读主写 的 生产消费模型。
这里我们对为什么采用 主读主写 的模型做一个解释 :
首先我们说下主写从读的一些缺陷:
1,数据一致性问题
数据从主节点转到从节点必然会有一个延时的时间窗口,这个时间窗口会导致主从节点的数据不一致性。即需要一个时间窗口后,主从节点才能保证数据是一致的。
2.延时问题
对于Kafka 而言,主从同步比较耗时,需要经历 网络-》主节点内存-》主节点磁盘 -》 网络 -》 从节点内存 -》从节点磁盘这几个阶段。
Kafka 主写主读 的优点:
1.可以简化代码的实现逻辑,减少出错的可能
2.将负载粒度细化均摊,与主写从读相比,不仅负载效能更好,而且对用户可控
3.没有延时的影响
4.在副本稳定的情况下,不会出现数据不一致的情况
下面我们对 副本中 HW, LEO, epoch 的概念进行细致讲解。
每个Kafka副本对象都有两个重要的属性 HW / LEO ,是每个Kafka 版本都有的概念。
LEO 标识的是每个分区中最后一条消息的下一个位置,分区的每个副本都有自己的LEO
ISR 中最小的LEO即为HW (注意 HW 实际指向的是确认消息的下一个位置), 俗称 高水位,消费者只能拉取到 HW 之前的 消息
图示如下:
我们先了解下 LEO .HW 什么时候会被更新:
Kafka有两套follower副本LEO(明白这个是搞懂后面内容的关键,因此请多花一点时间来思考):
1. 一套LEO保存在follower副本所在broker的副本管理机中;
2. 另一套LEO保存在leader副本所在broker的副本管理机中——换句话说,leader副本机器上保存了所有的follower副本的LEO。
一、leader副本何时更新LEO?
leader写log时就会自动地更新它自己的LEO值。
二. follower副本端的follower副本LEO何时更新?
follower副本端的LEO值就是其底层日志的LEO值,也就是说每当新写入一条消息,其LEO值就会被更新(类似于LEO += 1)。当follower发送FETCH请求后,leader将数据返回给follower,此时follower开始向底层log写数据,从而自动地更新LEO值
三. leader副本端的follower副本LEO何时更新?
leader副本端的follower副本LEO的更新发生在leader在处理follower FETCH请求时。一旦leader接收到follower发送的FETCH请求,它首先会从自己的log中读取相应的数据,但是在给follower返回数据之前,它先去更新follower的LEO(即上面所说的第二套LEO)
一、leader副本何时更新HW值?
前面说过了,leader的HW值就是分区HW值,因此何时更新这个值是我们最关心的,因为它直接影响了分区数据对于consumer的可见性 。以下4种情况下leader会尝试去更新分区HW——切记是尝试,有可能因为不满足条件而不做任何更新:
二、follower副本何时更新HW?
follower更新HW发生在其更新LEO之后,一旦follower向log写完数据,它会尝试更新它自己的HW值。具体算法就是比较当前LEO值与FETCH响应中leader的HW值,取两者的小者作为新的HW值。这告诉我们一个事实:如果follower的LEO值超过了leader的HW值,那么follower HW值是不会越过leader HW值的。
下面我们看下 0.11 之前版本,只有 HW , LEO 概念的 Kafka 的主从节点更新 LEO, HW 的流程
Step1 " 生产者向Leader副本中写入消息. 某一时刻,leader 副本的 LEO 增长至 5, 副本的 HW 还为0.
初始化情况
Step2' 之后 follower 副本 (不带阴影的方框)向 Leader 副本拉取消息,在拉取的请求中会带有 自身的LEO信息,这个 LEO 信息对应的是 FetchRequest 请求中的 fetch_offset.
Leader 副本返回给 follower 副本相应的信息,并且还带有自身的HW信息, 这个HW 信息对应的是 FetchResponse 中的 high_watermark.
此时两个 follower 副本 各自拉去到了消息,并且更新各自的LEO 为3和4. 与此同时,follower 副本还会更新自己的 HW, 更新 HW 的算法是比较当前 LEO 和 Leader 传过来的 HW 的值,取最小值作为自己的HW 值。
当前两个follower 副本的HW 都等于0 (min(0,0)=0)
Step3" 接下来 follower 副本再次请求拉去 leader 副本中的消息。
此时Leader 副本收到来自 follower 副本的 FetchRequest 请求,其中带有 LEO 的相关信息,选取其中的最小值作为新的 HW , 即 min(15, 3, 4) =3 。然后 连同消息 和 HW 一起返回 FetchResponse 给 Follower 副本。注意 Leader 副本 的HW 是一个很重要的东西,因为它直接影响了分区数据对消费者的可见性。
Step4 两个Follower 副本在收到新的消息之后。更新LEO , 并且更新子的HW 为3 , min(LEO, 3) = 3
注意: 这就是正常情况下的 LEO 与 HW 更新流程
除此之外,Leader 副本所在的节点会记录所有副本的LEO, follower 副本所在的节点只会记录自身的 LEO, 而不会记录其他副本的LEO。 对HW 而言,各个副本所在的节点 都只记录 它自身的HW.
通过以上学习,我们可以关注到:
以上所有的东西其实就想说明一件事情:Kafka使用HW值来决定副本备份的进度,而HW值的更新通常需要额外一轮FETCH RPC才能完成,故而这种设计是有问题的。它们可能引起的问题包括:
一、数据丢失
如前所述,使用HW值来确定备份进度时其值的更新是在下一轮RPC中完成的。现在翻到上面使用两种不同颜色标记的步骤处思考下, 如果follower副本在蓝色标记的第一步与紫色标记的第二步之间发生崩溃,那么就有可能造成数据的丢失。我们举个例子来看下。
上图中有两个副本:A和B。开始状态是A是leader。我们假设producer端min.insync.replicas设置为1,那么当producer发送两条消息给A后,A写入到底层log,此时Kafka会通知producer说这两条消息写入成功。
但是在broker端,leader和follower底层的log虽都写入了2条消息且分区HW已经被更新到2,但follower HW尚未被更新(也就是上面紫色颜色标记的第二步尚未执行)。倘若此时副本B所在的broker宕机,那么重启回来后B会自动把LEO调整到之前的HW值,故副本B会做日志截断(log truncation),将offset = 1的那条消息从log中删除,并调整LEO = 1,此时follower副本底层log中就只有一条消息,即offset = 0的消息。
B重启之后需要给A发FETCH请求,但若A所在broker机器在此时宕机,那么Kafka会令B成为新的leader,而当A重启回来后也会执行日志截断,将HW调整回1。这样,位移=1的消息就从两个副本的log中被删除,即永远地丢失了。
这个场景丢失数据的前提是在min.insync.replicas=1时,一旦消息被写入leader端log即被认为是“已提交”,而延迟一轮FETCH RPC更新HW值的设计使得follower HW值是异步延迟更新的,倘若在这个过程中leader发生变更,那么成为新leader的follower的HW值就有可能是过期的,使得clients端认为是成功提交的消息被删除。
二、leader/follower数据离散
除了可能造成的数据丢失以外,这种设计还有一个潜在的问题,即造成leader端log和follower端log的数据不一致。比如leader端保存的记录序列是r1,r2,r3,r4,r5,....;而follower端保存的序列可能是r1,r3,r4,r5,r6...。这也是非法的场景,因为顾名思义,follower必须追随leader,完整地备份leader端的数据。
我们依然使用一张图来说明这种场景是如何发生的:
这种情况的初始状态与情况1有一些不同的:A依然是leader,A的log写入了2条消息,但B的log只写入了1条消息。分区HW更新到2,但B的HW还是1,同时producer端的min.insync.replicas = 1。
这次我们让A和B所在机器同时挂掉,然后假设B先重启回来,因此成为leader,分区HW = 1。假设此时producer发送了第3条消息(绿色框表示)给B,于是B的log中offset = 1的消息变成了绿色框表示的消息,同时分区HW更新到2(A还没有回来,就B一个副本,故可以直接更新HW而不用理会A)之后A重启回来,需要执行日志截断,但发现此时分区HW=2而A之前的HW值也是2,故不做任何调整。此后A和B将以这种状态继续正常工作。
显然,这种场景下,A和B底层log中保存在offset = 1的消息是不同的记录,从而引发不一致的情形出现。
Kafka 0.11.0.0.版本解决方案
造成上述两个问题的根本原因在于HW值被用于衡量副本备份的成功与否以及在出现failture时作为日志截断的依据,但HW值的更新是异步延迟的,特别是需要额外的FETCH请求处理流程才能更新,故这中间发生的任何崩溃都可能导致HW值的过期。鉴于这些原因,Kafka 0.11引入了leader epoch来增强 HW值。Leader端多开辟一段内存区域专门保存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版本的位移,这就不会发生数据不一致和丢失的情况。
下面我们依然使用图的方式来说明下利用leader epoch如何规避上述两种情况
一、规避数据丢失
上图左半边已经给出了简要的流程描述,这里不详细展开具体的leader epoch实现细节(比如OffsetsForLeaderEpochRequest的实现),我们只需要知道每个副本都引入了新的状态来保存自己当leader时开始写入的第一条消息的offset以及leader版本。这样在恢复的时候完全使用这些信息而非水位来判断是否需要截断日志。
二、规避数据不一致
同样的道理,依靠leader epoch的信息可以有效地规避数据不一致的问题。
总结
0.11.0.0版本的Kafka通过引入leader epoch解决了原先依赖水位表示副本进度可能造成的数据丢失/数据不一致问题。有兴趣的读者可以阅读源代码进一步地了解其中的工作原理。
源代码位置:kafka.server.epoch.LeaderEpochCache.scala (leader epoch数据结构)、kafka.server.checkpoints.LeaderEpochCheckpointFile(checkpoint检查点文件操作类)还有分布在Log中的CRUD操作。