# 精尽 Kafka 面试题「最新更新时间:2024-02」 以下面试题,基于网络整理,和自己编辑。具体参考的文章,会在文末给出所有的链接。 如果胖友有自己的疑问,欢迎在星球提问,我们一起整理吊吊的 Kafka 面试题的大保健。 而题目的难度,艿艿尽量按照从容易到困难的顺序,逐步下去。 另外,这个面试题是建立在胖友看过 [《精尽【消息队列 】面试题》](http://svip.iocoder.cn/MQ/Interview) 。 如果可能的话,推荐胖友先阅读了 [《Kafka 权威指南》](https://u.jd.com/jJpbF8) ,更加系统可靠。 ## Apache Kafka 是什么? Kafka 是基于**发布与订阅**的**消息系统**。它最初由 LinkedIn 公司开发,之后成为 Apache 项目的一部分。Kafka 是一个分布式的,可分区的,冗余备份的持久性的日志服务。它主要用于处理活跃的流式数据。 在大数据系统中,常常会碰到一个问题,整个大数据是由各个子系统组成,数据需要在各个子系统中高性能、低延迟的不停流转。传统的企业消息系统并不是非常适合大规模的数据处理。为了同时搞定在线应用(消息)和离线应用(数据文件、日志),Kafka 就出现了。Kafka 可以起到两个作用: - 降低系统组网复杂度。 - 降低编程复杂度,各个子系统不在是相互协商接口,各个子系统类似插口插在插座上,Kafka 承担高速**数据总线**的作用。 🦅 **Kafka 的主要特点?** - 1、同时为发布和订阅提供高吞吐量。据了解,Kafka 每秒可以生产约 25 万消息(50MB),每秒处理 55 万消息(110MB)。 - 2、可进行持久化操作。将消息持久化到磁盘,因此可用于批量消费,例如 ETL ,以及实时应用程序。通过将数据持久化到硬盘,以及replication ,可以防止数据丢失。 - 3、分布式系统,易于向外扩展。所有的 Producer、Broker 和Consumer 都会有多个,均为分布式的。并且,无需停机即可扩展机器。 - 4、消息被处理的状态是在 Consumer 端维护,而不是由 Broker 端维护。当失败时,能自动平衡。 > 这段是从网络上找来的。感觉想要表达的意思是 > > - 消息是否被处理完成,是通过 Consumer 提交消费进度给 Broker ,而不是 Broker 消息被 Consumer 拉取后,就标记为已消费。 > - 当 Consumer 异常崩溃时,可以重新分配消息分区到其它的 Consumer 们,然后继续消费。 - 5、支持 online 和 offline 的场景。 🦅 **聊聊 Kafka 的设计要点?** 1)吞吐量 高吞吐是 Kafka 需要实现的核心目标之一,为此 kafka 做了以下一些设计: - 1、数据磁盘持久化:消息不在内存中 Cache ,直接写入到磁盘,充分利用磁盘的顺序读写性能。 > 直接使用 Linux 文件系统的 Cache ,来高效缓存数据。 - 2、zero-copy:减少 IO 操作步骤 > 采用 Linux Zero-Copy 提高发送性能。 > > - 传统的数据发送需要发送 4 次上下文切换。 > - 采用 sendfile 系统调用之后,数据直接在内核态交换,系统上下文切换减少为 2 次。根据测试结果,可以提高 60% 的数据发送性能。Zero-Copy 详细的技术细节可以参考 [《Efficient data transfer through zero copy》](https://developer.ibm.com/articles/j-zerocopy/) 文章。 - 3、数据批量发送 - 4、数据压缩 - 5、Topic 划分为多个 Partition ,提高并行度。 > 数据在磁盘上存取代价为 `O(1)`。 > > - Kafka 以 Topic 来进行消息管理,每个 Topic 包含多个 Partition ,每个 Partition 对应一个逻辑 log ,有多个 segment 文件组成。 > - 每个 segment 中存储多条消息(见下图),消息 id 由其逻辑位置决定,即从消息 id 可直接定位到消息的存储位置,避免 id 到位置的额外映射。 > - 每个 Partition 在内存中对应一个 index ,记录每个 segment 中的第一条消息偏移。 > > 发布者发到某个 Topic 的消息会被均匀的分布到多个 Partition 上(随机或根据用户指定的回调函数进行分布),Broker 收到发布消息往对应 Partition 的最后一个 segment 上添加该消息。 > 当某个 segment上 的消息条数达到配置值或消息发布时间超过阈值时,segment上 的消息会被 flush 到磁盘,只有 flush 到磁盘上的消息订阅者才能订阅到,segment 达到一定的大小后将不会再往该 segment 写数据,Broker 会创建新的 segment 文件。 2)负载均衡 - 1、Producer 根据用户指定的算法,将消息发送到指定的 Partition 中。 - 2、Topic 存在多个 Partition ,每个 Partition 有自己的replica ,每个 replica 分布在不同的 Broker 节点上。多个Partition 需要选取出 Leader partition ,Leader Partition 负责读写,并由 Zookeeper 负责 fail over 。 - 3、相同 Topic 的多个 Partition 会分配给不同的 Consumer 进行拉取消息,进行消费。 3)拉取系统 由于 Kafka Broker 会持久化数据,Broker 没有内存压力,因此, Consumer 非常适合采取 pull 的方式消费数据,具有以下几点好处: - 1、简化 Kafka 设计。 - 2、Consumer 根据消费能力自主控制消息拉取速度。 - 3、Consumer 根据自身情况自主选择消费模式,例如批量,重复消费,从尾端开始消费等。 4)可扩展性 > 通过 Zookeeper 管理 Broker 与 Consumer 的动态加入与离开。 - 当需要增加 Broker 节点时,新增的 Broker 会向 Zookeeper 注册,而 Producer 及 Consumer 会根据注册在 Zookeeper 上的 watcher 感知这些变化,并及时作出调整。 - 当新增和删除 Consumer 节点时,相同 Topic 的多个 Partition 会分配给剩余的 Consumer 们。 另外,推荐阅读 [《为什么 Kafka 这么快?》](https://mp.weixin.qq.com/s/pzVS7r3QaQPFwob-fY8b4A) 文章,写的更加细致。 ## Kafka 的架构是怎么样的? [![Kafka 架构图](11-Kafka 面试题.assets/ac883ce247c1ff31c7cd4244392dcaed.png)](http://static.iocoder.cn/ac883ce247c1ff31c7cd4244392dcaed)Kafka 架构图 Kafka 的整体架构非常简单,是分布式架构,Producer、Broker 和Consumer 都可以有多个。 - Producer,Consumer 实现 Kafka 注册的接口。 - 数据从 Producer 发送到 Broker 中,Broker 承担一个中间缓存和分发的作用。 - Broker 分发注册到系统中的 Consumer。Broker 的作用类似于缓存,即活跃的数据和离线处理系统之间的缓存。 - 客户端和服务器端的通信,是基于简单,高性能,且与编程语言无关的 TCP 协议。 几个重要的基本概念: - Topic:特指 Kafka 处理的消息源(feeds of messages)的不同分类。 - Partition:Topic 物理上的分组(分区),一个 Topic 可以分为多个 Partition 。每个 Partition 都是一个有序的队列。Partition 中的每条消息都会被分配一个有序的 id(offset)。 > - replicas:Partition 的副本集,保障 Partition 的高可用。 > - leader:replicas 中的一个角色,Producer 和 Consumer 只跟 Leader 交互。 > - follower:replicas 中的一个角色,从 leader 中复制数据,作为副本,一旦 leader 挂掉,会从它的 followers 中选举出一个新的 leader 继续提供服务。 - Message:消息,是通信的基本单位,每个 Producer 可以向一个Topic(主题)发布一些消息。 - Producers:消息和数据生产者,向 Kafka 的一个 Topic 发布消息的过程,叫做 producers 。 - Consumers:消息和数据消费者,订阅 Topic ,并处理其发布的消息的过程,叫做 consumers 。 > Consumer group:每个 Consumer 都属于一个 Consumer group,每条消息只能被 Consumer group 中的一个 Consumer 消费,但可以被多个 Consumer group 消费。 - Broker:缓存代理,Kafka 集群中的一台或多台服务器统称为 broker 。 > Controller:Kafka 集群中,通过 Zookeeper 选举某个 Broker 作为 Controller ,用来进行 leader election 以及 各种 failover 。 - ZooKeeper:Kafka 通过 ZooKeeper 来存储集群的 Topic、Partition 等元信息等。 😈 单纯角色来说,Kafka 和 RocketMQ 是基本一致的。比较明显的差异是: > RocketMQ 从 Kafka 演化而来。 - 1、Kafka 使用 Zookeeper 作为命名服务;RocketMQ 自己实现了一个轻量级的 Namesrv 。 - 2、Kafka Broker 的每个分区都有一个首领分区;RocketMQ 每个分区的“首领”分区,都在 Broker Master 节点上。 > RocketMQ 没有首领分区一说,所以打上了引号。 - 3、Kafka Consumer 使用 poll 的方式拉取消息;RocketMQ Consumer 提供 poll 的方式的同时,封装了一个 push 的方式。 > RocketMQ 的 push 的方式,也是基于 poll 的方式的封装。 - … 当然还有其它 … 🦅 **Kafka 为什么要将 Topic 进行分区?** 正如我们在 [「聊聊 Kafka 的设计要点?」](https://svip.iocoder.cn/Kafka/Interview/#) 问题中所看到的,是为了负载均衡,从而能够水平拓展。 - Topic 只是逻辑概念,面向的是 Producer 和 Consumer ,而 Partition 则是物理概念。如果 Topic 不进行分区,而将 Topic 内的消息存储于一个 Broker,那么关于该 Topic 的所有读写请求都将由这一个 Broker 处理,吞吐量很容易陷入瓶颈,这显然是不符合高吞吐量应用场景的。 - 有了 Partition 概念以后,假设一个 Topic 被分为 10 个 Partitions ,Kafka 会根据一定的算法将 10 个 Partition 尽可能均匀的分布到不同的 Broker(服务器)上。 - 当 Producer 发布消息时,Producer 客户端可以采用 random、key-hash 及轮询等算法选定目标 Partition ,若不指定,Kafka 也将根据一定算法将其置于某一分区上。 - 当 Consumer 拉取消息时,Consumer 客户端可以采用 Range、轮询 等算法分配 Partition ,从而从不同的 Broker 拉取对应的 Partition 的 leader 分区。 所以,Partiton 机制可以极大的提高吞吐量,并且使得系统具备良好的水平扩展能力。 ## Kafka 的应用场景有哪些? [![Kafka 的应用场景](11-Kafka 面试题.assets/3636ff4bd554ee1dfcfb92448073b5b8.png)](http://static.iocoder.cn/3636ff4bd554ee1dfcfb92448073b5b8)Kafka 的应用场景 1)消息队列 比起大多数的消息系统来说,Kafka 有更好的吞吐量,内置的分区,冗余及容错性,这让 Kafka 成为了一个很好的大规模消息处理应用的解决方案。消息系统一般吞吐量相对较低,但是需要更小的端到端延时,并常常依赖于 Kafka 提供的强大的持久性保障。在这个领域,Kafka 足以媲美传统消息系统,如 ActiveMQ 或 RabbitMQ 。 2)行为跟踪 Kafka 的另一个应用场景,是跟踪用户浏览页面、搜索及其他行为,以发布订阅的模式实时记录到对应的 Topic 里。那么这些结果被订阅者拿到后,就可以做进一步的实时处理,或实时监控,或放到 Hadoop / 离线数据仓库里处理。 3)元信息监控 作为操作记录的监控模块来使用,即汇集记录一些操作信息,可以理解为运维性质的数据监控吧。 4)日志收集 日志收集方面,其实开源产品有很多,包括 Scribe、Apache Flume 。很多人使用 Kafka 代替日志聚合(log aggregation)。日志聚合一般来说是从服务器上收集日志文件,然后放到一个集中的位置(文件服务器或 HDFS)进行处理。 然而, Kafka 忽略掉文件的细节,将其更清晰地抽象成一个个日志或事件的消息流。这就让 Kafka 处理过程延迟更低,更容易支持多数据源和分布式数据处理。比起以日志为中心的系统比如 Scribe 或者 Flume 来说,Kafka 提供同样高效的性能和因为复制导致的更高的耐用性保证,以及更低的端到端延迟。 5)流处理 这个场景可能比较多,也很好理解。保存收集流数据,以提供之后对接的 Storm 或其他流式计算框架进行处理。很多用户会将那些从原始 Topic 来的数据进行阶段性处理,汇总,扩充或者以其他的方式转换到新的 Topic 下再继续后面的处理。 例如一个文章推荐的处理流程,可能是先从 RSS 数据源中抓取文章的内容,然后将其丢入一个叫做“文章”的 Topic 中。后续操作可能是需要对这个内容进行清理,比如回复正常数据或者删除重复数据,最后再将内容匹配的结果返还给用户。这就在一个独立的 Topic 之外,产生了一系列的实时数据处理的流程。Strom 和 Samza 是非常著名的实现这种类型数据转换的框架。 6)事件源 事件源,是一种应用程序设计的方式。该方式的状态转移被记录为按时间顺序排序的记录序列。Kafka 可以存储大量的日志数据,这使得它成为一个对这种方式的应用来说绝佳的后台。比如动态汇总(News feed)。 7)持久性日志(Commit Log) Kafka 可以为一种外部的持久性日志的分布式系统提供服务。这种日志可以在节点间备份数据,并为故障节点数据回复提供一种重新同步的机制。Kafka 中日志压缩功能为这种用法提供了条件。在这种用法中,Kafka 类似于 Apache BookKeeper 项目。 ## Kafka 消息发送和消费的简化流程是什么? [![Kafka 消息发送和消费](11-Kafka 面试题.assets/456f3adab70714c0a1b1fdbd8a686732.png)](http://static.iocoder.cn/456f3adab70714c0a1b1fdbd8a686732)Kafka 消息发送和消费 - 1、Producer ,根据指定的 partition 方法(round-robin、hash等),将消息发布到指定 Topic 的 Partition 里面。 - 2、Kafka 集群,接收到 Producer 发过来的消息后,将其持久化到硬盘,并保留消息指定时长(可配置),而不关注消息是否被消费。 - 3、Consumer ,从 Kafka 集群 pull 数据,并控制获取消息的 offset 。至于消费的进度,可手动或者自动提交给 Kafka 集群。 🦅 **1)Producer 发送消息** Producer 采用 push 模式将消息发布到 Broker,每条消息都被 append 到 Patition 中,属于顺序写磁盘(顺序写磁盘效率比随机写内存要高,保障 Kafka 吞吐率)。Producer 发送消息到 Broker 时,会根据分区算法选择将其存储到哪一个 Partition 。 其路由机制为: - 1、指定了 Partition ,则直接使用。 - 2、未指定 Partition 但指定 key ,通过对 key 进行 hash 选出一个 Partition 。 - 3、Partition 和 key 都未指定,使用轮询选出一个 Partition 。 写入流程: - 1、Producer 先从 ZooKeeper 的 `"/brokers/.../state"` 节点找到该 Partition 的 leader 。 > 注意噢,Producer 只和 Partition 的 leader 进行交互。 - 2、Producer 将消息发送给该 leader 。 - 3、leader 将消息写入本地 log 。 - 4、followers 从 leader pull 消息,写入本地 log 后 leader 发送 ACK 。 - 5、leader 收到所有 ISR 中的 replica 的 ACK 后,增加 HW(high watermark ,最后 commit 的 offset) 并向 Producer 发送 ACK 。 🦅 **2)Broker 存储消息** 物理上把 Topic 分成一个或多个 Patition,每个 Patition 物理上对应一个文件夹(该文件夹存储该 Patition 的所有消息和索引文件)。 🦅 **3)Consumer 消费消息** high-level Consumer API 提供了 consumer group 的语义,一个消息只能被 group 内的一个 Consumer 所消费,且 Consumer 消费消息时不关注 offset ,最后一个 offset 由 ZooKeeper 保存(下次消费时,该 group 中的 Consumer 将从 offset 记录的位置开始消费)。 注意: - 1、如果消费线程大于 Patition 数量,则有些线程将收不到消息。 - 2、如果 Patition 数量大于消费线程数,则有些线程多收到多个 Patition 的消息。 - 3、如果一个线程消费多个 Patition,则无法保证你收到的消息的顺序,而一个 Patition 内的消息是有序的。 Consumer 采用 pull 模式从 Broker 中读取数据。 - push 模式,很难适应消费速率不同的消费者,因为消息发送速率是由 Broker 决定的。它的目标是尽可能以最快速度传递消息,但是这样很容易造成 Consumer 来不及处理消息,典型的表现就是拒绝服务以及网络拥塞。而 pull 模式,则可以根据 Consumer 的消费能力以适当的速率消费消息。 - 对于 Kafka 而言,pull 模式更合适,它可简化 Broker 的设计,Consumer 可自主控制消费消息的速率,同时 Consumer 可以自己控制消费方式——即可批量消费也可逐条消费,同时还能选择不同的提交方式从而实现不同的传输语义。 🦅 **Kafka Producer 有哪些发送模式?** Kafka 的发送模式由 Producer 端的配置参数 `producer.type`来设置。 - 这个参数指定了在后台线程中消息的发送方式是同步的还是异步的,默认是同步的方式,即 `producer.type=sync` 。 - 如果设置成异步的模式,即 `producer.type=async` ,可以是 Producer 以 batch 的形式 push 数据,这样会极大的提高 Broker的性能,但是这样会增加丢失数据的风险。 - 如果需要确保消息的可靠性,必须要将 `producer.type`设置为 sync 。 对于异步模式,还有 4 个配套的参数,如下: [![参数](11-Kafka 面试题.assets/792a59a9b2b4f271c179e81cf4278322.jpeg)](http://static.iocoder.cn/792a59a9b2b4f271c179e81cf4278322)参数 - 以 batch 的方式推送数据可以极大的提高处理效率,Kafka Producer 可以将消息在内存中累计到一定数量后作为一个 batch 发送请求。batch 的数量大小可以通过 Producer 的参数(`batch.num.messages`)控制。通过增加 batch 的大小,可以减少网络请求和磁盘 IO 的次数,当然具体参数设置需要在效率和时效性方面做一个权衡。 - 在比较新的版本中还有 ``` batch.size ``` 这个参数。Producer 会尝试批量发送属于同一个 Partition 的消息以减少请求的数量. 这样可以提升客户端和服务端的性能。默认大小是 16348 byte (16k). - 发送到 Broker 的请求可以包含多个 batch ,每个 batch 的数据属于同一个 Partition 。 - 太小的 batch 会降低吞吐. 太大会浪费内存。 具体的代码实现,可以看看 [《芋道 Spring Boot 消息队列 Kafka 入门》](http://www.iocoder.cn/Spring-Boot/RabbitMQ/?vip)的[「3. 快速入门」](https://svip.iocoder.cn/Kafka/Interview/#)和[「4. 批量发送消息」](https://svip.iocoder.cn/Kafka/Interview/#)小节。 🦅 **Kafka Consumer 是否可以消费指定的分区消息?** Consumer 消费消息时,向 Broker 发出“fetch”请求去消费特定分区的消息,Consumer 指定消息在日志中的偏移量(offset),就可以消费从这个位置开始的消息,Consumer 拥有了 offset 的控制权,可以向后回滚去重新消费之前的消息,这是很有意义的。 🦅 **Kafka 的 high-level API 和 low-level API 的区别?** High Level API - 屏蔽了每个 Topic 的每个 Partition 的 offset 管理(自动读取Zookeeper 中该 Consumer group 的 last offset)、Broker 失败转移、以及增减 Partition 时 Consumer 时的负载均衡(Kafka 自动进行负载均衡)。 - 如果 Consumer 比 Partition 多,是一种浪费。一个 Partition 上是不允许并发的,所以 Consumer 数不要大于 Partition 数。 Low Level API > Low-level API 也就是 Simple Consumer API ,实际上非常复杂。 - API 控制更灵活,例如消息重复读取,消息 offset 跳读,Exactly Once 原语。 - API 更复杂,offset 不再透明,需要自己管理,Broker 自动失败转移需要处理,增加 Consumer、Partition、Broker 需要自己做负载均衡。 ## Kafka 的网络模型是怎么样的? Kafka 基于高吞吐率和效率考虑,并没有使用第三方网络框架,而且自己基于 Java NIO 封装的。 🦅 **1)KafkaClient ,单线程 Selector 模型。** [![KafkaClient](11-Kafka 面试题.assets/00e8ec59cf40c62db53b4d66dc45e17c.jpeg)](http://static.iocoder.cn/00e8ec59cf40c62db53b4d66dc45e17c)KafkaClient > 实际上,就是 NettyClient 的 NIO 方式。 - 单线程模式适用于并发链接数小,逻辑简单,数据量小。 - 在 Kafka 中,Consumer 和 Producer 都是使用的上面的单线程模式。这种模式不适合 Kafka 的服务端,在服务端中请求处理过程比较复杂,会造成线程阻塞,一旦出现后续请求就会无法处理,会造成大量请求超时,引起雪崩。而在服务器中应该充分利用多线程来处理执行逻辑。 🦅 **2)KafkaServer ,多线程 Selector 模型。** > KafkaServer ,指的是 Kafka Broker 。 [![KafkaServer](11-Kafka 面试题.assets/8493bc98876609462fba617d520d1b9a.jpeg)](http://static.iocoder.cn/8493bc98876609462fba617d520d1b9a)KafkaServer Broker 的内部处理流水线化,分为多个阶段来进行(SEDA),以提高吞吐量和性能,尽量避免 Thead 盲等待,以下为过程说明。 > 实际上,就是 NettyServer 的 NIO 方式。 - Accept Thread 负责与客户端建立连接链路,然后把 Socket 轮转交给Process Thread 。 > 相当于 Netty 的 Boss EventLoop 。 - Process Thread 负责接收请求和响应数据,Process Thread 每次基于 Selector 事件循环,首先从 Response Queue 读取响应数据,向客户端回复响应,然后接收到客户端请求后,读取数据放入 Request Queue 。 > 相当于 Netty 的 Worker EventLoop 。 - Work Thread 负责业务逻辑、IO 磁盘处理等,负责从 Request Queue 读取请求,并把处理结果放入 Response Queue 中,待 Process Thread 发送出去。 > 相当于业务线程池。 😈 实际上,艿艿的想法,如果自己实现 MQ ,完全可以直接使用 Netty 作为网络通信框架。包括,RocketMQ 就是如此实现的。 🦅 **解释如何提高远程用户的吞吐量?** 如果 Producer、Consumer 位于与 Broker 不同的数据中心,则可能需要调优套接口缓冲区大小,以对长网络延迟进行摊销。 ## Kafka 的数据存储模型是怎么样的? Kafka 每个 Topic 下面的所有消息都是以 Partition 的方式分布式的存储在多个节点上。同时在 Kafka 的机器上,每个 Partition 其实都会对应一个日志目录,在目录下面会对应多个日志分段(LogSegment)。 ``` MacBook-Pro-5:test-0 yunai$ ls 00000000000000000000.index 00000000000000000000.timeindex leader-epoch-checkpoint 00000000000000000000.log 00000000000000000004.snapshot ``` - Topic 为 `test` ,Partition 为 0 ,所以文件目录是 `test-0` 。 LogSegment 文件由两部分组成,分别为 `.index` 文件和 `.log` 文件,分别表示为 segment **索引**文件和**数据**文件。这两个文件的命令规则为:Partition 全局的第一个 segment 从 0 开始,后续每个 segment 文件名为上一个 segment 文件最后一条消息的 offset 值,数值大小为 64 位,20 位数字字符长度,没有数字用 0 填充,如下,假设有 1000 条消息,每个 LogSegment 大小为 100 ,下面展现了 900-1000 的 `.index` 和 `.log` 文件: [`.index` 和 `.log` 文件](https://user-gold-cdn.xitu.io/2018/7/24/164ca4f7e778be7c?imageView2/0/w/1280/h/960/format/jpeg/ignore-error/1) - 由于 Kafka 消息数据太大,如果全部建立索引,即占了空间又增加了耗时,所以 Kafka 选择了稀疏索引的方式(通过 `.index` 索引 `.log` 文件),这样的话索引可以直接进入内存,加快偏查询速度。 🦅 **简单介绍一下如何读取数据?** 如果我们要读取第 911 条数据。 - 首先第一步,找到它是属于哪一段的,根据二分法查找到他属于的文件,找到 `0000900.index` 和 `00000900.log` 之后。 - 然后,去 `.index` 中去查找 `(911-900) =11` 这个索引或者小于 11 最近的索引,在这里通过二分法我们找到了索引是 `[10,1367]` 。 > - 10 表示,第 10 条消息开始。 > - 1367 表示,在 `.log` 的第 1367 字节开始。 > > 😈 所以,本图的第 911 条的“1360”是错的,相比“1367” 反倒小了。 - 然后,我们通过这条索引的物理位置 1367 ,开始往后找,直到找到 911 条数据。 上面讲的是如果要找某个 offset 的流程,但是我们大多数时候并不需要查找某个 offset ,只需要按照顺序读即可。而在顺序读中,操作系统会对内存和磁盘之间添加 page cahe ,也就是我们平常见到的预读操作,所以我们的顺序读操作时速度很快。但是 Kafka 有个问题,如果分区过多,那么日志分段也会很多,写的时候由于是批量写,其实就会变成随机写了,随机 I/O 这个时候对性能影响很大。所以一般来说 Kafka 不能有太多的Partition 。针对这一点,RocketMQ 把所有的日志都写在一个文件里面,就能变成顺序写,通过一定优化,读也能接近于顺序读。 > 并且,截止到 RocketMQ4 版本,索引文件,对每个数据文件中的消息,都有对应的索引。这个是和 Kafka 的稀疏索引不太一样的地方。 更详尽的,推荐阅读 [《Kafka 之数据存储》](http://matt33.com/2016/03/08/kafka-store/) 文章。 🦅 **为什么不能以 Partition 作为存储单位?** 如果就以 Partition 为最小存储单位,可以想象,当 Kafka Producer 不断发送消息,必然会引起 Partition 文件的无限扩张,将对消息文件的维护以及已消费的消息的清理带来严重的影响,因此,需以 segment 为单位将 Partition 进一步细分。 每个 Partition(目录)相当于一个巨型文件,被平均分配到多个大小相等的 segment(段)数据文件中(每个 segment 文件中消息数量不一定相等),这种特性也方便 old segment 的删除,即方便已被消费的消息的清理,提高磁盘的利用率。每个 Partition 只需要支持顺序读写就行,segment 的文件生命周期由服务端配置参数(`log.segment.bytes`,`log.roll.{ms,hours}` 等若干参数)决定。 ## Kafka 的消息格式是怎么样的? message 中的物理结构为: [![message 物理结构](11-Kafka 面试题.assets/a034ab2df1d5b67520432355e0bbf95b.png)](http://static.iocoder.cn/a034ab2df1d5b67520432355e0bbf95b)message 物理结构 参数说明: | 关键字 | 解释说明 | | :------------------ | :----------------------------------------------------------- | | 8 byte offset | 在parition(分区)内的每条消息都有一个有序的id号,这个id号被称为偏移(offset),它可以唯一确定每条消息在parition(分区)内的位置。即offset表示partiion的第多少message | | 4 byte message size | message大小 | | 4 byte CRC32 | 用crc32校验message | | 1 byte “magic” | 表示本次发布Kafka服务程序协议版本号 | | 1 byte “attributes” | 表示为独立版本、或标识压缩类型、或编码类型 | | 4 byte key length | 表示key的长度,当key为-1时,K byte key字段不填 | | K byte key | 可选 | | value bytes payload | 表示实际消息数据 | 不过,这是早期 Kafka 的版本,最新版本的格式,推荐阅读如下两篇文章: - [《一文看懂 Kafka 消息格式的演变》](http://www.iocoder.cn/Kafka/message-format/?vip) - [《Kafka 消息格式中的变长字段(Varints)》](http://www.iocoder.cn/Kafka/varints/) 当然,看懂这个数据格式,基本也能知道消息的大体格式。 ## Kafka 的副本机制是怎么样的? Kafka 的副本机制,是多个 Broker 节点对其他节点的 Topic 分区的日志进行复制。当集群中的某个节点出现故障,访问故障节点的请求会被转移到其他正常节点(这一过程通常叫 Reblance),Kafka 每个主题的每个分区都有一个主副本以及 0 个或者多个副本,副本保持和主副本的数据同步,当主副本出故障时就会被替代。 [![副本机制](11-Kafka 面试题.assets/1f42986437f76c108007e86a51ae1287.jpeg)](http://static.iocoder.cn/1f42986437f76c108007e86a51ae1287)副本机制 > 注意哈,下面说的 Leader 指的是每个 Topic 的某个分区的 Leader ,而不是 Broker 集群中的【集群控制器】。 在 Kafka 中并不是所有的副本都能被拿来替代主副本,所以在 Kafka 的Leader 节点中维护着一个 ISR(In sync Replicas)集合,翻译过来也叫正在同步中集合,在这个集合中的需要满足两个条件: - 1、节点必须和 Zookeeper 保持连接。 - 2、在同步的过程中这个副本不能落后主副本太多。 另外还有个 AR(Assigned Replicas)用来标识副本的全集,OSR 用来表示由于落后被剔除的副本集合,所以公式如下: - ISR = Leader + 没有落后太多的副本。 - AR = OSR + ISR 。 这里先要说下两个名词:HW 和 LEO 。 - HW(高水位 HighWatermark),是 Consumer 能够看到的此 Partition 的位置。 - LEO(logEndOffset),是每个 Partition 的 log 最后一条 Message 的位置。 - HW 能保证 Leader 所在的 Broker 失效,该消息仍然可以从新选举的Leader 中获取,不会造成消息丢失。 当 Producer 向 Leader 发送数据时,可以通过`request.required.acks` 参数来设置数据可靠性的级别: - 1(默认):这意味着 Producer 在 ISR 中的 Leader 已成功收到的数据并得到确认后发送下一条 message 。如果 Leader 宕机了,则会丢失数据。 - 0:这意味着 Producer 无需等待来自 Broker 的确认而继续发送下一批消息。这种情况下数据传输效率最高,但是数据可靠性确是最低的。 - -1:Producer 需要等待 ISR 中的所有 Follower 都确认接收到数据后才算一次发送完成,可靠性最高。但是这样也不能保证数据不丢失,比如当 ISR 中只有 Leader 时(其他节点都和 Zookeeper 断开连接,或者都没追上),这样就变成了 `acks=1` 的情况。 关于这块详详细的内容,推荐阅读 - [《Kafka 数据可靠性深度解读》](https://blog.csdn.net/u013256816/article/details/71091774) 的 [「3 高可靠性存储分析」](https://svip.iocoder.cn/Kafka/Interview/#) 小节 - [《Kafka 集群内复制功能深入剖析》](https://www.jianshu.com/p/03d6a335237f) ## ZooKeeper 在 Kafka 中起到什么作用? 关于 ZooKeeper 是什么,不了解的胖友,直接去看 [《精尽 Zookeeper 面试题》](http://svip.iocoder.cn/Zookeeper/Interview/) 。 在基于 Kafka 的分布式消息队列中,ZooKeeper 的作用有: - 1、Broker 在 ZooKeeper 中的注册。 - 2、Topic 在 ZooKeeper 中的注册。 - 3、Consumer 在 ZooKeeper 中的注册。 - 4、Producer 负载均衡。 > 主要指的是,Producer 从 Zookeeper 拉取 Topic 元数据,从而能够将消息发送负载均衡到对应 Topic 的分区中。 - 5、Consumer 负载均衡。 - 6、记录消费进度 Offset 。 > Kafka 已推荐将 consumer 的 Offset 信息保存在 Kafka 内部的 Topic 中。 - 7、记录 Partition 与 Consumer 的关系。 其实,总结起来,就是两类功能: - Broker、Producer、Consumer 和 Zookeeper 的交互。 > 对应 1、2、3、5 。 - 相应的状态存储到 Zookeeper 中。 > 对应 4、6、7 。 详细的每一点,看 [《再谈基于 Kafka 和 ZooKeeper 的分布式消息队列原理》](https://gitbook.cn/books/5bc446269a9adf54c7ccb8bc/index.html) 的 [「Kafka 架构中 ZooKeeper 以怎样的形式存在?」](https://svip.iocoder.cn/Kafka/Interview/#) 小节。 ## Kafka 如何实现高可用? 在 [「Kafka 的架构是怎么样的?」](https://svip.iocoder.cn/Kafka/Interview/#) 问题中,已经基本回答了这个问题。 [Kafka 集群](https://segmentfault.com/img/bVbcmpm?w=776&h=436) - Zookeeper 部署 2N+1 节点,形成 Zookeeper 集群,保证高可用。 - Kafka Broker 部署集群。每个 Topic 的 Partition ,基于【副本机制】,在 Broker 集群中复制,形成 replica 副本,保证消息存储的可靠性。每个 replica 副本,都会选择出一个 leader 分区(Partition),提供给客户端(Producer 和 Consumer)进行读写。 - Kafka Producer 无需考虑集群,因为和业务服务部署在一起。Producer 从 Zookeeper 拉取到 Topic 的元数据后,选择对应的 Topic 的 leader 分区,进行消息发送写入。而 Broker 根据 Producer 的 `request.required.acks` 配置,是写入自己完成就响应给 Producer 成功,还是写入所有 Broker 完成再响应。这个,就是胖友自己对消息的可靠性的选择。 - Kafka Consumer 部署集群。每个 Consumer 分配其对应的 Topic Partition ,根据对应的分配策略。并且,Consumer 只从 leader 分区(Partition)拉取消息。另外,当有新的 Consumer 加入或者老的 Consumer 离开,都会将 Topic Partition 再均衡,重新分配给 Consumer 。 > 注意噢,此处说的都是同一个 Kafka Consumer group 。 总的来说,Kafka 和 RocketMQ 的高可用方式是比较类似的,主要的差异在 Kafka Broker 的副本机制,和 RocketMQ Broker 的主从复制,两者的差异,以及差异带来的生产和消费不同。😈 当然,实际上,都是和“主” Broker 做消息的发送和读取不是?! ## 什么是 Kafka 事务? 推荐阅读 [《Kafka 事务简介》](https://blog.csdn.net/ransom0512/article/details/78840042) 文章。 😈 和想象中的,是不是有点差别?! 具体的代码实现,可以看看 [《芋道 Spring Boot 消息队列 Kafka 入门》](http://www.iocoder.cn/Spring-Boot/RabbitMQ/?vip)的[「11. 事务消息」](https://svip.iocoder.cn/Kafka/Interview/#)小节。 ## Kafka 是否会弄丢数据? > 艿艿:注意,Kafka 是否会丢数据,主要取决于我们如何使用。这点,非常重要噢。 🦅 **消费端弄丢了数据?** 唯一可能导致消费者弄丢数据的情况,就是说,你消费到了这个消息,然后消费者那边自动提交了 offset ,让 Broker 以为你已经消费好了这个消息,但其实你才刚准备处理这个消息,你还没处理,你自己就挂了,此时这条消息就丢咯。 这不是跟 RabbitMQ 差不多吗,大家都知道 Kafka 会自动提交 offset ,那么只要关闭自动提交 offset ,在处理完之后自己手动提交 offset ,就可以保证数据不会丢。但是此时确实还是可能会有重复消费,比如你刚处理完,还没提交 offset ,结果自己挂了,此时肯定会重复消费一次,自己保证幂等性就好了。 > RocketMQ push 模式下,在确认消息被消费完成,才会提交 Offset 给 Broker 。 生产环境碰到的一个问题,就是说我们的 Kafka 消费者消费到了数据之后是写到一个内存的 queue 里先缓冲一下,结果有的时候,你刚把消息写入内存 queue ,然后消费者会自动提交 offset 。然后此时我们重启了系统,就会导致内存 queue 里还没来得及处理的数据就丢失了。 具体的代码实现,可以看看 [《芋道 Spring Boot 消息队列 Kafka 入门》](http://www.iocoder.cn/Spring-Boot/RabbitMQ/?vip)的[「12. 消费进度的提交机制」](https://svip.iocoder.cn/Kafka/Interview/#)小节。 🦅 **Broker 弄丢了数据?** 这块比较常见的一个场景,就是 Kafka 某个 Broker 宕机,然后重新选举 Partition 的 leader。大家想想,要是此时其他的 follower 刚好还有些数据没有同步,结果此时 leader 挂了,然后选举某个 follower 成 leader 之后,不就少了一些数据?这就丢了一些数据啊。 生产环境也遇到过,我们也是,之前 Partition 的 leader 机器宕机了,将 follower 切换为 leader 之后,就会发现说这个数据就丢了。 所以此时一般是要求起码设置如下 4 个参数: - 给 Topic 设置 `replication.factor` 参数:这个值必须大于 1,要求每个 partition 必须有至少 2 个副本。 - 在 Kafka 服务端设置 `min.insync.replicas` 参数:这个值必须大于 1 ,这个是要求一个 leader 至少感知到有至少一个 follower 还跟自己保持联系,没掉队,这样才能确保 leader 挂了还有一个 follower 吧。 - 在 Producer 端设置 `acks=all`:这个是要求每条数据,必须是**写入所有 replica 之后,才能认为是写成功了**。 > 不过这个也不一定能够绝对保证,例如说,Broker 集群里,所有节点都挂了,只剩下一个节点。此时,`acks=all` 和 `acks=1` 就等价了。当然,也可以通过设置 `min.insync.replics` 参数,每次写入要求最小的同步副本数。 > > 这块也和朋友交流了下,他们金融场景下,`acks=all` 也是这么配置的。原因嘛,因为他们是金融场景呀。 - 在 Producer 端设置 `retries=MAX`(很大很大很大的一个值,无限次重试的意思):这个是**要求一旦写入失败,就无限重试**,卡在这里了。 我们生产环境就是按照上述要求配置的,这样配置之后,至少在 Kafka broker 端就可以保证在 leader 所在 Broker 发生故障,进行 leader 切换时,数据不会丢失。 🦅 **生产者会不会弄丢数据?** 如果按照上述的思路设置了 `acks=all` ,一定不会丢,要求是,你的 leader 接收到消息,所有的 follower 都同步到了消息之后,才认为本次写成功了。如果没满足这个条件,生产者会自动不断的重试,重试无限次。 - 关于 Kafka Producer 重试发送消息的逻辑的源码解析,可以看看 [《Kafka 重试机制解读》](https://www.jianshu.com/p/5bf9c7c02ada) 。 ------ 😈 另外,在推荐一篇文章 [《360 度测试:KAFKA 会丢数据么?其高可用是否满足需求?》](https://juejin.im/post/5bf8f2bee51d4507c12bc722) ,提供了一些测试示例。 关于这一块,可以重点看看 [《Kafka 权威指南》](https://u.jd.com/jJpbF8) 的 [「6.4 在可靠的系统里使用生产者」](https://svip.iocoder.cn/Kafka/Interview/#) 和 [「6.5 在可靠的系统里使用消费者」](https://svip.iocoder.cn/Kafka/Interview/#) 小节。 ## Kafka 如何保证消息的顺序性? Kafka 本身,并不像 RocketMQ 一样,提供顺序性的消息。所以,提供的方案,都是相对有损的。如下: > 这里的顺序消息,我们更多指的是,单个 Partition 的消息,被顺序消费。 - 方式一,Consumer ,对每个 Partition 内部单线程消费,单线程吞吐量太低,一般不会用这个。 - 方式二,Consumer ,拉取到消息后,写到 N 个内存 queue,具有相同 key 的数据都到同一个内存 queue 。然后,对于 N 个线程,每个线程分别消费一个内存 queue 即可,这样就能保证顺序性。 > 这种方式,相当于对【方式一】的改进,将相同 Partition 的消息进一步拆分,保证相同 key 的数据消费是顺序的。 > > 不过这种方式,消费进度的更新会比较麻烦。 当然,实际情况也不太需要考虑消息的顺序性,基本没有业务需要。 具体的代码实现,可以看看 [《芋道 Spring Boot 消息队列 Kafka 入门》](http://www.iocoder.cn/Spring-Boot/RabbitMQ/?vip)的[「10. 顺序消息」](https://svip.iocoder.cn/Kafka/Interview/#)小节。 ## 666. 彩蛋 😈 略显仓促的一篇文章,后续会重新在梳理一下。如果胖友对 Kafka 有什么疑惑,一定要在星球里提出,我们一起在讨论和解答一波,然后整理到这篇文章中。 同时,期待下厮大的 Kafka 新书。 参考与推荐如下文章: - [《高并发面试必问:分布式消息系统 Kafka 简介》](https://blog.csdn.net/caisini_vc/article/details/48007297) - [《14 个最常见的 Kafka 面试题及答案》](https://blog.csdn.net/yjh314/article/details/77568580) > 这篇博客,有点傻逼。。。。 - [《你需要知道的 Kafka》](https://juejin.im/post/5b573eafe51d45198f5c80a4) - [《Kafka 内部网络框架模型分析》](https://blog.csdn.net/lizhitao/article/details/52332749) - [《再谈基于 Kafka 和 ZooKeeper 的分布式消息队列原理》](https://gitbook.cn/books/5bc446269a9adf54c7ccb8bc/index.html) - [《如何保证消息的可靠性传输?(如何处理消息丢失的问题)》](https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/how-to-ensure-the-reliable-transmission-of-messages.md)