Kafka的原理理解,以及常规面试题(下)

分区副本

我们已经知道Kafka的每个topic都可以分为多个Partition,并且多个partition会均匀分布在集群的各个节点下。虽然这种方式能够有效的对数据进行分片,但是对于每个partition来说,都是单点的,当其中一个partition不可用的时候,那么这部分消息就没办法消费。

所以kafka为了提高partition的可靠性而提供了副本的概念(Replica),通过副本机制来实现冗余备份。 每个分区可以有多个副本,并且在副本集合中会存在一个leader的副本,所有的读写请求都是由leader 副本来进行处理。剩余的其他副本都做为follower副本,follower副本会从leader副本同步消息日志。

一般情况下,同一个分区的多个副本会被均匀分配到集群中的不同broker上,当leader副本所在的 broker出现故障后,可以重新选举新的leader副本继续对外提供服务。通过这样的副本机制来提高 kafka集群的可用性在这里插入代码片

Leader选取

分区的 Leader 副本选举对用户是完全透明的,它是由 Controller 独立完成的。你需要回答的是,在哪些场景下,需要执行分区 Leader 选举。每一种场景对应于一种选举策略。当前,Kafka 有 4 种分区 Leader 选举策略。

  • OfflinePartition Leader 选举:每当有分区上线时,就需要执行 Leader 选举。所谓的分区上线,可能是创建了新分区,也可能是之前的下线分区重新上线。这是最常见的分区 Leader 选举场景。
  • ReassignPartition Leader 选举:当你手动运行 kafka-reassign-partitions 命令,或者是调用 Admin 的 alterPartitionReassignments 方法执行分区副本重分配时,可能触发此类选举。假设原来的 AR 是[1,2,3],Leader 是 1,当执行副本重分配后,副本集 合 AR 被设置成[4,5,6],显然,Leader 必须要变更,此时会发生 Reassign Partition Leader 选举。
  • PreferredReplicaPartition Leader 选举:当你手动运行 kafka-preferred-replica- election 命令,或自动触发了 Preferred Leader 选举时,该类策略被激活。所谓的 Preferred Leader,指的是 AR 中的第一个副本。比如 AR 是[3,2,1],那么, Preferred Leader 就是 3。
  • ControlledShutdownPartition Leader 选举:当 Broker 正常关闭时,该 Broker 上 的所有 Leader 副本都会下线,因此,需要为受影响的分区执行相应的 Leader 选举。
  • 总的执行流程如下
  1. 优先从isr列表中选出第一个作为leader副本,这个叫优先副本,理想情况下有限副本就是该分区的leader副本
  2. 如果isr列表为空,则查看该topic的unclean.leader.election.enable配置。
  3. 为true则代表允许选用非isr列表的副本作为leader,那么此时就意味着数据可能丢失。并且从其他副本中选出一个作为leader副本,并且isr列表只包含该leader 副本。一旦选举成功,则将选举后 的leader和isr和其他副本信息写入到该分区的对应的zk路径上
  4. 为 false的话,则表示不允许,直接抛出NoReplicaOnlineException异常,造成leader

名词解释

  • leader副本:响应clients端读写请求的副本
  • follower副本:被动地备份leader副本中的数据,不能响应clients端读写请求。
  • ISR副本:包含了leader副本和所有与leader副本保持同步的follower副本 ,具体来说,ISR集合中的副本必须满足两个条件
    • 副本所在节点必须维持着与zookeeper的连接
    • 副本最后一条消息的offset与leader副本的最后一条消息的offset之间的差值不能超过指定的阈值 (replica.lag.time.max.ms) replica.lag.time.max.ms:如果该follower在此时间间隔内一直没有追 上过leader的所有消息,则该follower就会被剔除isr列表
  • LEO:即日志末端位移(log end offset),记录了该副本底层日志(log)中下一条消息的位移值。注意是下一条消息!也就是说,如果LEO=10,那么表示该副本保存了10条消息,位移值范围是[0, 9]。另外, leader LEO和follower LEO的更新是有区别的
  • HW:即上面提到的水位值。对于同一个副本对象而言,其HW值不会大于LEO值。小于等于HW值的所有消息都被认为是“已备份”的(replicated)。同理,leader副本和follower副本的HW更新是有区别的

