11.LEO、LSO、AR、ISR、HW 都表示什么含义?
在我看来,这纯属无聊的炫技。试问我不知道又能怎样呢?!不过既然问到了,我们就统一说一说。
- LEO:Log End Offset。日志末端位移值或末端偏移量,表示日志下一条待插入消息的位移值。举个例子,如果日志有 10 条消息,位移值从 0 开始,那么,第 10 条消息的位移值就是 9。此时,LEO = 10。
- LSO:Log Stable Offset。这是 Kafka 事务的概念。如果你没有使用到事务,那么这个值不存在(其实也不是不存在,只是设置成一个无意义的值)。该值控制了事务型消费者能够看到的消息范围。它经常与 Log Start Offset,即日志起始位移值相混淆,因为有些人将后者缩写成 LSO,这是不对的。在 Kafka 中,LSO 就是指代 Log Stable Offset。
- AR:Assigned Replicas。AR 是主题被创建后,分区创建时被分配的副本集合,副本个数由副本因子决定。ISR:In-Sync Replicas。Kafka 中特别重要的概念,指代的是 AR 中那些与 Leader 保持同步的副本集合。在 AR 中的副本可能不在 ISR 中,但 Leader 副本天然就包含在 ISR 中。关于 ISR,还有一个常见的面试题目是如何判断副本是否应该属于 ISR。目前的判断依据是:Follower 副本的 LEO 落后 Leader LEO 的时间,是否超过了 Broker 端参数 replica.lag.time.max.ms 值。如果超过了,副本就会被从 ISR 中移除。
- HW:高水位值(High watermark)。这是控制消费者可读取消息范围的重要字段。一个普通消费者只能“看到”Leader 副本上介于 Log Start Offset 和 HW(不含)之间的所有消息。水位以上的消息是对消费者不可见的。关于 HW,问法有很多,我能想到的最高级的问法,就是让你完整地梳理下 Follower 副本拉取 Leader 副本、执行同步机制的详细步骤。这就是我们的第 20 道题的题目,一会儿我会给出答案和解析。
12.Kafka 能否手动删除消息?
首先 Kafka 是支持手动删除消息的, 当然它本身提供了消息留存策略,能够自动删除过期的消息。
Kafka 将消息存储到磁盘中,随着写入数据不断增加,磁盘占用空间越来越大,为了控制占用空间就需要对消息做一定的清理操作。Kafka 存储日志结构分析中每一个分区副本(Replica)都对应一个 Log,而 Log 又可以分为多个日志分段(LogSegment),这样就便于 Kafka 对日志的清理操作。
1)**普通消息:**我们可以使用 Kafka-delete-records 命令或者通过程序调用 Admin.deleteRecords 方法来删除消息。两者底层都是调用 Admin 的 deleteRecords 的方法,通过将分区的 LEO 值抬高来间接删除消息。
2)**设置key且参数 cleanup.policy=delete/campact 的消息:**可以依靠 Log Cleaner 组件提供的功能删除该 Key 的消息。
**日志删除(Log Retention):**按照一定的保留策略直接删除不符合条件的日志分段(LogSegment)。
**日志压缩(Log Compaction):**针对每个消息的key进行整合,对于有相同key的不同value值,只保留最后一个版本。
01
日志删除
Kafka 的日志管理器(LogManager)中有一个专门的日志清理任务通过周期性检测和删除不符合条件的日志分段文件(LogSegment),这里我们可以通过设置 Kafka Broker 端的参数「** log.retention.check.interval.ms**」,默认值为300000,即5分钟。
在 Kafka 中一共有3种保留策略:
基于时间策略
日志删除任务会周期检查当前日志文件中是否有保留时间超过设定的阈值**(retentionMs) 来寻找可删除的日志段文件集合(deletableSegments)**。
其中 **retentionMs **可以通过 Kafka Broker 端的这几个参数的大小判断的
log.retention.ms > log.retention.minutes > log.retention.hours优先级来设置,默认情况只会配置 log.retention.hours 参数,值为168即为7天。
**
这里需要注意:删除过期的日志段文件,并不是简单的根据该日志段文件的修改时间计算的,而是要根据该日志段中最大的时间戳 largestTimeStamp 来计算的,首先要查询该日志分段所对应的时间戳索引文件,查找该时间戳索引文件的最后一条索引数据,如果时间戳值大于0,则取值,否则才会使用最近修改时间(lastModifiedTime)。
【删除步骤】:
-
首先从 Log 对象所维护的日志段的跳跃表中移除要删除的日志段,用来确保已经没有线程来读取这些日志段。
-
将日志段所对应的所有文件,包括索引文件都添加上“.deleted”的后缀。
-
最后交给一个以“delete-file”命名的延迟任务来删除这些以“ .deleted ”为后缀的文件。默认1分钟执行一次, 可以通过 file.delete.delay.ms 来配置。
基于日志大小策略
日志删除任务会周期检查当前日志大小是否超过设定的阈值**(retentionSize)** 来寻找可删除的日志段文件集合**(deletableSegments)**。
其中retentionSize这里我们可以通过 Kafka Broker 端的参数log.retention.bytes 来设置, 默认值为-1,即无穷大。
这里需要注意的是 log.retention.bytes 设置的是Log中所有日志文件的大小,而不是单个日志段的大小。单个日志段可以通过参数 log.segment.bytes 来设置,默认大小为1G。
【删除步骤】:
-
首先计算日志文件的总大小Size和 retentionSize 的差值,即需要删除的日志总大小。
-
然后从日志文件中的第一个日志段开始进行查找可删除的日志段的文件集合(deletableSegments)
-
找到后就可以进行删除操作了。
基于日志起始偏移量
该策略判断依据是日志段的下一个日志段的起始偏移量 baseOffset 是否小于等于 logStartOffset,如果是,则可以删除此日志分段。
【如下图所示 删除步骤】:
-
首先从头开始遍历每个日志段,日志段 1 的下一个日志分段的起始偏移量为20,小于 logStartOffset 的大小,将日志段1加入deletableSegments。
-
日志段2的下一个日志偏移量的起始偏移量为35,也小于 logStartOffset 的大小,将日志分段2页加入 deletableSegments。
-
日志段3的下一个日志偏移量的起始偏移量为50,也小于 logStartOffset 的大小,将日志分段3页加入 deletableSegments。
-
日志段4的下一个日志偏移量通过对比后,在 logStartOffset 的右侧,那么从日志段4开始的所有日志段都不会加入 deletableSegments。
-
待收集完所有的可删除的日志集合后就可以直接删除了。
02
日志压缩
**日志压缩 Log Compaction 对于有相同key的不同value值,只保留最后一个版本。**如果应用只关心 key 对应的最新 value 值,则可以开启 Kafka 相应的日志清理功能,Kafka 会定期将相同 key 的消息进行合并,只保留最新的 value 值。
Log Compaction 可以类比 Redis 中的 RDB 的持久化模式。我们可以想象下,如果每次消息变更都存 Kafka,在某一时刻, Kafka 异常崩溃后,如果想快速恢复,可以直接使用日志压缩策略, 这样在恢复的时候只需要恢复最新的数据即可,这样可以加快恢复速度。
13.__consumer_offsets 是做什么用的?
这是一个内部主题,公开的官网资料很少涉及到。因此,我认为,此题属于面试官炫技一类的题目。你要小心这里的考点:该主题有 3 个重要的知识点,你一定要全部答出来,才会显得对这块知识非常熟悉。
- 它是一个内部主题,无需手动干预,由 Kafka 自行管理。当然,我们可以创建该主题。
- 它的主要作用是负责注册消费者以及保存位移值。可能你对保存位移值的功能很熟悉,但其实该主题也是保存消费者元数据的地方。千万记得把这一点也回答上。另外,这里的消费者泛指消费者组和独立消费者,而不仅仅是消费者组。
- Kafka 的 GroupCoordinator 组件提供对该主题完整的管理功能,包括该主题的创建、写入、读取和 Leader 维护等。
14. 生产者发送消息时如何选择分区的?
15.Kafka 的哪些场景中使用了零拷贝(Zero Copy)?
Zero Copy 是特别容易被问到的高阶题目。在 Kafka 中,体现 Zero Copy 使用场景的地方有两处:基于 mmap 的索引和日志文件读写所用的 TransportLayer。
先说第一个。索引都是基于 MappedByteBuffer 的,也就是让用户态和内核态共享内核态的数据缓冲区,此时,数据不需要复制到用户态空间。不过,mmap 虽然避免了不必要的拷贝,但不一定就能保证很高的性能。在不同的操作系统下,mmap 的创建和销毁成本可能是不一样的。很高的创建和销毁开销会抵消 Zero Copy 带来的性能优势。由于这种不确定性,在 Kafka 中,只有索引应用了 mmap,最核心的日志并未使用 mmap 机制。
再说第二个。TransportLayer 是 Kafka 传输层的接口。它的某个实现类使用了 FileChannel 的 transferTo 方法。该方法底层使用 sendfile 实现了 Zero Copy。对 Kafka 而言,如果 I/O 通道使用普通的 PLAINTEXT,那么,Kafka 就可以利用 Zero Copy 特性,直接将页缓存中的数据发送到网卡的 Buffer 中,避免中间的多次拷贝。相反,如果 I/O 通道启用了 SSL,那么,Kafka 便无法利用 Zero Copy 特性了。
16.Kafka 为什么不支持读写分离?
在 Kafka 中,生产者写入消息、消费者读取消息的操作都是与 leader 副本进行交互的,从 而实现的是一种主写主读的生产消费模型。 Kafka 并不支持主写从读,因为主写从读有 2 个很明显的缺点:
1.数据一致性问题: 数据从主节点转到从节点必然会有一个延时的时间窗口,这个时间 窗口会导致主从节点之间的数据不一致。某一时刻,在主节点和从节点中 A 数据的值都为 X,之后将主节点中 A 的值修改为 Y那么在这个变更通知到从节点之前,应用读取从节点中的 A 数据的值并不为最新的 Y,由此便产生了数据不一致的问题。读写分离适用于那种读负载很大,而写操作相对不频繁的场景,可 Kafka 不属于这样的场景。
2.延时问题: 类似 Redis 这种组件,数据从写入主节点到同步至从节点中的过程需要经历 网络一主节点内存一网络一从节点内存 这几个阶段,整个过程会耗费一定的时间。而在 Kafka 中,主从同步会比 Redis 更加耗时,它需要经历 网络一主节点内存一主节点磁盘一网络一从节 点内存一从节点磁盘 这几个阶段。对延时敏感的应用而言,主写从读的功能并不太适用,
而 kafka 的主写主读的优点就很多了
1.可以简化代码的实现逻辑,减少出错的可能
2.将负载粒度细化均摊,与主写从读相比,不仅负载效能更好,而且对用户可控;
3.没有延时的影响;
4.在副本稳定的情况下,不会出现数据不一致的情况
17. 如何调优 Kafka?
回答任何调优问题的第一步,就是确定优化目标,并且定量给出目标!这点特别重要。对于 Kafka 而言,常见的优化目标是吞吐量、延时、持久性和可用性。每一个方向的优化思路都是不同的,甚至是相反的。
确定了目标之后,还要明确优化的维度。有些调优属于通用的优化思路,比如对操作系统、JVM 等的优化;有些则是有针对性的,比如要优化 Kafka 的 TPS。我们需要从 3 个方向去考虑。
- Producer 端:增加 batch.size、linger.ms,启用压缩,关闭重试等。
- Broker 端:增加 num.replica.fetchers,提升 Follower 同步 TPS,避免 Broker Full GC 等。
- Consumer:增加 fetch.min.bytes 等
18.Controller 发生网络分区(Network Partitioning)时,Kafka 会怎么样?
这道题目能够诱发我们对分布式系统设计、CAP 理论、一致性等多方面的思考。不过,针对故障定位和分析的这类问题,我建议你首先言明“实用至上”的观点,即不论怎么进行理论分析,永远都要以实际结果为准。一旦发生 Controller 网络分区,那么,第一要务就是查看集群是否出现“脑裂”,即同时出现两个甚至是多个 Controller 组件。这可以根据 Broker 端监控指标 ActiveControllerCount 来判断。
现在,我们分析下,一旦出现这种情况,Kafka 会怎么样。
由于 Controller 会给 Broker 发送 3 类请求,即 LeaderAndIsrRequest、StopReplicaRequest 和 UpdateMetadataRequest,因此,一旦出现网络分区,这些请求将不能顺利到达 Broker 端。这将影响主题的创建、修改、删除操作的信息同步,表现为集群仿佛僵住了一样,无法感知到后面的所有操作。因此,网络分区通常都是非常严重的问题,要赶快修复。
19.Kafka 3.X 「2.8版本开始」为什么移除 Zookeeper 的依赖的原因有以下2点:
1)**集群运维层面:**Kafka 本身就是一个分布式系统,如果还需要重度依赖 Zookeeper,集群运维成本和系统复杂度都很高。
2)**集群性能层面:**Zookeeper 架构设计并不适合这种高频的读写更新操作, 由于之前的提交位移的操作都是保存在 Zookeeper 里面的,这样的话会严重影响 Zookeeper 集群的性能。
20. 简述 Follower 副本消息同步的完整流程
首先,Follower 发送 FETCH 请求给 Leader。接着,Leader 会读取底层日志文件中的消息数据,再更新它内存中的 Follower 副本的 LEO 值,更新为 FETCH 请求中的 fetchOffset 值。最后,尝试更新分区高水位值。Follower 接收到 FETCH 响应之后,会把消息写入到底层日志,接着更新 LEO 和 HW 值。
Leader 和 Follower 的 HW 值更新时机是不同的,Follower 的 HW 更新永远落后于 Leader 的 HW。这种时间上的错配是造成各种不一致的原因。
参考资料
1.华仔聊技术
2.五分钟学大数据
3.胡夕 《Kafka 核心源码解读》