Kafka服务器数据同步——基于水印的数据同步方式

767 阅读8分钟

1、背景

Kafka是由Apache软件基金会开发的一个开源流处理平台,由Scala和Java编写。Kafka是一种高吞吐量的分布式发布订阅消息系统,它可以处理消费者在网站中的所有动作流数据。

为保证Kafka集群的高可用性,Kafka会在集群若干个服务器上分配同一个分区。集群会选择一个分区leader,若干分区follower。由leader接受producer写入的数据,然后由follower向leader同步数据。本文就是对同步数据的过程进行简介,该过程也是典型的基于水印的数据同步方式。

2、名词解释

2.1 基本名词

  • 主题:topic。逻辑概念,用于区分业务
  • 服务器:broker。接收持久化消息,管理topic,权限管理,consumer重平衡等等
  • 分区:partition。一个有序的消息序列,一个topic对应多个分区
  • 消息:record。不做解释
  • 消息位移:Offset。分区中每条消息的位置
  • 生产者:producer。
  • 消费者:consumer。

image.png 注意,该图展示了kafka的逻辑架构。

2.2 副本与ISR

这两个是broker里面的两个概念名词。

所谓副本,就是kafka为了提高可用性,将同一个parition数据分散到不同broker的数据备份。同时kafka会选出一个leader副本用于对外读,follower则需要主动向leader副本请求同步kafka日志,以保证主从副本数据保持一致。

所谓ISR,就是有资格能被评选为leader副本的follower副本集合。只有leader和ISR中所有副本都同步状态了,才会被kafka认为该消息已经提交。

image.png

2.3 副本中的概念

起始位移( base offset ) :该副本第一条消息的位移

高水印值( high watermark, HW ): 该副本最新一条已经提交的消息位移。如果某个消息的offset小于该值,则所有的副本都已经同步这条消息了;如果某个消息的offset大于该值,则肯说明些副本还没同步到这条消息。换一种说法,所有offset小于该值的消息对consumer是可见的;而大于该值的是不可见的。该值非常重要,他的值影响到副本主从同步的位置,影响到consumer消费数据的位置。应该注意的是,HW不止在leader里存在,在follower里也存在,其原因就是为了防止leader崩溃,follower也能立即顶替leader进行正常工作(最终一致性)。

日志末端位移 (Clog end offset, LEO ): 该副本最后一条信息的位移

image.png

3、消息同步流程

注意,这里的主体流程是kafka 0.11版本以前的同步流程。0.11版本以后,略有不同,后文会介绍。

3.1 整体流程

image.png ①broker1上的 leader 副本接收到消息,把自己的 LEO 值更新为 1 。

②broker2 和 broker3 上的 follower 副本各自发送请求给 broker 1。(一般情况下,是follower定时给leader发送fetch请求的,就好像heartbeat心跳)

③broker1收到fetch请求后,主动分别把该消息推送给 follower 副本 。

④follower 副本接收到消息后各自更新自己的 LEO 为 1,并返回response。

⑤leader 副本接收到其他 follower 副本的数据请求响应( response )之后,更新 HW 值为 1 。 此时位移为 0 的这条消息可以被 consumer 消费。

3.2 流程细节

原则:HW是当前已知的所有LEO的最小值。为什么呢?正常情况下,各个broker的partition数据都是顺序写入的,最小的LEO意味着所有的副本都同步到了这个LEO以前的所有数据,就满足了“HW之前的消息都已经同步完成”的要求。

为了便于描述,我们假设有两个副本在同步数据,一个leader一个follower

  • 第一轮fetch

image.png 在某一时刻,leader收到了一条信息,写入了底层数据,接下来就是数据同步的过程了。

1、leader的LEO +1,好理解,有了一条信息,尾数需要加一。

2、leader尝试更新HW,取所有副本LEO最小值,本例是0。那么,哪里获取各个副本最小值呢?leader副本本地有个地方专门负责缓存这个数据,其他follower通过fetch请求告知leader

