一、前言
在现在的分布式环境时代,任何一款中间件产品,大多都有一套机制去保障一致性的,Kafka 作为一个商业级消息中间件,音讯一致性的重要性可想而知,那 Kafka 如何保障一致性的呢?本文从高水位更新机制、正本同步机制以及 Leader Epoch 几个方面去介绍 Kafka 是如何保障一致性的。
二、HW 和 LEO
要想 Kafka 保障一致性,咱们必须先理解 HW(High Watermark)高水位和 LEO(Log End Offset)日志末端位移,看上面这张图你就清晰了:
高水位的作用:
- 定义音讯可见性,即用来标识分区下的哪些音讯是能够被消费者生产的。
- 帮忙 Kafka 实现正本同步
这里咱们不探讨 Kafka 事务,因为事务机制会影响消费者所能看到的音讯的范畴,它不只是简略依赖高水位来判断。它依附一个名为 LSO(Log Stable Offset)的位移值来判断事务型消费者的可见性。
日志末端位移的作用:
- 正本写入下一条音讯的位移值
- 数字 15 所在的方框是虚线,这就阐明,这个正本以后只有 15 条音讯,位移值是从 0 到 14,下一条新音讯的位移是 15。
- 介于高水位和 LEO 之间的音讯就属于未提交音讯。这也反馈出一个事实,那就是:同一个正本对象,其高水位值不会大于 LEO 值。
高水位和 LEO 是正本对象的两个重要属性。Kafka 所有正本都有对应的高水位和 LEO 值,而不仅仅是 Leader 正本。只不过 Leader 正本比拟非凡,Kafka 应用 Leader 正本的高水位来定义所在分区的高水位。换句话说,分区的高水位就是其 Leader 正本的高水位。
三、HW 和 LEO 的更新机制
当初,咱们晓得了每个正本对象都保留了一组高水位值和 LEO 值,但实际上,在 Leader 正本所在的 Broker 上,还保留了其余 Follower 正本的 LEO 值,请看下图:
从图中能够看出,Broker 0 上保留了某分区的 Leader 正本和所有 Follower 正本的 LEO 值,而 Broker 1 上仅仅保留了该分区的某个 Follower 正本。Kafka 把 Broker 0 上保留的这些 Follower 正本又称为近程正本(Remote Replica)。Kafka 正本机制在运行过程中,会更新 Broker 1 上 Follower 正本的高水位和 LEO 值,同时也会更新 Broker 0 上 Leader 正本的高水位和 LEO 以及所有近程正本的 LEO, 但它不会更新近程正本的高水位值,也就是我在图中标记为灰色的局部 。
这里你可能就困惑了?
- 为啥 Leader 正本所在的 Broker 上,还保留了其余 Follower 正本的 LEO 值?
- 为啥 Leader 正本所在的 Broker 上不会更新 Follower 正本 HW?
别着急,老周带你看下源码:
在 kafka.cluster.Partition#makeLeader
中:
Leader 正本所在的 Broker 上只有重置更新近程正本的 LEO,并没有近程正本的 HW。
这里你又可能会问了?
- 为什么要在 Broker 0 上保留这些近程正本呢?
- Broker 0 不会更新近程正本 HW,那近程正本的 HW 的更新机制又是怎么的呢?
Broker 0 上保留这些近程正本的次要作用是,帮忙 Leader 正本确定其高水位,也就是分区高水位。
第二个问题咱们间接来看下 HW 和 LEO 被更新的机会:
3.1 Leader 正本
解决生产者申请的逻辑如下:
- 写入音讯到本地磁盘
-
更新分区高水位值
- 获取 Leader 正本所在 Broker 端保留的所有近程正本 LEO 值(LEO-1,LEO-2,……,LEO-n)
- 获取 Leader 正本高水位值:currentHW
- 更新 currentHW = max{currentHW, min(LEO-1, LEO-2, ……,LEO-n)}
解决 Follower 正本拉取音讯的逻辑如下:
- 读取磁盘(或页缓存)中的音讯数据
- 应用 Follower 正本发送申请中的位移值更新近程正本 LEO 值
- 更新分区高水位值(具体步骤与解决生产者申请的步骤雷同)
3.2 Follower 正本
从 Leader 拉取音讯的解决逻辑如下:
- 写入音讯到本地磁盘
- 更新 LEO 值
-
更新高水位值
- 获取 Leader 发送的高水位值:currentHW
- 获取步骤 2 中更新过的 LEO 值:currentLEO
- 更新高水位为 min(currentHW, currentLEO)
四、正本同步机制
搞清楚了下面 HW 和 LEO 的更新机制后,咱们举一个单分区且有两个正本的主题来演示下 Kafka 正本同步的全流程。
当生产者发送一条音讯时,Leader 和 Follower 正本对应的 HW 和 LEO 是怎么被更新的呢?
首先是初始状态。上面这张图中的 remote LEO 就是方才的近程正本的 LEO 值。在初始状态时,所有值都是 0。
当生产者给主题分区发送一条音讯后,状态变更为:
此时,Leader 正本胜利将音讯写入了本地磁盘,故 LEO 值被更新为 1。
Follower 再次尝试从 Leader 拉取音讯。和之前不同的是,这次有音讯能够拉取了,因而状态进一步变更为:
这时,Follower 正本也胜利地更新 LEO 为 1。此时,Leader 和 Follower 正本的 LEO 都是 1,但各自的高水位仍然是 0,还没有被更新。它们须要在下一轮的拉取中被更新,如下图所示:
在新一轮的拉取申请中,因为位移值是 0 的音讯曾经拉取胜利,因而 Follower 正本这次申请拉取的是位移值 =1 的音讯。Leader 正本接管到此申请后,更新近程正本 LEO 为 1,而后更新 Leader 高水位为 1。做完这些之后,它会将以后已更新过的高水位值 1 发送给 Follower 正本。Follower 正本接管到当前,也将本人的高水位值更新成 1。至此,一次残缺的音讯同步周期就完结了。事实上,Kafka 就是利用这样的机制,实现了 Leader 和 Follower 正本之间的同步。
五、Leader Epoch 机制
下面的正本同步机制仿佛很完满,咱们无妨来思考下这种场景:
从方才的剖析中,咱们晓得,Follower 正本的高水位更新须要一轮额定的拉取申请能力实现。如果把下面那个例子扩大到多个 Follower 正本,状况可能更糟,兴许须要多轮拉取申请。也就是说,Leader 正本高水位更新和 Follower 正本高水位更新在工夫上是存在错配的。这种错配是很多“数据失落”或“数据不统一”问题的本源。基于此,社区在 0.11 版本正式引入了 Leader Epoch 概念,来躲避因高水位更新错配导致的各种不统一问题。
所谓 Leader Epoch,咱们大抵能够认为是 Leader 版本。它由两局部数据组成。
Epoch
。一个枯燥减少的版本号。每当正本领导权产生变更时,都会减少该版本号。小版本号的 Leader 被认为是过期 Leader,不能再行使 Leader 势力。起始位移(Start Offset)
。Leader 正本在该 Epoch 值上写入的首条音讯的位移。
我举个例子来阐明一下 Leader Epoch。假如当初有两个 Leader Epoch<0, 0> 和 <1, 120>,那么,第一个 Leader Epoch 示意版本号是 0,这个版本的 Leader 从位移 0 开始保留音讯,一共保留了 120 条音讯。之后,Leader 产生了变更,版本号减少到 1,新版本的起始位移是 120。
Kafka Broker 会在内存中为每个分区都缓存 Leader Epoch 数据,同时它还会定期地将这些信息长久化到一个 checkpoint 文件中。当 Leader 正本写入音讯到磁盘时,Broker 会尝试更新这部分缓存。如果该 Leader 是首次写入音讯,那么 Broker 会向缓存中减少一个 Leader Epoch 条目,否则就不做更新。这样,每次有 Leader 变更时,新的 Leader 正本会查问这部分缓存,取出对应的 Leader Epoch 的起始位移,以防止数据失落和不统一的状况。
源码在 org.apache.kafka.raft.LeaderState
中:
Kafka Broker 会在内存中为每个分区都缓存 Leader Epoch 数据:
同时它还会定期地将这些信息长久化到一个 checkpoint 文件中:
org.apache.kafka.common.message.LeaderAndIsrRequestData.LeaderAndIsrPartitionState#write
接下来,咱们来看一个理论的例子,它展现的是 Leader Epoch 是如何避免数据失落的。请先看下图:
开始时,正本 A 和正本 B 都处于失常状态,A 是 Leader 正本。某个应用了默认 acks 设置的生产者程序向 A 发送了两条音讯,A 全副写入胜利,此时 Kafka 会告诉生产者说两条音讯全副发送胜利。
当初咱们假如 Leader 和 Follower 都写入了这两条音讯,而且 Leader 正本的高水位也曾经更新了,但 Follower 正本高水位还未更新——这是可能呈现的。还记得吧,Follower 端高水位的更新与 Leader 端有工夫错配。假使此时正本 B 所在的 Broker 宕机,当它重启回来后,正本 B 会执行日志截断操作,将 LEO 值调整为之前的高水位值,也就是 1。这就是说,位移值为 1 的那条音讯被正本 B 从磁盘中删除,此时正本 B 的底层磁盘文件中只保留有 1 条音讯,即位移值为 0 的那条音讯。
当执行完截断操作后,正本 B 开始从 A 拉取音讯,执行失常的音讯同步。如果就在这个节骨眼上,正本 A 所在的 Broker 宕机了,那么 Kafka 就别无选择,只能让正本 B 成为新的 Leader,此时,当 A 回来后,须要执行雷同的日志截断操作,行将高水位调整为与 B 雷同的值,也就是 1。这样操作之后,位移值为 1 的那条音讯就从这两个正本中被永远地抹掉了。这就是这张图要展现的数据失落场景。
严格来说 ,这个场景产生的前提是 Broker 端参数 min.insync.replicas
设置为 1
。此时一旦音讯被写入到 Leader 正本的磁盘,就会被认为是“已提交状态”,但现有的工夫错配问题导致 Follower 端的高水位更新是有滞后的。如果在这个短暂的滞后工夫窗口内,接连产生 Broker 宕机,那么这类数据的失落就是不可避免的。
当初,咱们来看下如何利用 Leader Epoch 机制来躲避这种数据失落。请看下图:
场景和之前大抵是相似的,只不过援用 Leader Epoch 机制后,Follower 正本 B 重启回来后,须要向 A 发送一个非凡的申请去获取 Leader 的 LEO 值。在这个例子中,该值为 2。当获知到 Leader LEO=2 后,B 发现该 LEO 值不比它本人的 LEO 值小,而且缓存中也没有保留任何起始位移值 > 2 的 Epoch 条目,因而 B 无需执行任何日志截断操作。这是对高水位机制的一个显著改良,即正本是否执行日志截断不再依赖于高水位进行判断。
当初,正本 A 宕机了,B 成为 Leader。同样地,当 A 重启回来后,执行与 B 雷同的逻辑判断,发现也不必执行日志截断,至此位移值为 1 的那条音讯在两个正本中均失去保留。前面当生产者程序向 B 写入新音讯时,正本 B 所在的 Broker 缓存中,会生成新的 Leader Epoch 条目:[Epoch=1, Offset=2]。之后,正本 B 会应用这个条目帮忙判断后续是否执行日志截断操作。这样,通过 Leader Epoch 机制,Kafka 完满地躲避了这种数据失落场景。