Kafka 问题收集
Kafka 问题收集
Kafka关键字名词
ISR
所有与leader副本保持一定程度同步的副本(包括leader副本在内)组成 ISR
(In Sync Replicas)。 ISR 集合是 AR 集合的一个子集。
AR
分区中的所有副本统称为 AR
(Assigned Replicas)
OSR
于leader副本同步滞后过多的副本(不包括leader副本)将组成 OSR
(Out-of-Sync Replied)由此可见,AR = ISR + OSR。
HW
HW是High Watermak的缩写, 俗称高水位,它表示了一个特定消息的偏移量(offset),消费之只能拉取到这个offset之前的消息。
LEO
Log End Offset的缩写,它表示了当前日志文件中下一条待写入消息的offset。分区ISR集合中的每个副本都会维护自身的LEO,而ISR集合中最小的LEO即为分区的HW。
这个offset未必在硬盘中,可能目前只在内存中还没有被flush到硬盘。
LW
Low Watermark的缩写,俗称“低水位”,代表AR集合中最小的logStartOffset值。
一般情况下,日志文件的起始偏移量 logStartOffset 等于第一个日志分段的 baseOffset,但这并不是绝对的,旧日志的清理和消息删除都有可能促使LW的增长。
LSO
特指LastStableOffset
,它与kafka 事务有关。对于未完成的事务而言,LSO的值等于事务中的第一条消息所在的位置(firstUnstableOffset);对于已经完成的事务而言,它的值等同于HW相同。
Kafka的一个消费端的参数——isolation.level
,这个参数用来配置消费者的事务隔离级别。字符串类型,有效值为“read_uncommitted”和 “read_committed”,表示消费者所消费到的位置,如果设置为“read_committed”,那么消费者就会忽略事务未提交的消息,即只能消费到 LSO(LastStableOffset)的位置,默认情况下为 “read_uncommitted
”,即可以消费到 HW(High Watermark)
处的位置。注意:follower副本的事务隔离级别也为“read_uncommitted”,并且不可修改。
这个LSO还会影响Kafka消费滞后量(也就是Kafka Lag,很多时候也会被称之为消息堆积量)的计算。不妨我们先来看一下下面这幅图。
在图中,对每一个分区而言,它的 Lag 等于 HW – ConsumerOffset 的值,其中 ConsumerOffset 表示当前的消费位移。当然这只是针对普通的情况。如果为消息引入了事务,那么 Lag 的计算方式就会有所不同。
如果消费者客户端的 isolation.level 参数配置为“read_uncommitted”(默认),那么 Lag的计算方式不受影响;如果这个参数配置为“read_committed”,那么就要引入 LSO 来进行计算了。
对未完成的事务而言,LSO 的值等于事务中第一条消息的位置(firstUnstableOffset,如上图所示),对已完成的事务而言,它的值同 HW 相同, 所以我们可以得出一个结论:LSO≤HW≤LEO。(如下图所示)
所以,对于分区中有未完成的事务,并且消费者客户端的 isolation.level 参数配置为“read_committed”的情况,它对应的 Lag 等于 LSO – ConsumerOffset 的值。
logStartOffset
logStartOffset 日志段集合中第一个日志段(segment)的基础位移,也就是这个日志对象的基础位移
总结
HW、LW 是分区层面的概念;而LEO、LogStartOffset 是日志层面的概念;LSO 是事务层面的概念。
隔离级别未提交:消费者能消费的数据 = [LW,HW)
隔离级别已提交:消费者能消费的数据 = [LW,LSO)
分区副本ISR选举机制
当 Leader
副本宕机之后,会从ISR
同步副本列表中剔除,然后取剩下的ISR
列表中第一个为Leader
副本,显然有可能还有些副本数据没有及时同步完成,当选择为Leader
副本之后有可能数据会丢失。
相关配置:
## 默认10s,isr中的follow没有向isr发送心跳包就会被移除
replica.lag.time.max.ms = 10000
## 根据leader 和副本的信息条数差值决定是否从isr 中剔除此副本,此信息条数差值根据配置参数,在broker数量较少,或者网络不足的环境中,建议提高此值.
## Kafka 0.9.0.0版本以后已经删除该配置
replica.lag.max.messages = 4000
## follower与leader之间的socket超时时间
replica.socket.timeout.ms=30*1000
## 数据同步时的socket缓存大小
replica.socket.receive.buffer.bytes=64*1024
## replicas每次获取数据的最大大小
replica.fetch.max.bytes =1024*1024
## replicas同leader之间通信的最大等待时间,失败了会重试
replica.fetch.wait.max.ms =500
## fetch的最小数据尺寸,如果leader中尚未同步的数据不足此值,将会阻塞,直到满足条件
replica.fetch.min.bytes =1
## leader进行复制的线程数,增大这个数值会增加follower的IO
num.replica.fetchers=1
## 每个replica检查是否将最高水位进行固化的频率
replica.high.watermark.checkpoint.interval.ms = 5000
## leader的不平衡比例,若是超过这个数值,会对分区进行重新的平衡
leader.imbalance.per.broker.percentage = 10
## 检查leader是否不平衡的时间间隔
leader.imbalance.check.interval.seconds = 300
Kafka 工作流程
Pub-Sub 消息模型工作流程
下面是 Pub-Sub 消息模型的工作流程
- 生产者定期向topic发送消息。
- Kafka broker 根据配置将topic的消息存储到指定的partition上。Kafka确保所有的消息均匀分布在topic的所有partition上。如果producer发送了两条消息,并且该topic有两个partition,则每个partition会有一条消息。
- Consumer 订阅指定的topic。
- 一旦消费者订阅了topic,Kafka将向消费者提供topic的当前
offset
,并且还将offset
保存在Zookeeper中。 - 消费者将定期请求Kafka(如100 Ms)新消息。
- 一旦Kafka从生产者接收到消息,它将这些消息转发给消费者。
- 消费者将收到消息并进行处理。
- 一旦消息被处理,消费者将向Kafka broker发送确认。
- 一旦Kafka收到确认,它将
offset
更改为新值,并在Zookeeper中更新它。 由于offset
在Zookeeper中被维护,消费者可以正确地读取下一条消息,即使服务器宕机后重启。 - 以上流程将重复,直到消费者停止请求。
- 消费者可以随时回退/跳转到某个topic的期望
offset
处,并读取所有后续消息。
队列消息模型工作流程 & Consumer Group
在基于队列的消息系统中,取代单个消费者的是订阅了相同topic的一群拥有相同Group ID
的消费者集群。简单来说,订阅具有相同“组ID”的主题的消费者被认为是单个组,并且消息在它们之间共享。 让我们检查这个系统的实际工作流程。
- 生产者定期向topic发送消息。
- Kafka broker 根据配置将topic的消息存储到指定的partition上。
- 单个consumer以名为
Group-1
的Group ID
订阅名为Topic-01
的topic。 - Kafka 会以和Pub-Sub消息模型相同的方式和consumer进行交互知道新的消费者以同样的Group ID加入到消费者分组中。
- 一旦新的消费者加入后,Kafka将操作切换到共享模式,将所有topic的消息在两个消费者间进行均衡消费。这种共享行为直到加入的消费者结点数目达到该topic的分区数。
- 一旦消费者的数目大于topic的分区数,则新的消费者不会收到任何消息直到已经存在的消费者取消订阅。出现这种情况是因为Kafka中的每个消费者将被分配至少一个分区,并且一旦所有分区被分配给现有消费者,新消费者将必须等待。
该功能被称为 “Consumer Group”。以同样的方式,Kafka将以非常简单和高效的方式提供这两种系统功能。
ZooKeeper的角色
Apache Kafka的一个关键依赖是Apache Zookeeper,它是一个分布式配置和同步服务。 Zookeeper是Kafka代理和消费者之间的协调接口。 Kafka服务器通过Zookeeper集群共享信息。 Kafka在Zookeeper中存储基本元数据,例如关于主题,代理,消费者偏移(队列读取器)等的信息。由于所有关键信息存储在Zookeeper中,并且它通常在其整个集群上复制此数据,因此Zookeeper的故障不会影响Kafka集群的状态。一旦Zookeeper重新启动, Kafka将恢复状态。 这为Kafka带来了零停机时间。 Kafka代理之间的领导者选举也通过使用Zookeeper在领导失败的情况下完成。
要了解更多关于Zookeeper,请参考http://www.tutorialspoint.com/zookeeper/
Kafka 副本管理器ReplicaManager
replicaManager主要用来管理topic在本broker上的副本信息。并且读写日志的请求都是通过replicaManager进行处理的。
每个replicaManager实例都会持有一个Pool[TopicPartition, Partition]类型的allPartitions变量。Pool其实就是一个Map的封装。通过key(TopicPartition)我们可以知道这个partition的编号,以及他对应的partiton信息。
接着parition信息中有持有该partiton的各个Replica的信息,通过获取到本broker上的Replica,可以间接获取到Replica对应的Log对象实例,然后用Log对象实例来处理日志相关的读写操作。
关于 ReplicaManager 的 allPartitions 变量可以看下面这张图(假设 Partition 设置的是3副本):
Kafka 源码解析之 ReplicaManager 详解(十五)
Kafka 分区数是不是越多越好?
客户端/服务器端需要使用的内存就越多
Kafka0.8.2之后,在客户端producer有个参数batch.size,默认是16KB。它会为每个分区缓存消息,一旦满了就打包将消息批量发出。看上去这是个能够提升性能的设计。不过很显然,因为这个参数是分区级别的,如果分区数越多,这部分缓存所需的内存占用也会更多。假设你有10000个分区,按照默认设置,这部分缓存需要占用约157MB的内存。而consumer端呢?我们抛开获取数据所需的内存不说,只说线程的开销。如果还是假设有10000个分区,同时consumer线程数要匹配分区数(大部分情况下是最佳的消费吞吐量配置)的话,那么在consumer client就要创建10000个线程,也需要创建大约10000个Socket去获取分区数据。这里面的线程切换的开销本身已经不容小觑了。
服务器端的开销也不小,如果阅读Kafka源码的话可以发现,服务器端的很多组件都在内存中维护了分区级别的缓存,比如controller,FetcherManager等,因此分区数越多,这种缓存的成本就越大。
文件句柄的开销
每个分区在底层文件系统都有属于自己的一个目录。该目录下通常会有两个文件: base_offset.log和base_offset.index。Kafak的controller和ReplicaManager会为每个broker都保存这两个文件句柄(file handler)。很明显,如果分区数越多,所需要保持打开状态的文件句柄数也就越多,最终可能会突破你的ulimit -n的限制。
降低高可用性
Kafka通过副本(replica)机制来保证高可用。具体做法就是为每个分区保存若干个副本(replica_factor指定副本数)。每个副本保存在不同的broker上。期中的一个副本充当leader 副本,负责处理producer和consumer请求。其他副本充当follower角色,由Kafka controller负责保证与leader的同步。如果leader所在的broker挂掉了,contorller会检测到然后在zookeeper的帮助下重选出新的leader——这中间会有短暂的不可用时间窗口,虽然大部分情况下可能只是几毫秒级别。但如果你有10000个分区,10个broker,也就是说平均每个broker上有1000个分区。此时这个broker挂掉了,那么zookeeper和controller需要立即对这1000个分区进行leader选举。比起很少的分区leader选举而言,这必然要花更长的时间,并且通常不是线性累加的。如果这个broker还同时是controller情况就更糟了。
- 客户端会为每个分区调用一条线程处理,多线程并发地处理分区消息,分区越多,意味着处理的线程数也就越多,到一定程度后,会造成线程切换开销大;
- 其中一个 broker 挂掉后,如果此时分区特别多,Kafka 分区 leader 重新选举的时间大大增加;
- 每个分区对应都有文件句柄,分区越多,系统文件句柄就越多;
- 客户端在会为每个分区分配一定的缓冲区,如果分区过多,分配的内存也越大。
Kafka 为什么不支持分区减少
实现此功能需要考虑的因素很多,比如删除掉的分区中的消息该作何处理?
如果随着分区一起消失则消息的可靠性得不到保障;如果需要保留则又需要考虑如何保留。
直接存储到现有分区的尾部,消息的时间戳就不会递增,如此对于Spark、Flink这类需要消息时间戳(事件时间)的组件将会受到影响;
如果分散插入到现有的分区中,那么在消息量很大的时候,内部的数据复制会占用很大的资源,而且在复制期间,此主题的可用性又如何得到保障?与此同时,顺序性问题、事务性问题、以及分区和副本的状态机切换问题都是不得不面对的。
反观这个功能的收益点却是很低,如果真的需要实现此类的功能,完全可以重新创建一个分区数较小的主题,然后将现有主题中的消息按照既定的逻辑复制过去即可。
虽然分区数不可以减少,但是分区对应的副本数是可以减少的,这个其实很好理解,你关闭一个副本时就相当于副本数减少了。不过正规的做法是使用kafka-reassign-partition.sh
脚本来实现。
Kakfa 消费消息,pull or push
Kafka 是 Pull 模式的消息队列,即 Consumer 连到消息队列服务上,主动请求新消息,如果要做到实时性,需要采用长轮询,Kafka 在0.8的时候已经支持长轮询模式。
https://juejin.cn/post/6854573212496953351)
Kafka 时间轮
DelayedOperationPurgatory是用来缓存延时请求(Delayed Request)的。所谓延时请求,就是那些一时未满足条件不能立刻处理的请求。比如设置了 acks=all 的 PRODUCE 请求,一旦设置了 acks=all,那么该请求就必须等待 ISR 中所有副本都接收了消息后才能返回,此时处理该请求的 IO 线程就必须等待其他 Broker 的写入结果。当请求不能立刻处理时,它就会暂存在 Purgatory 中。稍后一旦满足了完成条件,IO 线程会继续处理该请求,并将 Response 放入对应网络线程的响应队列中。
Tombstone消息
翻译过来就是墓碑消息,Tombstone消息的Value 为 null。Tombstone消息在注册消息和位移消息中都可能出现。如果在注册消息中出现,表示Kafka可以将该消费者组元数据从位移主题中删除;如果在位移消息中出现了,则表示Kafka能够将该消费者组在某主题分区上的位移提交数据删除。这很好的保证了,内部位移主题不会持续增加磁盘占用空间。
Kafka 机架的分配策略(机架感知)
从狭义字面上理解,机架感知的意思是Kafka会把partition的各个replicas分散到不同的机架上,以提高机架故障时的数据安全性。假如所有replicas(包括leader)都在一个机架上,那么当这个机架发生故障时,所有这个机架上的server都不能提供服务,就会发生数据丢失,而多个机架同时发生故障的概率则要小得多。
从广义上来讲,这实际上是个broker分组功能,可以将不同组的brokers分散到不同的区域中,以提高单个区域发生故障时整个集群的可用性。比如碰到有个用户,他们公司的网络分成若干个分区,他们希望当单个网络分区故障,或者跟其他分区无法通信时,Kafka仍旧能够保证正常工作,这就可以用到broker分组功能。
你可以通过修改server.properties来指定broker属于哪个特定的组(机架):
broker.rack=rack-id-n
当创建,更新或者replicas重新分布时,将会遵从分组规则,确保单个partition的所有replicas被分散到尽可能多的组内,最多会被分散到min(#racks, replication-factor) 个不同的组。
Kafka 消息堆积(Lag)
- LogStartOffset:表示一个Partition的起始位移,初始为0,虽然消息的增加以及日志清除策略的影响,这个值会阶段性的增大。
- ConsumerOffset:消费位移,表示Partition的某个消费者消费到的位移位置。
- HighWatermark:简称HW,代表消费端所能“观察”到的Partition的最高日志位移,HW大于等于ConsumerOffset的值。
- LogEndOffset:简称LEO, 代表Partition的最高日志位移,其值对消费者不可见。比如在ISR(In-Sync-Replicas)副本数等于3的情况下(如下图所示),消息发送到Leader A之后会更新LEO的值,Follower B和Follower C也会实时拉取Leader A中的消息来更新自己,HW就表示A、B、C三者同时达到的日志位移,也就是A、B、C三者中LEO最小的那个值。由于B、C拉取A消息之间延时问题,所以HW必然不会一直与Leader的LEO相等,即LEO>=HW。
Lag=HW - ConsumerOffset。对于这里大家有可能有个误区,就是认为Lag应该是LEO与ConsumerOffset之间的差值。但是LEO是对消费者不可见的,既然不可见何来消费滞后。
ConsumerOffset位移获取
ConsumerOffset,Kafka中有两处可以存储,一个是Zookeeper,而另一个是__consumer_offsets
这个内部topic中,前者是0.8.x版本中的使用方式,但是随着版本的迭代更新,现在越来越趋向于后者。就拿1.0.0版本来说,虽然默认是存储在__consumer_offsets
中。
对于消费位移来说,其一般不会实时的更新,而更多的是定时更新,这样可以提高整体的性能。那么这个定时的时间间隔就是ConsumerOffset的误差区间之一。
Kafka 失效副本 OSR
怎么样判定一个分区是否有副本是处于同步失效状态的呢?从Kafka 0.9.x版本开始通过唯一的一个参数replica.lag.time.max.ms(默认大小为10,000)来控制,当ISR中的一个follower副本滞后leader副本的时间超过参数replica.lag.time.max.ms指定的值时即判定为副本失效,需要将此follower副本剔出除ISR之外。
实现原理:
当follower副本将leader副本的LEO(Log End Offset,每个分区最后一条消息的位置)之前的日志全部同步时,则认为该follower副本已经追赶上leader副本,此时更新该副本的lastCaughtUpTimeMs标识。Kafka的副本管理器(ReplicaManager)会启动一个副本过期检测的定时任务,而这个定时任务会定时检查当前时间与副本的lastCaughtUpTimeMs差值是否大于参数replica.lag.time.max.ms指定的值。
千万不要错误的认为follower副本只要拉取leader副本的数据就会更新lastCaughtUpTimeMs,试想当leader副本的消息流入速度大于follower副本的拉取速度时,follower副本一直不断的拉取leader副本的消息也不能与leader副本同步,如果还将此follower副本置于ISR中,那么当leader副本失效,而选取此follower副本为新的leader副本,那么就会有严重的消息丢失。
Kafka源码注释中说明了一般有两种情况会导致副本失效:
- follower副本进程卡住,在一段时间内根本没有向leader副本发起同步请求,比如频繁的Full GC。
- follower副本进程同步过慢,在一段时间内都无法追赶上leader副本,比如IO开销过大。
Kafka 副本crash场景
假设某个partition中的副本数为3,replica-0, replica-1, replica-2分别存放在broker0, broker1和broker2中。AR=(0,1,2),ISR=(0,1)。
设置request.required.acks=-1, min.insync.replicas=2,unclean.leader.election.enable=false。这里将broker0中的副本也称之为broker0起初broker0为leader,broker1为follower,broker2为OSR
-
当ISR中的replica-0出现crash的情况时,broker1选举为新的leader,此时ISR=(1),因为受min.insync.replicas=2影响,write不能服务,但是read能继续正常服务。此种情况恢复方案:
-
尝试恢复(重启)replica-0,如果能起来,系统正常;
-
如果replica-0不能恢复,需要将min.insync.replicas设置为1,恢复write功能。
-
-
当ISR中的replica-0出现crash,紧接着replica-1也出现了crash, 此时[ISR=(1),leader=-1],不能对外提供服务,此种情况恢复方案:
- 尝试恢复replica-0和replica-1,如果都能起来,则系统恢复正常;
- 如果replica-0起来,而replica-1不能起来,这时候仍然不能选出leader,因为当设置unclean.leader.election.enable=false时,leader只能从ISR中选举,当ISR中所有副本都失效之后,需要ISR中最后失效的那个副本能恢复之后才能选举leader, 即replica-0先失效,replica-1后失效,需要replica-1恢复后才能选举leader。保守的方案建议把unclean.leader.election.enable设置为true,但是这样会有丢失数据的情况发生,这样可以恢复read服务。同样需要将min.insync.replicas设置为1,恢复write功能;
- replica-1恢复,replica-0不能恢复,这个情况上面遇到过,read服务可用,需要将min.insync.replicas设置为1,恢复write功能;
- replica-0和replica-1都不能恢复,这种情况可以参考情形2.
-
当ISR中的replica-0, replica-1同时宕机,此时[ISR=(0,1)],不能对外提供服务,此种情况恢复方案:尝试恢复replica-0和replica-1,当其中任意一个副本恢复正常时,对外可以提供read服务。直到2个副本恢复正常,write功能才能恢复,或者将将min.insync.replicas设置为1。
Kafka 多线程消费消息
无论是Kafka官方提供的客户端API,还是Spring封装的Spring Kafka,在消息消费方面,均只是实现了默认情况下的1个Consumer1个线程。若希望1个Consumer有多个线程来加快消费速率,以进一步提升对partition的并行消费能力,则需要开发者自己实现(比如该Consumer一次拉取100条消息,分发给多个线程并行处理)。
这在一定程度上,也对客户端的并行消费能力造成了一定了限制,默认情况下,要提升并行能力,则只能通过增加所订阅Topic的Partition数量,才能增加消费线程数量,进而才能扩展客户端的并行消费能力。比如:
- 对现有的Kafka集群扩容(如增加broker实例数量,重新分区,增加Partition的数量)
- 将部分Topic从现有的Kafka集群中拆分出来,放到新建的Kafka集群中(本质上也属于一种扩容)
以上2种扩展方式不可避免的需要涉及到数据迁移,因此,客观来说,这2种扩展方式相对有点“重”。
优点 | 缺点 | |
---|---|---|
方法1(每个线程维护一个KafkaConsumer) | 方便实现 速度较快,因为不需要任何线程间交互 易于维护分区内的消息顺序 | 更多的TCP连接开销(每个线程都要维护若干个TCP连接) consumer数受限于topic分区数,扩展性差 频繁请求导致吞吐量下降 线程自己处理消费到的消息可能会导致超时,从而造成rebalance |
方法2 (单个(或多个)consumer,多个worker线程) | 可独立扩展consumer数和worker数,伸缩性好 | 实现麻烦通常难于维护分区内的消息顺序处理链路变长,导致难以保证提交位移的语义正确性 |
方案如下:
如上图所示,整体交互以及线程的分工与前面的“至多消费一次”是类似的,最主要的差异在于,工作线程和拉取线程分别多了一项工作:更新offset、提交offset。
两类线程分工:
- 拉取线程:拉取对应Topic的消息,并将消息分发给工作线程;同时异步提交已完成业务处理的消息的offset;
- 工作线程:完成消息的业务处理后,把该消息的offset同步更新到待提交的offset池子中(但不提交);
如上图所示,拉取线程与工作线程是通过阻塞队列来异步解耦,即每个工作线程都会有一个阻塞队列,拉取线程将拉取到的消息放入到该队列后就立即返回,工作线程会从该队列中获取消息,进行后续的业务处理。两者都是异步并行的。
如上图所示,通过一个公共的Map来暂存待提交的offset,该Map的key是消息所属Partition,value则是要提交的offset。能放入到该Map中的offset,说明其对应的消息已完成了业务处理。
offset的更新(工作线程)与offset的提交(拉取线程)则是通过该Map来异步解耦。工作线程完成该消息的业务处理后,就会把该消息对应的offset放入Map(更新该Partition最新完成的offset,但不提交),拉取线程则会一直检查该Map的每个Partition的offset是否有更新,若有更新,则就会提交该offset到Kafka。
Kafka 读写空中接力(Flying on Air)
- 当Producer push消息到Broker发生写操作时,Broker只是将数据写入Page Cache中,并将该页置上dirty标志。
- 当Consumer 从Broker pull消息发生读操作时,Broker会首先在Page Cache中查找内容,如果有就直接返回了,没有的话就会从磁盘读取文件再写回Page Cache。
- 可见,只要Producer 生产者与Consumer 消费者的速度相差不大,消费者会直接读取之前生产者写入Page Cache的数据,在内存里完成接力,根本没有磁盘读访问。而比起在内存中维护一份消息数据的传统做法,这既不会重复浪费一倍的内存,Page Cache又不需要GC(可以放心使用大把内存了),而且即使Kafka Broker重启了,Page Cache还依然在。
所以在进行KAFKA + Flink流测试时,有时会很少看到磁盘读操作
Kafka checkpoint机制定期持久化
recovery-point-offset-checkpoint:它保存的是第一条未flush到磁盘的消息。Kafka对它进行checkpointing能够显著加速日志段恢复(recover)的速度,因为直接从recovery point offset所在的日志段开始恢复即可,没必要从头恢复日志段。毕竟生产环境上,分区下的日志段文件可能是非常多的。
kafka中会有一个定时任务负责将所有分区的LEO刷写到恢复点文件recovery-point-offset-checkpoint中,定时周期由broker端参数log.flush.offset.checkpoint.interval.ms配置,默认值60000,即60s。Kafka在启动时会检查文件的完整性,如果没有.kafka_cleanshutdown这个文件,就会进入一个recover逻辑,recover就是从此文件中的offset开始。
replication-offset-checkpoint:用来存储每个replica的HW,表示已经被commited的offset信息。失败的follower开始恢复时,会首先将自己的日志截断到上次的checkpointed时刻的HW,然后向leader拉取消息。
kafka有一个定时任务负责将所有分区的HW刷写到复制点文件replication-offset-checkpoint中,定时周期由broker端参数replica.high.watermark.checkpoint.interval.ms配置,默认值5000,即5s。
log-start-offset-checkpoint:对应logStartOffset,用来标识日志的起始偏移量。
kafka中有一个定时任务负责将所有分区的logStartOffset刷写到起始点文件log-start-offset-checkpoint中,定时周期有broker端参数log.flush.start.offset.checkpoint.interval.ms配置,默认值60000,即60s。
cleaner-offset-checkpoint:存了每个log的最后清理offset。
Kafka 分区及其副本的均匀分布算法
为了更好的做负载均衡,Kafka尽量将所有的Partition和其副本均匀分配到整个集群上。
第一个分区(编号为0)的第一个副本放置位置是随机从 brokerList 选择的;
其它分区的第一个副本放置位置相对于第0个分区依次往后移。也就是如果我们有5个 Broker,5个分区,假设第一个分区放在第四个 Broker 上,那么第二个分区将会放在第五个 Broker 上;第三个分区将会放在第一个 Broker 上;第四个分区将会放在第二个 Broker 上,依次类推;
剩余的副本相对于第一个副本放置位置其实是由 nextReplicaShift 决定的,而这个数也是随机产生的;
假设现在有5个 Broker,分区数为5,副本为3的主题,假设第一个分区放在broker0上,下一个副本依次放下一个broker,那分布大概为:
如果再考虑到机架的因素,我可以举例,现在如果我们有两个机架的 Kafka 集群,brokers 0、1 和 2 同属于一个机架1;brokers 3、 4 和 5 属于机架2。现在我们对这些 Broker 进行排序:0, 3, 1, 4, 2, 5(每个机架依次选择一个Broker进行排序)。按照机架的 Kafka 分区放置算法,如果分区0的第一个副本放置到broker 4上面,那么其第二个副本将会放到broker 2上面,第三个副本将会放到 broker 5上面;同理,分区1的第一个副本放置到broker 2上面,其第二个副本将会放到broker 5上面,第三个副本将会放到 broker 0上面。这就保证了这两个副本放置到不同的机架上面,即使其中一个机架出现了问题,我们的 Kafka 集群还是可以正常运行的。现在把机架因素考虑进去的话,我们的分区看起来像下面一样:
Kafka leader 分区自动平衡机制
broker配置auto.leader.rebalance.enable=true
,开启分区自动平衡
当 partition 1 的 leader,就是 broker.id = 1 的节点挂掉后,那么 leader 0 或 leader 2 成为 partition 1 的 leader,那么 leader 0 或 leader 2 会管理两个 partition 的读写,性能会下 降,当 leader 1 重新启动后,如果开启了 leader 均衡机制,那么 leader 1 会重新成为 partition 1 的 leader,降低 leader 0 或 leader 2 的负载
Kafka 首选领导者Preferred Leader
Kafka认为leader分区副本最初的分配(每个节点都处于活跃状态)是均衡的。这些被最初选中的分区副本就是所谓的首选领导者(preferred leaders)。
在 broker 挂掉之后,分区 leader 会变更,久而久之就会变得不均衡,Kafka 默认序号最小的副本为 Preferred leader,在 broker 重启回来后,Kafka 会重新调整分区的 Preferred leader 成为 leader,Preferred leader 选举分为手动选举和自动选举,涉及参数 auto.leader.rebalance.enable,还有个默认允许 10% 不均衡策略等等。
KafkaConsumer API 提交偏移量方式
自动提交
最简单的方式就是让消费者自动提交偏移量。如果 enable.auto.commit 被设置为true,那么每过 5s,消费者会自动把从 poll() 方法轮询到的最大偏移量提交上去。提交时间间隔由 auto.commit.interval.ms 控制,默认是 5s。
使用自动提交是存在隐患的,假设我们使用默认的 5s 提交时间间隔,在最近一次提交之后的 3s 发生了再均衡,再均衡之后,消费者从最后一次提交的偏移量位置开始读取消息。这个时候偏移量已经落后了 3s ,所以在这 3s 内到达的消息会被重复处理。
可以通过修改提交时间间隔来更频繁地提交偏移量,减小可能出现重复消息的时间窗,不过这种情况是无法完全避免的。基于这个原因,Kafka 也提供了手动提交偏移量的 API,使得用户可以更为灵活的提交偏移量。
手动提交当前偏移量
把 auto.commit.offset 设置为 false,可以让应用程序决定何时提交偏移量。使用 commitSync() | commitAsync() 提交偏移量。这个 API 会提交由 poll() 方法返回的最新偏移量,提交成功后马上返回,如果提交失败就抛出异常。
commitSync() 将会同步提交由 poll() 返回的最新偏移量,如果处理完所有记录后要确保调用了 commitSync(),否则还是会有丢失消息的风险,如果发生了在均衡,从最近一批消息到发生在均衡之间的所有消息都将被重复处理;异步提交 commitAsync() 与同步提交 commitSync() 最大的区别在于异步提交不会进行重试,同步提交会一致进行重试。 一般情况下,针对偶尔出现的提交失败,不进行重试不会有太大的问题,因为如果提交失败是因为临时问题导致的,那么后续的提交总会有成功的。但是如果在关闭消费者或再均衡前的最后一次提交,就要确保提交成功。 因此,在消费者关闭之前一般会组合使用commitAsync和commitSync提交偏移量。
同步和异步组合提交:
try {
while (true) {
ConsumerRecords<String, String> records = consumer.poll(1000);
for (ConsumerRecord<String, String> record : records) {
System.out.println("value = " + record.value() + ", topic = " + record.topic() + ", partition = "
+ record.partition() + ", offset = " + record.offset());
}
// 如果一切正常,我们使用 commitAsync() 方法来提交
// 这样速度更快,而且即使这次提交失败,下一次提交很可能会成功
consumer.commitAsync();
}
}catch (Exception e) {
e.printStackTrace();
}finally {
try {
// 使用 commitSync() 方法会一直重试,直到提交成功或发生无法恢复的错误
// 确保关闭消费者之前成功提交了偏移量
consumer.commitSync();
}finally {
consumer.close();
}
}
手动提交特定的偏移量
消费者API允许调用 commitSync() 和 commitAsync() 方法时传入希望提交的 partition 和 offset 的 map,即提交特定的偏移量。
Kafka 重新加载消费场景
场景一:Kafka上在实时被灌入数据,但kafka上已经积累了两天的数据,如何从最新的offset开始消费?
- 将group.id换成新的名字(相当于加入新的消费组)
properties.setProperty(“auto.offset.reset”, "latest”)
由于latest是默认值,所以也可以不用设置第2步
场景二:kafka在实时在灌入数据,kafka上已经积累了两天的数据,如何从两天前最开始的位置消费?
- 将group.id换成新的名字
properties.setProperty(“auto.offset.reset”, "earliest”)
场景三:不更改group.id,只是添加了properties.setProperty("auto.offset.reset", "earliest”)
,consumer会从两天前最开始的位置消费吗?
不会,只要不更改消费组,只会从上次消费结束的地方继续消费
场景四:不更改group.id,只是添加了properties.setProperty("auto.offset.reset", "latest”)
,consumer会从距离现在最近的位置消费吗?
不会,只要不更改消费组,只会从上次消费结束的地方继续消费
消费者群组对应的分区offset存储在哪里
最新版本保存在kafka中,对应的主题是_consumer_offsets。老版本是在zookeeper中。
无Zookeeper,还能做哪些操作?
离开了Zookeeper, Kafka 不能对Topic 进行新增操作, 但是仍然可以produce 和consume 消息.
如果设置的副本数大于Broker会怎么样?
假如当前我们搭建了三个Broker的集群,但是我此时指定4个Replica时,会出现org.apache.kafka.common.errors.InvalidReplicationFactorException: Replication factor: 4 larger than available brokers: 3
异常
集群中各Broker的元数据metadata cache什么时候更新?
集群中新增加的broker是如何获取这些cache,并且其他broker是如何知晓它的?
当有新broker启动时,它会在Zookeeper中进行注册,此时监听Zookeeper的controller就会立即感知这台新broker的加入,此时controller会更新它自己的缓存(注意:这是controller自己的缓存,不是本文讨论的metadata cache)把这台broker加入到当前broker列表中,之后它会发送UpdateMetadata请求给集群中所有的broker(也包括那台新加入的broker)让它们去更新metadata cache。
一旦这些broker更新cache完成,它们就知道了这台新broker的存在,同时由于新broker也更新了cache,故现在它也有了集群所有的状态信息。
ISR的伸缩指什么
ISR的伸缩指:leader副本负责维护和跟踪 ISR 集合中所有follower副本的滞后状态,当follower副本落后太多或失效时,leader副本会把它从 ISR 集合中剔除。如果 OSR 集合中所有follower副本“追上”了leader副本,那么leader副本会把它从 OSR 集合转移至 ISR 集合。
默认情况下,当leader副本发生故障时,只有在 ISR 集合中的follower副本才有资格被选举为新的leader,而在 OSR 集合中的副本则没有任何机会(不过这个可以通过配置来改变)。
消费者提交offset时提交的是最新消息的offset还是offset+1?
当前消费者需要提交的消费位移是offset+1
哪些情形会造成重复消费
消费端 主要原因是:数据已经被消费但是offset没有提交。
- 消费者端手动提交 :如果先消费消息,再更新offset位置,导致消息重复消费。
- 消费者端自动提交 :设置offset为自动提交,关闭kafka时,如果在close之前,调用 consumer.unsubscribe() 则有可能部分offset没提交,下次重启会重复消费。
- Rebalance :一个consumer正在消费一个分区的一条消息,还没有消费完,发生了rebalance(加入了一个consumer),从而导致这条消息没有消费成功,rebalance后,另一个consumer又把这条消息消费一遍。
生产者端 :生产者因为业务问题导致的宕机,在重启之后可能数据会重发
哪些情形会造成丢失数据
- 消费者端自动提交 :设置offset为自动定时提交,当offset被自动定时提交时,数据还在内存中未处理,此时刚好把线程kill掉,那么offset已经提交,但是数据未处理,导致这部分内存中的数据丢失。
- 消费者端手动提交 :先提交位移,但是消息还没消费完就宕机了,造成了消息没有被消费。自动位移提交同理
- acks未设置为all :如果在broker还没把消息同步到其他broker的时候宕机了,那么消息将会丢失
提高生产者的吞吐量
- 修改batch.size的大小。用来设置一个批次可占用的内存大小,batch.size默认大小为16k,提高批次大小可以一定程度提高吞吐量。这就好比用小卡车拉货物和用大卡车拉货物,小卡车单位时间内不能一次拉完货物,那么来回就需要消耗额外时间,而大卡车一次性把货物拉走,那么就节省了来回的时间。
- 修改linger.ms的时间,用来设置 Producer 在发送批次前的等待时间,默认为0,即直接发送数据,一般设置为5-100ms,通过设置延迟时间一次性可以发送更多的数据。
- 修改compression.type的数据压缩类型,默认snappy。根据不同的业务场景选择不同的数据压缩方法,提高数据压缩率。kafka提供的压缩类型有:gzip,snappy,lz4,zstd。
- 修改RecordAccumulator缓冲区的大小。默认32m,增加缓冲区大小可以那么每个batch.size的值就更大,每次发送的数据更多。
Producer 数据乱序
kafka的producer客户端在向broker发送数据时并不是直接将数据发送出去,而是将数据先缓存到本地的双端缓存队列中,sender线程会不断地检测缓存队列中地数据,若队列中地数据达到了设置的(batch.size)值地容量或者达到一定的时间(linger.ms),sender就会创建一个InFlightRequests线程,该线程负责将数据发送到broker上。kafka 默认的InFlightRequests是5个,InFlightRequests线程发送数据后不需要broker的应答就可以发下一个数据,因此最多可以一起发5个请求。比如,需要发送0,1,2,3,4这五个数据依次被发送出去,但是2这个数据没有接收成功,客户端则重新2的数据,此时broker接收到的数据的顺序就变为了01342,而非01234。
Producer 数据有序
- kafka在1.x之前保证数据单分区有序,需要将
InFlightRequests
线程数设置为1个(max.in.flight.requests.per.connection=1
)。当线程数为1时就能保证broker收到数据确认后再发下一条数据。 - kafka在1.x以后在未开启幂等性的情况下,处理流程跟1.x以前的版本一样。
- 1.x以后在开启幂等性的情况下,可以将
max.in.flight.requests.per.connection
设置为小于等于5。原因是,Kafka服务端会缓存最近发过来的元数据,等缓存满了5个后就会对这些元数据进行排序,这样就可以保证数据有序了。
幂等性只能保证单会话内数据有序,还是使用单分区有序方式好
MQ事务消息是如何实现的?
这里的实现方式类似于“两阶段提交”,在MySQL的事务中也是处理的。
Kafka 如何保证高可用
高可用体现在集群的高可用和数据的高可用两个方面:
- 集群的高可用体现Leader节点在Down之后可以重新从Follower节点中重新选择;
- 数据的高可用体现在“多副本机制”、“ISR列表”、“HW”、“Leader Epoch”
- 通过配置acks=all实现多个副本都复制成功的时候才算数据接收成功;
- ISR列表指的是达到数据同步标准的Follower节点;
- HW是High Water Mark 高水位,防止消费者读到未同步的数据;
- Leader Epoch 是解决的HW错位导致的数据不一致的问题:
- 1,Epoch:是一个单调递增的版本号;
- 2,Start Offset:是我们下一次需要从哪开始访问的位移;
Kafka是否支持事务?
在0.11之后是支持事务的。
Kafka Producer 的执行过程
- Producer生产消息
- 从Zookeeper找到Partition的Leader(老版本的方式,新版本可以通过任意一个broker获取元数据信息)
- 推送消息
- 通过ISR列表通知给Follower
- Follower从Leader拉取消息,并发送ack
- Leader收到所有副本的ack,更新Offset,并向Producer发送ack,表示消息写入成功。
如何提升 Producer 的性能?
批量,异步,压缩
Group下Consumer的数量大于partition的数量
多余的 consumer 将处于无用状态,不消费数据。
Kafka Consumer 是否是线程安全的?
不安全;所以在Consumer端采用的是 单线程消费,多线程处理
kafka消费消息时支持三种模式
at most once模式 最多一次。保证每一条消息commit成功之后,再进行消费处理。消息可能会丢失,但不会重复。
at least once模式 至少一次。保证每一条消息处理成功之后,再进行commit。消息不会丢失,但可能会重复。
exactly once模式 精确传递一次。将offset作为唯一id与消息同时处理,并且保证处理的原子性。消息只会处理一次,不丢失也不会重复。但这种方式很难做到。
kafka默认的模式是at least once,但这种模式可能会产生重复消费的问题,所以我们的业务逻辑必须做幂等设计。
而我们的业务场景保存数据时使用了INSERT INTO ...ON DUPLICATE KEY UPDATE语法,不存在时插入,存在时更新,是天然支持幂等性的。