同步过程

初始状态

初始状态下,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请求过来
  • 第二种是follower阻塞在leader指定时间之内,leader副本收到producer的请求

第一种情况

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

  1. 把消息追加到log文件,同时更新leader副本的LEO
  2. 尝试更新leader HW值,这个时候由于follower副本还没有发送fetch请求,那么leader的remote LEO仍然是0。leader会比较自己的LEO以及remote LEO的值发现最小值是0,与HW的值相同,所 以不会更新HW
ollower fetch消息(第一轮)

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请求时被更新

follower fetch消息(第二轮)

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这条消息。

第二种情况

  1. leader将消息写入本地日志,更新Leader的LEO
  2. 唤醒follower的fetch请求
  3. 更新HW

问题

但是在使用LEO和HW这种机制的时候,因为需要两轮请求,才能真正的完成数据的同步,那么如果在第二轮请求的时候,Leader副本发生了变更或者宕机了,这种情况又怎么处理呢

同步时出现数据不一致解决方案

在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/Follower 模型并没有规定 Follower 副本不可以对外提供读服务。很多框架都是允许这么做的,只是 Kafka 最初为了避免不一致性的问题,而采用了让 Leader 统一提供服务的方式。

  • 场景不适用。读写分离适用于那种读负载很大,而写操作相对不频繁的场景,可 Kafka 不属于这样的场景。
  • 同步机制。Kafka 采用 PULL 方式实现 Follower 的同步,因此,Follower 与 Leader 存 在不一致性窗口。如果允许读 Follower 副本,就势必要处理消息滞后(Lagging)的问题。
    但是从 Kafka 2.4 之后,Kafka 提供了有限度的读写分离,也就是说,Follower 副本能够对外提供读服务。

消息的可靠性

分区副本

你可以创建更多的分区来提升可靠性,但是分区数过多也会带来性能上的开销,一般来说,3个副本就能满足对大部分场景的可靠性要求

生产端

生产者发送消息的可靠性,也就是我要保证我这个消息一定是到了broker并且完成了多副 本的持久化,但这种要求也同样会带来性能上的开销。它有几个可选项

  • 1 ,生产者把消息发送到leader副本,leader副本在成功写入到本地日志之后就告诉生产者消息提交成功,但是如果isr集合中的follower副本还没来得及同步leader副本的消息, leader挂了,就会造成消息丢失
  • -1 ,消息不仅仅写入到leader副本,并且被ISR集合中所有副本同步完成之后才告诉生产者已经提交成功,这个时候即使leader副本挂了也不会造成数据丢失。
  • 0:表示producer不需要等待broker的消息确认。这个选项时延最小但同时风险最大(因为当server宕机时,数据将会丢失)。

消费端

enable.auto.commit默认为true,也就是自动提交offset,自动提交是批量执行的,有一个时间窗口,这种方式会带来重复提交或者消息丢失的问题,所以对于高可靠性要求的程序,要使用手动提 交。 对于高可靠要求的应用来说,宁愿重复消费也不应该因为消费异常而导致消息丢失

消息的存储

一个topic的多个partition在物理磁盘上的保存路径,路径保存在 /tmp/kafka-logs/topic_partition,包 含日志文件、索引文件和时间索引文件
在这里插入图片描述

kafka是通过分段的方式将Log分为多个LogSegment,LogSegment是一个逻辑上的概念,一个 LogSegment对应磁盘上的一个日志文件和一个索引文件,其中日志文件是用来记录消息的。索引文件 是用来保存消息的索引

