副本数据同步原理

初始状态

leader和follower的HW和LEO都是0,leader副本会保存remote LEO,表示所有follower LEO,也会被初始化为0。这个时候,producer没有发送消息。follower会不断地个leader发送FETCH 请求,但是因为没有数据,这个请求会被leader寄存,当在指定的时间之后会强制完成请求,这个时间 配置是(replica.fetch.wait.max.ms),如果在指定时间内producer有消息发送过来,那么kafka会唤醒 fetch请求,让leader继续处理

第一种情况

生产者发送一条消息 leader处理完producer请求之后,follower发送一个fetch请求过来 。状态图如下

消息中间件 rabbitMQ rocketMq Kafka 异同点 kafka消息中间件原理_数据

leader副本收到请求以后,会做几件事情

1. 把消息追加到log文件,同时更新leader副本的LEO

2. 尝试更新leader HW值。这个时候由于follower副本还没有发送fetch请求,那么leader的remote LEO仍然是0。leader会比较自己的LEO以及remote LEO的值发现最小值是0,与HW的值相同,所 以不会更新HW

follower fetch消息

消息中间件 rabbitMQ rocketMq Kafka 异同点 kafka消息中间件原理_kafka_02

follower 发送fetch请求,leader副本的处理逻辑是:

1. 读取log数据、更新remote LEO=0(follower还没有写入这条消息,这个值是根据follower的fetch 请求中的offset来确定的)

2. 尝试更新HW,因为这个时候LEO和remoteLEO还是不一致,所以仍然是HW=0

3. 把消息内容和当前分区的HW值发送给follower副本

follower副本收到response以后 1. 将消息写入到本地log,同时更新follower的LEO 2. 更新follower HW,本地的LEO和leader返回的HW进行比较取小的值,所以仍然是0 第一次交互结束以后,HW仍然还是0,这个值会在下一次follower发起fetch请求时被更新

消息中间件 rabbitMQ rocketMq Kafka 异同点 kafka消息中间件原理_数据_03

follower发第二次fetch请求,leader收到请求以后 1. 读取log数据 2. 更新remote LEO=1, 因为这次fetch携带的offset是1. 3. 更新当前分区的HW,这个时候leader LEO和remote LEO都是1,所以HW的值也更新为1 4. 把数据和当前分区的HW值返回给follower副本,这个时候如果没有数据,则返回为空

follower副本收到response以后 1. 如果有数据则写本地日志,并且更新LEO 2. 更新follower的HW值 到目前为止,数据的同步就完成了,意味着消费端能够消费offset=1这条消息

第二种情况

由于leader副本暂时没有数据过来,所以follower的fetch会被阻塞,直到等待超时或者 leader接收到新的数据。当leader收到请求以后会唤醒处于阻塞的fetch请求。处理过程基本上和前面说 的一致 1. leader将消息写入本地日志,更新Leader的LEO 2. 唤醒follower的fetch请求 3. 更新HW kafka使用HW和LEO的方式来实现副本数据的同步,本身是一个好的设计,但是在这个地方会存在一个 数据丢失的问题 

数据丢失的问题

前提 min.insync.replicas=1

//设定ISR中的最小副本数是多少,默认值为1(在server.properties中配 置), 并且acks参数设置为-1(表示需要所有副本确认)时,此参数才生效.表达的含义是,至少需要多少个副本同步才能表示消息是提交的, 所以,当 min.insync.replicas=1 的时候,一旦消息被写入leader端log即被认为是“已提交”,而延迟一轮FETCH RPC更新HW值的设计使 得follower HW值是异步延迟更新的,倘若在这个过程中leader发生变更,那么成为新leader的 follower的HW值就有可能是过期的,使得clients端认为是成功提交的消息被删除。

消息中间件 rabbitMQ rocketMq Kafka 异同点 kafka消息中间件原理_缓存_04

producer的ack

acks配置表示producer发送消息到broker上以后的确认值。有三个可选项

  • 0:表示producer不需要等待broker的消息确认。这个选项时延最小但同时风险最大(因为当server宕 机时,数据将会丢失)。
  • 1:表示producer只需要获得kafka集群中的leader节点确认即可,这个选择时延较小同时确保了 leader节点确认接收成功。
  • all(-1):需要ISR中所有的Replica给予接收确认,速度最慢,安全性最高,但是由于ISR可能会缩小到仅 包含一个Replica,所以设置参数为all并不能一定避免数据丢失,

数据丢失的解决方案

在kafka0.11.0.0版本之后,引入了一个leader epoch来解决这个问题,所谓的leader epoch实际上是 一对值(epoch,offset),epoch代表leader的版本号,从0开始递增,当leader发生过变更,epoch 就+1,而offset则是对应这个epoch版本的leader写入第一条消息的offset,比如 (0,0), (1,50) ,表示第一个leader从offset=0开始写消息,一共写了50条。第二个leader版本号是1,从 offset=50开始写,这个信息会持久化在对应的分区的本地磁盘上,文件名是 /tmp/kafkalog/topic/leader-epoch-checkpoint

leader broker中会保存这样一个缓存,并且定期写入到checkpoint文件中 当leader写log时它会尝试更新整个缓存: 如果这个leader首次写消息,则会在缓存中增加一个条目;否 则就不做更新。而每次副本重新成为leader时会查询这部分缓存,获取出对应leader版本的offset

我们基于同样的情况来分析,follower宕机并且恢复之后,有两种情况,如果这个时候leader副本没有 挂,也就是意味着没有发生leader选举,那么follower恢复之后并不会去截断自己的日志,而是先发送 一个OffsetsForLeaderEpochRequest请求给到leader副本,leader副本收到请求之后返回当前的 LEO。

