- 高水位介绍
你可能听说过高水位但不一定耳闻过Leader Epoch。前者是Kafka中重要概念,而后者是社区0.11版本新推出的,主要是为了弥补高水位机制的一些缺陷。
首先来看一下基本定义,什么是高水位?或者说什么是水位?水位一词多用于流式处理领域,比如Spark Streaming或Flink框架中都有水位的概念。教科书中关于水位的的经典定义是:在时刻T,任意创建时间为T',且T' <= T 的所有事件都已经到达或被观测到,那么T就被定义为水位。另外的还有这样表述水位的:水位是一个单调增加且表征最早未完成工作的时间戳。借别人一张图来说明一下:
图中Completed的蓝色部分代表已完成工作,In-Flight的红色部分代表正在进行的工作,两者的边界是水位线。Kafka的水位不是时间戳,更与时间无关,他和位置信息绑定,即是用消息位移表征。Kafka源码中使用的是高水位,下面统一使用高水位或它的缩写HW,Kafka中海油低水位( Low Watermark ),它是与Kafka删除消息相关联的概念。
高水位的作用:
定义消息可见性,即用来标识分区下的哪些消息是可以被消费者消费的;
帮助Kafka完成副本同步。
下面这张图展示了多个与高水位相关的Kafka术语,我来详细解释一下并澄清一些常见的误区:
假设这是某个分区Leader副本的高水位图,在高水位以下的消息,即位移小于8的所有消息,被认为是已提交消息,其他的是未提交消息,位移值等于高水位的消息也属于未提交消息。注意,这里不讨论Kafka事务,因为事务机制会影响消费者所能看到的消息的范围,他不只是简单依赖高水位来判断,也依靠LSO ( Log Stable Offset ) 的位移值来判断事务性消费者的可见性。
图中还有一个日志末端位移的概念,即Log End Offset,简称LEO,它表示副本写入下一条消息的位移值,这个副本当前只有15条消息,位移值0到14,下一条新消息位移值是15。从图中看来,也从一个侧面告诉我们,高水位值不会大于LEO值。高水位和LEO是副本对象两个重要属性,Kafka所有副本都有对应高水位和LEO值,而不仅仅是Leader副本。只不过Leader副本比较特殊,Kafka使用Leader副本的高水位来定义所在分区的高水位,即分区高水位也就是Leader副本的高水位。
- 高水位更新机制
现在,我们知道了每个副本对象都保存了一组高水位值和LEO值,实际上,在Leader副本所在Broker上,还保存了Follower副本的LEO值。
图中,我们可以看到,Broker 0上保存了某分区的Leader副本和所有Follower副本的LEO值,而Broker1上仅仅保存该分区的某个Follower副本。Kafka把Broker0上保存的这些Follower副本又称为远程副本。Kafka副本机制在运行过程中,会更新Broker 1上Follower副本高水位和LEO值,同时也会更新Broker 0是上Leader副本的高水位和LEO以及所有远程副本的LEO值,但不会更新远程副本的高水位值。为什么要在Broker 0上保存这些远程副本呢?主要作用是,帮助Leader副本确定高水位,也就是分区高水位。下表展示了这些值被更新的时机,只有搞清楚了更新机制,才能开始讨论Kafka副本机制原理,以及它是如何使用高水位来执行副本消息同步的。
这里稍微解释一下,什么叫与Leader副本保持同步,判断条件有两个:
该远程Follower副本在ISR中;
该远程Follower副本LEO 值落后于Leader副本LEO值的时间,不超过Broker端参数replica.lag.time.max.ms的值,默认值是10秒。
咋一看,这两条件像一回事,因为目前某个副本能否进入ISR就是靠第2个条件判断。但有些会发生这样的情况:即Follower副本已经追上Leader进度,却不在ISR中,比如刚刚重启回来的副本。如果Kafka只判断第1个条件,就可能出现某些副本具备进入ISR资格,但却尚未进入ISR中的情况。此时,分区高水位就可能超过ISR中副本LEO,而高水位>LEO的情形是不被允许的。下面,再从Leader副本和Follower副本两个维度,总结一下高水位和LEO的更新机制。
Leader副本中,处理生产者请求的逻辑如下:
写入消息到本地磁盘;
更新分区高水位值:获取Leader副本所在Broker端保存的所有远程副本LEO值{LEO-1,LEO-2, ..., LEO-n};获取Leader副本高水位值,currentHW;更新currentHW=max(currentHW, LEO-1, LEO-2, ..., LEO-n)。
处理Follower副本拉取消息的逻辑如下:
读取磁盘(或页缓存)中的消息数据;使用Follower副本发送请求中的位移值更新远程副本LEO值;更新分区高水位值(同上)。
Follower副本中,从Leader拉取消息的处理逻辑如下:
写入消息到本地磁盘;更新LEO值;更新高水位值:获取Leader发送的高水位值,currentHW;获取currentLEO;更新高水位为min(currentHW, currentLEO)。
- 副本同步机制解析
举几个实际的例子,说明Kafka副本同步的全流程。该例子使用一个单分区且有两个副本的主题,当生产发送一条消息时,Leader和Follower副本对应高水位是怎么被更新的呢?首先是初始状态。remote LEO就是远程副本的LEO值。在初始状态下,所有值都是0,当生产者给主题分区发送一条消息后,Leader副本将消息写入本地磁盘,故LEO值被更新为1 。
当Follower副本再次尝试从Leader拉取消息,这次有消息可以拉取了,Follower副本也更新LEO为1.此时,Leader和Follower副本的LEO都是1 ,但各自的高水位依然是0,他们需要在下一轮的拉取中被更新。
在新一轮的拉取请求中,由于位移值是0的消息已经拉取成功,这次请求拉取的位移值=1的消息。Leader副本接收到此请求后,更新远程副本LEO为1 ,然后更新Leader高水位为1,然后将自己高水位值1发送给Follower副本,Follower副本接收到后也将自己的HW更新为1.至此,一次完整的消息同步周期就结束了。
- Leader Epoch登场
依托于高水位,Kafka既界定了消息的对外可见性,又实现了异步的副本同步机制。但还是要思考一下这里存在的问题。从刚才的分析中,我们知道,Follower副本的高水位更新需要一轮额外的拉取请求才能实现,如果把上面例子扩展到多个Follower副本,可能徐亚多轮拉取请求。也就是说,Leader副本高水位更新和Follower副本高水位更新在时间上存在错配,这种错配是很多“数据丢失”或“数据不一致”问题的根源。基于此,社区在0.11版本正是引入Leader Epoch概念,规避高水位更新错配导致的各种不一致问题。
所谓的Leader Epoch。大致可认为是Leader版本,由两部分组成:Epoch 和 起始位移。
Epoch,一个单调增加的版本号,每当副本领导权发生变更,都会增加该版本号。小版本号的Leader被认为是过期的,不能再行使Leader权力。
起始位移,Leader副本在该Epoch值上写入首条消息的位移。
举个例子来说明Leader Epoch,假设现在有两个Leader Epoch,<0, 0>和<1, 120>,第一个表示版本号是0,从位移0开始保存消息,保存了120条消息。之后,Leader发生变更,版本号增加到1,起始位移是120。Kafka Broker会在内存中为每个分区都缓存Leader Epoch数据,同时还定期将这些信息持久化到checkpoint文件中。当Leader副本写入消息到磁盘时,Broker会尝试更新这部分缓存,如果是首次写入,就增加一条Leader Epoch条目,否则不做更新。
先来看看单纯依赖高水位是怎么造成数据丢失的。开始时,Leader副本A和副本B都处于正常状态,某个使用默认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相同逻辑判断,这样位移值为1的那条消息在两副本中均保存下来。后面当生产者程序向B写入新消息时,副本B所在的Broker缓存中,会生成新的Leader Epoch条目:[Epoch=1,Offset=2]。之后,副本B会依据这个条目判断后续是否会执行日志截断操作。