message的查找

  1. 根据offset的值,查找segment段中的index索引文件。由于索引文件命名是以上一个文件的最后一个offset进行命名的,所以,使用二分查找算法能够根据offset快速定位到指定的索引文件。
  2. 找到索引文件后,根据offset进行定位,找到索引文件中的符合范围的索引。(kafka采用稀疏索引的方式来提高查找性能)
  3. 得到position以后,再到对应的log文件中,从position出开始查找offset对应的消息,将每条消息 的offset与目标offset进行比较,直到找到消息

日志的清除以及压缩

日志清除策略

日志的分段存储,一方面能够减少单个文件内容的大小,另一方面,方便kafka进行日志 清理。日志的清理策略有两个

  1. 根据消息的保留时间,当消息在kafka中保存的时间超过了指定的时间,就会触发清理过程
  2. 根据topic存储的数据大小,当topic所占的日志文件大小大于一定的阀值,则可以开始删除最旧的消息。
    • kafka会启动一个后台线程,定期检查是否存在可以删除的消息通过log.retention.bytes和log.retention.hours这两个参数来设置,当其中任意一个达到要求,都会执行删除。
    • 默认的保留时间是:7天

日志压缩策略

Kafka还提供了“日志压缩(Log Compaction)”功能,通过这个功能可以有效的减少日志文件的大小,缓解磁盘紧张的情况,在很多实际场景中,消息的key和value的值之间的对应关系是不断变化的,就像数据库中的数据会不断被修改一样,消费者只关心key对应的最新的value。因此,我们可以开启kafka 的日志压缩功能,服务端会在后台启动启动Cleaner线程池,定期将相同的key进行合并,只保留最新的 value值。

磁盘存储性能优化

磁盘写入

Kafka采用了顺序写入磁盘的方式,来提高了写入速度

零拷贝

消息从发送到落地保存,broker维护的消息日志本身就是文件目录,每个文件都是二进制保存,生产者和消费者使用相同的格式来处理。在消费者获取消息时,服务器先从硬盘读取数据到内存,然后把内存中的数据原封不动的通过socket发送给消费者

虽然这个操作描述起来很简单,但实际上经历了很多步骤。

  1. 操作系统将数据从磁盘读入到内核空间的页缓存
  2. 应用程序将数据从内核空间读入到用户空间缓存中
  3. 应用程序将数据写回到内核空间到socket缓存中
  4. 操作系统将数据从socket缓冲区复制到网卡缓冲区,以便将数据经网络发出
    在这里插入图片描述

通过“零拷贝”技术,可以去掉这些没必要的数据复制操作,同时也会减少上下文切换次数。现代的unix 操作系统提供一个优化的代码路径,用于将数据从页缓存传输到socket;在Linux中,是通过sendfile系统调用来完成的。

Java提供了访问这个系统调用的方法:FileChannel.transferTo API 使用sendfile,只需要一次拷贝就行,允许操作系统将数据直接从页缓存发送到网络上。所以在这个优化的路径中,只有最后一步将数据拷贝到网卡缓存中是需要的
在这里插入图片描述

页缓存

页缓存是操作系统实现的一种主要的磁盘缓存,但凡设计到缓存的,基本都是为了提升i/o性能,所以页缓存是用来减少磁盘I/O操作的。

磁盘高速缓存有两个重要因素:

  1. 访问磁盘的速度要远低于访问内存的速度,若从处理器L1和L2高速缓存访问则速度更快。
  2. 数据一旦被访问,就很有可能短时间内再次访问。正是由于基于访问内存比磁盘快的多,所以磁盘的内存缓存将给系统存储性能带来质的飞越。
  • 当一个进程准备读取磁盘上的文件内容时, 操作系统会先查看待读取的数据所在的页(page)是否在页缓存(pagecache)中,
    • 如果存在(命中)则直接返回数据, 从而避免了对物理磁盘的I/0操作;
    • 如果没有命中, 则操作系统会向磁盘发起读取请求并将读取的数据页存入页缓存, 之后再将数据返回给进程。
  • 同样,如果一个进程需要将数据写入磁盘, 那么操作系统也会检测数据对应的页是否在页缓存中,
    • 如果不存在, 则会先在页缓存中添加相应的页, 最后将数据写入对应的页。 被修改过后的页也就变成了脏页, 操作系统会在合适的时间把脏页中的数据写入磁盘, 以保持数据的 一 致性