这个时候,follower发送了fetch(fetch请求里会带着自己现在的LEO,现在是0),leader收到了fetch

3、leader收到了fetch,尝试更新HW。所有副本LEO最小值。自己的是1,fetch里是0,那么是0。

4、获取offset >follower LEO的数据放到response里,同时将自己的HW(注意!!此时是全局LEO最小值)放到response里,本例里是0

5、follower接受到了response,将数据写入,同时更新LEO,本例里+1

6、follower尝试更新HW,是全局LEO最小值,比较response里的HW和自己的LEO取最小值即可(上面4步的特性,这里就用到了)。本例里更新后是0

此时第一轮fetch结束,应该注意到,第一轮fetch完成后,数据虽然同步过去了,但是还不可见,因为leader此时还不知道follower是不是同步成功了

  • 第二轮fetch

image.png 1、follower发送了fetch请求,携带自己的LEO=1

2、leader尝试更新HW,全局LEO的最小值,因此是1

3、获取offset >follower LEO的数据放到response里,这次没有数据,同时将自己的HW=1放到response里

4、follower收到信息,没数据写入,然后尝试更新自己的HW,全局最小值,本例HW=1

此时第二轮fetch结束,此时此刻,数据同步才真正结束,这条新数据对外可见了

3.3 数据不一致的问题

综合上面的论述,经过两轮fetch过程后才会对外可见。这个时间差就容易导致数据丢失或者不一致的问题

场景一:两轮fetch中间发生follwer和leader先后崩溃,前提:leader写入完成即认为已提交

image.png

如图,假如第二轮fetch发生,A已经更新了HW,但是还没有包装response返回给B,此时B发生了崩溃。重启后的B会将LEO调整成崩溃前的HW值,那么后面的数据就被删除了(看,此时出现了一次数据不一致)。这个时候B想要向A发起fetch,如果这个时候恰好A挂掉了,B被选为leader,A重启回来后就会fetch取leader的HW和自己的LEO比较取最小值,最后得到HW=1,这样原来HW=2的数据就永久丢失了。

场景二:两轮fetch中,follower和leader同时崩溃

image.png

还是上面那种情况,第二轮fetch发生,A已经更新了HW,但是还没有包装response返回给B。这个时候leader和follower同时崩溃,然后B先重启成为leader了。这个时候,producer发送了一个消息记录到B,此时因为没有follower,因此直接更新HW到2。而后A回来成为follower,这时,A发现自己的HW和B的HW相等,因此不做变更。但是A的HW指向的消息和B的HW指向的消息并不是一回事,这显然就不是我们想要的了。

3.4 数据不一致问题的解决

kafka 0.11版本之后引入了leader epoch(我理解其实就是带有版本信息的HW)来取代HW,同时重启后的follower增加了一种请求,解决了这个问题。(这也是为啥商业使用基本上都用0.11之后的版本)。epoch实际上是一对值(epoch,offset)。 epoch 表示 leader 的版本号,offset表示本次写入的位置。比如(0,0)就代表这是第一次写入,写入位置是0 。(1,120)就代表,这是第二次写入,写入位置是120(前面已经写了119个数据)

场景一的解决:

image.png A和B都会保存epoch值。在A挂掉前,B重启后,B向A发出请求得到回复,得到的response里的epoch为(0 , 2)。A挂掉后,B被重新选择为leader,此时会把epoch加1,同时由于B没有存储offset >= 2的数据,因此不做任何截断。A回来再次向B同步则不会出现数据丢失问题。

场景二的解决:

image.png 如图,当B先重启回来,接受了消息。此时B的epoch是(1 , 1)。截止此时此刻,A和B都有offset=1的数据,但是这俩数据不一样。然后A重启回来,发送B消息,返回epoch是(1 , 1)。于是抛弃自己offset = 1的数据,转而存储B的数据。应该说,虽然保证了副本数据的一致性,但是出现了数据丢失的情况。因此,实际应用上,如果把kafka作为消息队列使用,并不会应用在需要强可靠性的场景中,例如付款。