主要从控制器、consumer主题管理、kafka的副本机制三个方面对broker进行深入研究,梳理清楚broker端的一些基础流程。
注意:文章中的部分内容节选自极客时间《kafka核心技术与实战》。
控制器的作用?
在 Kafka 集群中会有一个或多个 broker,其中有一个 broker 会被选举为控制器(KafkaController)。
1.管理整个集群中所有分区和副本的状态。比如:某分区leader副本出现故障,控制器为该分区选举leader副本。ISR发生变化,控制器通知所有broker更新元数据。
2.控制器负责分区的重新分配。
控制器的选举?
因为一个kafka集群中会有多个broker,那么是如何选举一个broker为控制器呢?
其实这个还是比较简单,第一个在zk上写入/controller这个节点的broker,即是控制器, /controller是一个临时节点。
控制器的具体职责以及实现
控制器这里会监听以下几个重要zk节点的变化:
初期kafka没有broker controller概念,每一个broker都有监听,这样容易造成zk的羊群效应,什么是羊群效应,见:https://www.cnblogs.com/bnbqian/p/4846308.html。
这样的话,其他的broker极少需要再监听ZooKeeper中的数据变化,这样省去了很多不必要的麻烦,同时避免了羊群效应。不过每个broker还是会对/controller节点添加监听器,以此来监听此节点的数据变化(ControllerChangeHandler),这个监听是不可避免的,添加这个监听的原因是broker挂掉后,其他broker得到通知,马上可以选举出一个新的broker controller。
控制器重新选举?
假如控制器运行过程中突然宕机,那么此节点就会在zk上消失,其他broker收到watch通知,其他broker会去竞争,第一个写入controller节点成功的broker,就是控制器。
分区leader重选举?
场景:
假设分区leader位于控制器,控制器宕机后,势必要重新选举一个控制器,那么对应的分区leader是如何重新选举呢?
与此对应的选举策略(ControlledShutdownPartitionLeaderElectionStrategy)为:从AR列表中找到第一个存活的副本,且这个副本在目前的ISR列表中,与此同时还要确保这个副本不处于正在被关闭的节点上。
场景:新的分区增加或者原分区leader下线,需要选举一个leader。
对应的选举策略为OfflinePartitionLeaderElectionStrategy。这种策略的基本思路是:
按照 AR 集合中副本的顺序查找第一个存活的副本,并且这个副本在ISR集合中。如果ISR集合中没有可用的副本,那么此时还要再检查一下所配置的unclean.leader.election.enable参数(默认值为false)。如果这个参数配置为true,那么表示允许从非ISR列表中的选举leader,从AR列表中找到第一个存活的副本即为leader。
__consumer_offsets 在 Kafka 源码中有个更为正式的名字,叫位移主题。将 Consumer 的位移数据作为一条条普通的 Kafka 消息,提交到 __consumer_offsets 中。可以这么说,__consumer_offsets 的主要作用是保存 Kafka 消费者的位移信息。
位移主题的 Key 中应该保存 3 部分内容:
kafka如何清除位移主题中的过期消息?
Kafka 使用 Compact 策略来删除位移主题中的过期消息,避免该主题无限期膨胀。那么应该如何定义 Compact 策略中的过期呢?对于同一个 Key 的两条消息 M1 和 M2,如果 M1 的发送时间早于 M2,那么 M1 就是过期消息。Compact 的过程就是扫描日志的所有消息,剔除那些过期的消息,然后把剩下的消息整理在一起。
图中位移为 0、2 和 3 的消息的 Key 都是 K1。Compact 之后,分区只需要保存位移为 3 的消息,因为它是最新发送的。Kafka 提供了专门的后台线程定期地巡检待 Compact 的主题,看看是否存在满足条件的可删除数据。这个后台线程叫 Log Cleaner。很多实际生产环境中都出现过位移主题无限膨胀占用过多磁盘空间的问题,如果你的环境中也有这个问题,去检查一下 Log Cleaner 线程的状态,通常都是这个线程挂掉了导致的。
副本基础知识回顾:
1.一个分区中包含一个或多个副本,其中一个为leader副本,其余为follower副本,各个副本位于不同的broker节点中。只有leader副本对外提供服务,follower副本只负责数据同步。
2.分区中的所有副本统称为 AR,而ISR 是指与leader 副本保持同步状态的副本集合,当然leader副本本身也是这个集合中的一员。
3.LEO标识每个分区中最后一条消息的下一个位置,分区的每个副本都有自己的LEO,ISR中最小的LEO即为HW,俗称高水位,消费者只能拉取到HW之前的消息。
从生产者发出的一条消息首先会被写入分区的leader副本,不过还需要等待ISR集合中的所有follower 副本都同步完之后才能被认为已经提交,之后才会更新分区的 HW,进而消费者可以消费到这条消息。这种场景是指acks = all的这种配置。
副本失效:
Kafka从0.9.x版本开始就通过唯一的broker端参数replica.lag.time.max.ms来抉择,当ISR集合中的一个follower副本滞后leader副本的时间超过此参数指定的值时则判定为同步失败,需要将此follower副本剔除出ISR集合,即进入OSR。
replica.lag.time.max.ms参数的默认值为1000,,当follower副本将leader副本LEO(LogEndOffset)之前的日志全部同步时,则认为该 follower 副本已经追赶上 leader 副本。
副本失效可能的情况:
follower副本进程卡住,在一段时间内根本没有向leader副本发起同步请求,比如频繁的FullGC。
follower副本进程同步过慢,在一段时间内都无法追赶上leader副本,比如I/O开销过大。
高水位与LEO:
LEO: 还有一个日志末端位移的概念,即 Log End Offset,简写是 LEO。它表示副本写入下一条消息的位移值
HW: 即已提交的消息+1 。
位移值等于高水位的消息也属于未提交消息。也就是说,高水位上的消息是不能被消费者消费的。
还有一个日志末端位移的概念,即 Log End Offset,简写是 LEO。它表示副本写入下一条消息的位移值。注意,数字 15 所在的方框是虚线,这就说明,这个副本当前只有 15 条消息,位移值是从 0 到 14,下一条新消息的位移是 15。显然,介于高水位和 LEO 之间的消息就属于未提交消息。这也从侧面告诉了我们一个重要的事实,那就是:同一个副本对象,其高水位值不会大于 LEO 值。
Kafka 使用 Leader 副本的高水位来定义所在分区的高水位。换句话说,分区的高水位就是其 Leader 副本的高水位。
远程副本与本地副本:
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 副本:
处理生产者请求的逻辑如下:
1.写入消息到本地磁盘。
2.更新分区高水位值。
i. 获取 Leader 副本所在 Broker 端保存的所有远程副本 LEO 值(LEO-1,LEO-2,……,LEO-n)。
ii. 获取 Leader 副本高水位值:currentHW。iii. 更新 currentHW = max{currentHW, min(LEO-1, LEO-2, ……,LEO-n)}。
处理 Follower 副本拉取消息的逻辑如下:
1.读取磁盘(或页缓存)中的消息数据。
2.使用 Follower 副本发送请求中的位移值更新远程副本 LEO 值。
3.更新分区高水位值(具体步骤与处理生产者请求的步骤相同)。
Follower 副本:
1.写入消息到本地磁盘。
2.更新 LEO 值。
3.更新高水位值。
i. 获取 Leader 发送的高水位值:currentHW。
ii. 获取步骤 2 中更新过的 LEO 值:currentLEO。
iii. 更新高水位为 min(currentHW, currentLEO)。
副本同步机制图解:
1.初始值默认都是0,leader副本和follower副本的hw和leo都为0。
2.当生产者给主题分区发送一条消息后,状态变更为:
此时,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。至此,一次完整的消息同步周期就结束了
Leader Epoch
什么是leader epoch ?
Follower 副本的高水位更新需要一轮额外的拉取请求才能实现。如果把上面那个例子扩展到多个 Follower 副本,情况可能更糟,也许需要多轮拉取请求。也就是说,Leader 副本高水位更新和 Follower 副本高水位更新在时间上是存在错配的。这种错配是很多“数据丢失”或“数据不一致”问题的根源。社区在 0.11 版本正式引入了 Leader Epoch 概念,来规避因高水位更新错配导致的各种不一致问题。
所谓 Leader Epoch,大致可以认为是 Leader 版本。它由两部分数据组成。
1.Epoch。一个单调增加的版本号。每当副本领导权发生变更时,都会增加该版本号。小版本号的 Leader 被认为是过期 Leader,不能再行使 Leader 权力。
2.起始位移(Start Offset)。Leader 副本在该 Epoch 值上写入的首条消息的位移。
举个例子:
两个 Leader Epoch<0, 0> 和 <1, 120>,那么,第一个 Leader Epoch 表示版本号是 0,这个版本的 Leader 从位移 0 开始保存消息,一共保存了 120 条消息。之后,Leader 发生了变更,版本号增加到 1,新版本的起始位移是 120。
leader epoch流程:
Kafka Broker 会在内存中为每个分区都缓存 Leader Epoch 数据,同时它还会定期地将这些信息持久化到一个 checkpoint 文件中。当 Leader 副本写入消息到磁盘时,Broker 会尝试更新这部分缓存。如果该 Leader 是首次写入消息,那么 Broker 会向缓存中增加一个 Leader Epoch 条目,否则就不做更新。这样,每次有 Leader 变更时,新的 Leader 副本会查询这部分缓存,取出对应的 Leader Epoch 的起始位移,以避免数据丢失和不一致的情况。
未使用leader epoch造成数据丢失的情况:
1.副本A,副本B,如上:副本A是Leader副本,高水位是2,副本B是follower副本,高水位是1。这个时候follower的副本高水位还未更新,因为是有时间延迟的,需要再次发起一个请求才会更新。
2.副本B重启之后,副本 B 会执行日志截断操作,将 LEO 值调整为之前的高水位值,也就是 1。这就是说,位移值为 1 的那条消息被副本 B 从磁盘中删除,此时副本 B 的底层磁盘文件中只保存有 1 条消息,即位移值为 0 的那条消息。
3.当执行完截断操作后,副本 B 开始从 A 拉取消息,执行正常的消息同步。如果就在这个节骨眼上,副本 A 所在的 Broker 宕机了,那么 Kafka 就别无选择,只能让副本 B 成为新的 Leader,此时,当 A 回来后,需要执行相同的日志截断操作,即将高水位调整为与 B 相同的值,也就是 1。
4.这样操作之后,位移值为 1 的那条消息就从这两个副本中被永远地抹掉了。这就是这张图要展示的数据丢失场景。
1. Follower 副本 B 重启回来后,需要向 A 发送一个特殊的请求去获取 Leader 的 LEO 值。在这个例子中,该值为 2。当获知到 Leader LEO=2 后,B 发现该 LEO 值不比它自己的 LEO 值小,而且缓存中也没有保存任何起始位移值 > 2 的 Epoch 条目,因此 B 无需执行任何日志截断操作。
2.副本 A 宕机了,B 成为 Leader。同样地,当 A 重启回来后,执行与 B 相同的逻辑判断,发现也不用执行日志截断。