消息都是先被写入页缓存, 然后由操作系统负责具体的刷盘任务的, 但在Kafka中同样提供了同步刷盘及间断性强制刷盘(fsync), 可以通过 log.flush.interval.messages 和 log.flush.interval.ms 参数来控制。

同步刷盘能够保证消息的可靠性,避免因为宕机导致页缓存数据还未完成同步时造成的数据丢失。但是实际使用上,我们没必要去考虑这样的因素以及这种问题带来的损失,消息可靠性可以由多副本来解决,同步刷盘会带来性能的影响。 刷盘的操作由操作系统去完成即可

补充

Leader 总是 -1,怎么办?

在生产环境中,你一定碰到过“某个主题分区不能工作了”的情形。使用命令行查看状态的 话,会发现 Leader 是 -1,于是,你使用各种命令都无济于事,最后只能用“重启大 法”。
但是,有没有什么办法,可以不重启集群,就能解决此事呢?这就是此题的由来。
我直接给答案:删除 ZooKeeper 节点 /controller,触发 Controller 重选举。 Controller 重选举能够为所有主题分区重刷分区状态,可以有效解决因不一致导致的 Leader 不可用问题。

如何调优 Kafka?

回答任何调优问题的第一步,就是确定优化目标,并且定量给出目标!这点特别重要。对于 Kafka 而言,常见的优化目标是吞吐量、延时、持久性和可用性。每一个方向的优化思路都 是不同的,甚至是相反的。
确定了目标之后,还要明确优化的维度。有些调优属于通用的优化思路,比如对操作系统、 JVM 等的优化;有些则是有针对性的,比如要优化 Kafka 的 TPS。我们需要从 3 个方向去考虑

  1. Producer 端:增加 batch.size、linger.ms,启用压缩,关闭重试等。
  2. Broker 端:增加 num.replica.fetchers,提升 Follower 同步 TPS,避免 Broker Full GC 等。
  3. Consumer:增加 fetch.min.bytes 等

Java Consumer 为什么采用单线程来获取消息?

在回答之前,如果先把这句话说出来,一定会加分:Java Consumer 是双线程的设计。一 个线程是用户主线程,负责获取消息;另一个线程是心跳线程,负责向 Kafka 汇报消费者 存活情况。将心跳单独放入专属的线程,能够有效地规避因消息处理速度慢而被视为下线 的“假死”情况。
单线程获取消息的设计能够避免阻塞式的消息获取方式。单线程轮询方式容易实现异步非阻塞式,这样便于将消费者扩展成支持实时流处理的操作算子。因为很多实时流处理操作算子都不能是阻塞式的。另外一个可能的好处是,可以简化代码的开发。多线程交互的代码是非常容易出错的。

常见面试题

 1. Kafka中的ISR、AR又代表什么?
 2. Kafka中的HW、LEO等分别代表什么
 3. 有哪些情形会造成重复消费,以及如何避免
 4. 如何通过offset,找到message
 5. kafka的消息存储结构
 6. 失效副本是什么,主要判断条件是什么,怎么恢复
 7. Kafka的那些设计让它有如此高的性能(分区、生产者消费者模型、offset提交方式、应答模式、日志格式)
 8. kafka follower如何与leader同步数据
 9. Kafka中的消息是否会丢失,以及如何解决
 10. 为什么Kafka不支持读写分离
 11. kafka的零拷贝以及页缓存是什么

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值