如果follower副本的leaderEpoch和leader副本的epoch相同, leader的leo只可能大于或者等于 follower副本的leo值,所以这个时候不会发生截断

如果follower副本和leader副本的epoch值不同,那么leader副本会查找follower副本传过来的 epoch+1在本地文件中存储的StartOffset返回给follower副本,也就是新leader副本的LEO。这样也避 免了数据丢失的问题

如果leader副本宕机了重新选举新的leader,那么原本的follower副本就会变成leader,意味着epoch 从0变成1,使得原本follower副本中LEO的值的到了保留

Leader副本的选举过程

1. KafkaController会监听ZooKeeper的/brokers/ids节点路径,一旦发现有broker挂了,执行下面 的逻辑。这里暂时先不考虑KafkaController所在broker挂了的情况,KafkaController挂了,各个 broker会重新leader选举出新的KafkaController

2. leader副本在该broker上的分区就要重新进行leader选举,目前的选举策略是

  • a) 优先从isr列表中选出第一个作为leader副本,这个叫优先副本,理想情况下有限副本就是该分 区的leader副本
  • b) 如果isr列表为空,则查看该topic的unclean.leader.election.enable配置。 unclean.leader.election.enable:为true则代表允许选用非isr列表的副本作为leader,那么此 时就意味着数据可能丢失,为false的话,则表示不允许,直接抛出NoReplicaOnlineException异常,造成leader副本选举失 败。
  • c) 如果上述配置为true,则从其他副本中选出一个作为leader副本,并且isr列表只包含该leader 副本。一旦选举成功,则将选举后 的leader和isr和其他副本信息写入到该分区的对应的zk路径上

消息的存储

kafka是使用日志文件的方式来保存生产者和发送者的消息,每条消息都有一 个offset值来表示它在分区中的偏移量,Log并不是直接对应在一个磁盘上的日志文件,而是对应磁盘上的一个目录,这个目录的命名规则 是<topic_name><partion_id>

消息中间件 rabbitMQ rocketMq Kafka 异同点 kafka消息中间件原理_kafka_05

一个topic的多个partition在物理磁盘上的保存路径,路径保存在 /tmp/kafka-logs/topic_partition,包 含日志文件、索引文件和时间索引文件.kafka是通过分段的方式将Log分为多个LogSegment,LogSegment是一个逻辑上的概念,一个 LogSegment对应磁盘上的一个日志文件和一个索引文件,其中日志文件是用来记录消息的。索引文件 是用来保存消息的索引。

LogSegment

每个partition相当于一个巨型文件被平均分配到多 个大小相等的segment数据文件中(每个segment文件中的消息不一定相等),这种特性方便已经被消 费的消息的清理,提高磁盘的利用率

log.segment.bytes=107370 (设置分段大小),默认是1gb,我们把这个值调小以后,可以看到日志 分段的效果

消息中间件 rabbitMQ rocketMq Kafka 异同点 kafka消息中间件原理_缓存_06

segment file由2大部分组成,分别为index file和data file,此2个文件一一对应,成对出现,后 缀".index"和“.log”分别表示为segment索引文件、数据文件

segment文件命名规则:partion全局的第一个segment从0开始,后续每个segment文件名为上一个 segment文件最后一条消息的offset值进行递增。数值最大为64位long大小,20位数字字符长度,没有 数字用0填充

segment中index和log的对应关系

消息中间件 rabbitMQ rocketMq Kafka 异同点 kafka消息中间件原理_缓存_07

如图所示,index中存储了索引以及物理偏移量。 log存储了消息的内容。索引文件的元数据执行对应数 据文件中message的物理偏移地址。举个简单的案例来说,以[4053,80899]为例,在log文件中,对应 的是第4053条记录,物理偏移量(position)为80899. position是ByteBuffer的指针位置

在partition中如何通过offset查找message

1. 根据offset的值,查找segment段中的index索引文件。由于索引文件命名是以上一个文件的最后 一个offset进行命名的,所以,使用二分查找算法能够根据offset快速定位到指定的索引文件

2. 找到索引文件后,根据offset进行定位,找到索引文件中的符合范围的索引。(kafka采用稀疏索 引的方式来提高查找性能)

3. 得到position以后,再到对应的log文件中,从position出开始查找offset对应的消息,将每条消息 的offset与目标offset进行比较,直到找到消息

磁盘存储的性能优化

kafka采用顺序写的方式存储数据

零拷贝

通过“零拷贝”技术,可以去掉这些没必要的数据复制操作,同时也会减少上下文切换次数。现代的unix 操作系统提供一个优化的代码路径,用于将数据从页缓存传输到socket;在Linux中,是通过sendfile系 统调用来完成的。Java提供了访问这个系统调用的方法:FileChannel.transferTo API 使用sendfile,只需要一次拷贝就行,允许操作系统将数据直接从页缓存发送到网络上。所以在这个优 化的路径中,只有最后一步将数据拷贝到网卡缓存中是需要的

消息中间件 rabbitMQ rocketMq Kafka 异同点 kafka消息中间件原理_kafka_08

 

 页缓存

页缓存是操作系统实现的一种主要的磁盘缓存

Kafka中大量使用了页缓存, 这是Kafka实现高吞吐的重要因素之 一 。 虽然消息都是先被写入页缓存, 然后由操作系统负责具体的刷盘任务的, 但在Kafka中同样提供了同步刷盘及间断性强制刷盘(fsync), 可以通过 log.flush.interval.messages 和 log.flush.interval.ms 参数来控制