消息中间件面试题之kafka

Kafka的特性

1.消息持久化

2.高吞吐量

3.扩展性

4.多客户端支持

5.Kafka Streams

注意:当你不会说的时候,就围绕着kafka你知道的kafka的功能来说,比方说消息的持久化机制。

说一说Kafka你熟悉的参数?

创建生产者对象时有三个属性必须指定

bootstrap.servers:说白了就是指定都有哪些节点

key.serializer:键的序列化器

value:serializer: 值的序列化器

acks:指定了必须要有多少个分区副本收到消息,生产者才会认为写入消息是成功的,这个参数对消息丢失的可能性有重大影响。

acks=0:⽣产者不等待broker对消息的确认,只要将消息放到缓冲区,就认为消息已经发送完成。

acks=1: 表示消息只需要写到主分区即可,然后就相应客户端,而不等待副本分区的确认。

acks=all: 表示消息需要写到主分区,并且会等待所有的ISR副本分区确认记录。

retries:重试次数,当消息返送出现错误的时候,系统会重发消息。根客户端收到错误时,重发一样。

compression.type:指定消息的压缩类型。

max.request.size:控制生产者发送请求的最大大小。

request.timeout.ms:客户端等待请求响应的最大值。说白了就是一个超时时间。

batch.size:当多个消息被发送同一个分区时,生产者会把它们放在同一个批次里。该参数指定了一个批次可以使用的内存大小,按照字节数计算。当批次内存被填满后,批次里的所有消息会被发送出去。

kafka中,可以不用zookeeper么?

在新版本的kafka中可以不使用zookeeper。但是kafka本身是自带zookeeper的。但是为了安全考虑,我们通常会外接我们自己的zookeeper。现在的新版本可以使用Kafka with Kraft,就可以完全抛弃zookeeper。

为什么Kafka不支持读写分离?

1、数据一致性问题:数据从主节点转到从节点,必然会有一个延时的时间窗口,这个时间窗口会导致主从节点之间的数据不一致。

2、延时问题:Kafka追求高性能,如果走主从复制,延时严重

Kafka中是怎么做到消息顺序性的

一个 topic,一个 partition,一个 consumer,内部单线程消费,最傻瓜式的一种做法。

构建ProducerRecord消息的时候,我们可以通过构造方法指定要发送到哪个分区。

public ProducerRecord(String topic, Integer partition, K key, V value, Iterable<Header> headers) {
}

Kafka为什么那么快?

  1. 利用 Partition 实现并行处理
  2. 顺序写磁盘
  3. 充分利用 Page Cache
  4. 零拷贝技术
  5. 批处理
  6. 数据压缩

kafka怎样做消息广播(面试的时候有被问到)

这里需要问面试官一下,你是想要实现消息的广播发送还是消息的广播消费。


消息的广播发送:

Kafka 本身不提供直接的“广播”机制,但可以通过创建多个主题(topics)并将消息发送到每个主题来实现广播效果。每个订阅者只能看到他所订阅的主题中的消息。


消息的广播消费:

总所周知,我们每一个消费组中只有一个消费者能消费到某个主题下的那一条消息。这个时候我们可以通过在每个消费者分组上加上UUID,这样就能保证每个项目启动的消费者的分组是不同的。这样就能够达到广播消费的目的了。

kafka中的消息粒度,partition下面还有一个消息存储的粒度,叫什么(面试的时候有被问到)

Kafka中的partition下面存储的是LogSegment‌。每个partition在物理上被分割成多个大小相等的LogSegment,每个LogSegment由一个数据文件(.log)和一个索引文件(.index)组成。数据文件存储实际的消息数据,而索引文件存储消息的索引信息,包括消息的物理偏移地址等元数据‌。

如果我配置了7天,那么我消费的时候如果指定的是earlist的话,那么有没有可能消费到7天之外的数据。(面试的时候有被问到)

有可能
https://blog.csdn.net/weixin_42305433/article/details/109731388

kafka消息删除的粒度,是消息级别的删除吗(面试有被问到)

Kafka的删除策略以segment为单位,而非单条消息。

kafka的消息是以主题为单位进行归类的,各个主题之间彼此是独立的,互不影响。

每个主题又可以分为一个或多个分区。

每个分区各自存在一个记录消息数据的日志文件。

分区日志文件中包含很多的LogSegment(也就是说日志分段),默认情况下一个LogSegment是1G。

kafka一共有两种消息删除策略,一种是消息删除,一种是消息压缩。

消息删除:在Kafka中,消息一旦被写入到分区中,就不可以被直接删除。这是因为Kafka的设计目标是实现高性能的消息持久化存储,而不是作为一个传统的队列,所以不支持直接删除消息。

然而,Kafka提供了消息的过期策略来间接删除消息。具体来说,可以通过设置消息的过期时间(TTL)来控制消息的生命周期。一旦消息的时间戳超过了设定的过期时间,Kafka会将其标记为过期,并在后续的清理过程中删除这些过期的消息。

Kafka的清理过程由消费者组中的消费者来执行。消费者消费主题中的消息,并将消费的进度提交到Kafka。一旦消息被提交,Kafka就可以安全地删除这些消息。

另一方面,如果需要从Kafka中完全删除消息,可以通过设置合适的保留策略来实现。Kafka支持两种保留策略:基于时间和基于大小。基于时间的保留策略会根据消息的时间戳来删除旧的消息,而基于大小的保留策略会根据分区的大小来删除旧的消息。可以根据业务需求选择适合的保留策略。

需要注意的是,删除消息并不会立即释放磁盘空间。删除的消息只是被标记为删除,并在后续的清理过程中才会真正释放磁盘空间。因此,即使消息被删除,磁盘空间也不会立即释放,而是会在清理过程中逐渐释放。

消息压缩会将所有key相同的消息进行合并。这个一般使用在大数据领域。

kafka为什么这么快?(面试有被问到)

kafka对于大数据是支持的,比如说Hadoop

主要有以下这么四点:

磁盘顺序

读写页缓存:直接使用操作系统的页缓存特性提高处理速度,进而避免了JVM GC带来的性能损耗。

零拷贝

批量操作:kafka也支持消息压缩和批量发送数据

kafka文件删除有两种方式,一种是基于时间,一种是基于分区文件的大小

image

kafka几个核心点(面试有被问到)

acks:

该选项控制着已发送消息的持久性。

acks=0 :⽣产者不等待broker的任何消息确认。只要将消息放到了socket的缓冲区,就认为消息 已发送。不能保证服务器是否收到该消息, retries 设置也不起作⽤,因为客户端不关⼼消息是 否发送失败。客户端收到的消息偏移量永远是-1。

acks=1 :leader将记录写到它本地⽇志,就响应客户端确认消息,⽽不等待follower副本的确 认。如果leader确认了消息就宕机,则可能会丢失消息,因为follower副本可能还没来得及同步该 消息。

acks=all :leader等待所有同步的副本确认该消息。保证了只要有⼀个同步副本存在,消息就不 会丢失。这是最强的可⽤性保证。等价于 acks=-1 。默认值为1,字符串。可选值:[all, -1, 0, 1]

retries

设置该属性为⼀个⼤于1的值,将在消息发送失败的时候重新发送消息。该重试与客户端收到异常 重新发送并⽆⼆⾄。允许重试但是不设置 max.in.flight.requests.per.connection 为1,存 在消息乱序的可能,因为如果两个批次发送到同⼀个分区,第⼀个失败了重试,第⼆个成功了,则 第⼀个消息批在第⼆个消息批后。int类型的值,默认:0,可选值:[0,...,2147483647]

kafka消费消息的时候,我们可以使用同步提交offset也可以异步提交offset。

kafka怎样保证消息不丢失(面试有被问到)

在生产者端,我们可以设置acks的值,来保证消息不丢失

在broker端,我们可以设置分区和副本的机制来保证消息的不丢失。

在消费者端,我们可以使用在消息被正确消费之后手动提交偏移量的方式来保证。

也可以在消息投递之前,添加一张日志表(采用顺序写的方式)来保证。

kafka生产者都干了哪些事情?(面试有被问到)

image

为什么kafka在客户端发送的时候需要做一个缓存区?

1.为了减少IO的开销(单个 -> 批次)

2.减少GC

缓存发送:主要是这两个参数控制(只要1个满足)

大小:batch.size = 16384(16K)

时间:linger.ms = 0(说白了就是只要有了消息就会进行发送)

序列化器‌:将消息对象序列化为字节流,以便存储和传输。

分区器‌:根据配置的分区策略,将消息分配到不同的分区中

kafka生产者是怎样将消息发送到broker上面的?(面试有被问到)

后台是通过一个sender线程使用TCP的方式发送到broker上面的。当然它是自定义的TCP协议。

kafka搭建为什么要使用zookeeper(面试有被问到)

1.每个broker在启动的时候都会向zookeeper中注册自己的信息(ip port),broker创建的节点是临时节点类型,如果broker发生宕机,这个临时节点也会被删除,这个时候就会发生主从切换。

2.每个topic下面可能会有多个分区信息,这个时候topic和对应分区以及broker的关系也会被记录到zookeeper中。

3.生产者的负载均衡,因为每个broker都会注册自己的信息,每个broker上线下线,生产者都能够感知到,这样生产者就能够负载均衡的发送消息了。

4.消费者的负载均衡,每个消费者分组都包含了若干个消费者,每条消息只会发送到组里面的一个消费者中。同上。

5.维系分区和消费者的关系。

6.消息消费的偏移量,也就是每个分区消息的消费进度信息。

搭建kafka集群的时候需要注意什么?(面试有被问到)

我总结有这么以下几点:

需要搭建zookeeper集群

kafka的安装配置,肯定是要改配置文件的。

网络配置:因为是broker集群,我们一定要确保每个节点都能够正常的进行通信,如果不能通信的话,这个时候我们就需要关闭防火墙,或者是开发某个端口。

版本一致性:确保Kafka集群的各个节点使用相同版本的Kafka,以避免兼容性问题‌。

‌Topic分区和副本配置:根据需求设置Topic的分区数和副本数,副本数可以保证数据的冗余和可用性‌1。

Broker ID唯一性:每个Kafka节点的broker.id必须是唯一的,且集群中的每个节点都应该有一个唯一的标识‌1。

环境准备:包括节点规划、JDK环境配置等,确保每个节点都安装了JDK,因为Kafka需要Java运行环境‌2。

说一下kafka怎样保证消息的幂等性(面试有被问到)

说白了就是不重复消费
就说redis的setnx

说一下怎样搭建kafka集群(面试有被问到)

要搭建Kafka集群,您需要准备以下条件:

  1. 至少三台机器(或者在同一台机器上运行多个虚拟实例)。
  2. 相同的Kafka版本在所有机器上。
  3. 一个ZooKeeper集群,用于管理Kafka集群状态。

以下是搭建Kafka集群的基本步骤:

  1. 安装Java。
  2. 下载并解压Kafka到每台机器上。
  3. 配置ZooKeeper集群(如果尚未设置)。
  4. 配置Kafka服务器属性,例如server.properties

以下是配置Kafka集群的关键设置示例:

broker.id: 每个Kafka实例的唯一标识。

listeners: 监听的地址和端口。

zookeeper.connect: ZooKeeper集群的地址。

示例server.properties配置:

broker.id=1
listeners=PLAINTEXT://:9092
zookeeper.connect=zk1:2181,zk2:2181,zk3:2181

对于其他Kafka实例,您需要更改broker.id来保证唯一性,并相应更改listenerszookeeper.connect中的配置。

启动Kafka集群通常按以下顺序进行:

  1. 启动ZooKeeper集群。
  2. 启动Kafka服务。

启动Kafka服务的命令通常是:

kafka-server-start.sh /path/to/your/kafka/config/server.properties

确保每个Kafka实例的配置文件都已正确配置,并且所有端口都没有被其他服务占用。

什么是 Rebalance 机制

在 Kafka 中,Rebalance(再平衡)是一种机制,用于在消费者组(Consumer Group)内重新分配分区(Partition)的所有权。

当有消费者加入或离开消费者组、或者主题(Topic)的分区数量发生变化时,就会触发 Rebalance。它的目的是确保消费者组内的每个消费者能够公平合理地消费消息,避免某个消费者负担过重或部分消息得不到处理。

kafka自动提交的时候设置的超时时间是多少呀

Kafka 消费者默认的自动提交间隔时间是 5 秒。这个时间是通过auto.commit.interval.ms参数来设置的。这意味着每隔 5 秒,消费者会自动将它所消费到的消息的偏移量提交给 Kafka 集群。

import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import java.util.Properties;
public class KafkaConsumerExample {
    public static void main(String[] args) {
        Properties props = new Properties();
        // 设置其他必要的消费者配置参数,如bootstrap.servers等
        props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS, "3000");
        KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
        // 后续的消费者操作代码
    }
}

如果消费kafka某个批次中的某条消息消费失败,应该怎么办 (面试有被问到)

在 Kafka 批量消费场景中,若批次中某条消息处理失败,核心处理原则是 "精准控制偏移量,避免因单条失败影响整个批次",同时保证消息不丢失、可追溯。以下是面试中常考的处理方案:

一、核心思路:拆分处理粒度,单独跟踪偏移量

批量消费的优势是提升效率,但失败处理的关键是将批次拆分为单条处理,为每条消息单独记录处理状态,避免 "一条失败,全批重消费" 的问题。

二、具体处理步骤

1. 逐条处理消息,维护分区级成功偏移量

  • 批量拉取消息后,遍历每条消息单独处理;
  • 为每个分区维护一个Map<TopicPartition, OffsetAndMetadata>,仅记录已成功处理的消息偏移量
  • 最终只提交 "已成功的偏移量",失败消息因未提交偏移量,会在下次拉取时重新消费。

代码示例

// 批量拉取消息
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
// 存储每个分区已成功处理的最大偏移量
Map<TopicPartition, OffsetAndMetadata> successOffsets = new HashMap<>();

for (ConsumerRecord<String, String> record : records) {
    TopicPartition partition = new TopicPartition(record.topic(), record.partition());
    try {
        // 处理单条消息
        processMessage(record);
        // 处理成功:更新该分区的成功偏移量(+1表示下一条待消费位置)
        successOffsets.put(partition, new OffsetAndMetadata(record.offset() + 1));
    } catch (Exception e) {
        // 单条消息失败:不更新偏移量,仅记录日志
        log.error("消息处理失败: topic={}, partition={}, offset={}", 
                 record.topic(), record.partition(), record.offset(), e);
        // 执行单条消息的重试逻辑
        if (!retryMessage(record)) {
            // 重试失败:放入死信队列
            sendToDLQ(record);
            // 【谨慎操作】若业务允许丢弃,可强制提交该偏移量(需业务确认)
            // successOffsets.put(partition, new OffsetAndMetadata(record.offset() + 1));
        }
    }
}

// 提交所有成功处理的偏移量
if (!successOffsets.isEmpty()) {
    consumer.commitSync(successOffsets);
}

2. 单条消息的重试策略

对失败消息单独重试,避免重试整个批次导致的重复处理:

  • 有限重试:设置最大重试次数(如 3 次),配合指数退避间隔(1s、2s、4s),避免瞬时错误阻塞消费;
  • 隔离重试:将失败消息放入内存队列异步重试,不阻塞主消费线程(适合高吞吐场景)。
private boolean retryMessage(ConsumerRecord<String, String> record) {
    int maxRetries = 3;
    for (int i = 0; i < maxRetries; i++) {
        try {
            processMessage(record);
            return true; // 重试成功
        } catch (Exception e) {
            log.warn("第{}次重试失败, offset={}", i+1, record.offset(), e);
            if (i == maxRetries - 1) return false; // 达到最大次数
            // 指数退避
            Thread.sleep(1000L * (1 << i)); 
        }
    }
    return false;
}

3. 死信队列(DLQ)隔离永久失败

对重试后仍失败的消息(如格式错误、业务逻辑无法处理):

  • 发送到专门的死信主题(如topic.dlq),附带原消息的元数据(分区、偏移量、失败原因);
  • 死信队列由单独消费组处理,支持人工介入修复后重新投递,避免阻塞主流程。

4. 极端场景:暂停异常分区

若某分区消息持续失败(如依赖服务异常),可暂停该分区消费:

// 记录处理失败的分区
Set<TopicPartition> failedPartitions = new HashSet<>();
// ...处理中若某分区多次失败,加入failedPartitions...

// 暂停失败分区,5分钟后重试
if (!failedPartitions.isEmpty()) {
    // 暂停对失败分区的消费
    consumer.pause(failedPartitions);
    scheduledExecutor.schedule(() -> consumer.resume(failedPartitions), 5, TimeUnit.MINUTES);
}

三、必须强调的保障机制

  • 幂等性设计:失败消息会被重复消费,需通过消息唯一 ID(如msgId)或乐观锁确保重复处理无副作用;
  • 手动提交偏移量:关闭自动提交(enable.auto.commit=false),确保偏移量提交仅依赖业务处理结果。

面试总结话术

处理批次中单条消息失败的核心是 "精准控制偏移量,隔离失败消息"

  1. 批量拉取后逐条处理,用Map记录每个分区的成功偏移量,仅提交已成功部分;
  2. 单条失败消息通过有限重试解决瞬时错误,永久失败则放入死信队列;
  3. 配合幂等性设计避免重复消费的影响,极端情况可暂停异常分区。

这套方案既保留了批量消费的效率,又保证了消息处理的可靠性,是生产环境的标准实践。

kafka内部有哪种机制能防止发送两条一样的消息(面试有问到)

这是一个非常经典的Java面试题,考察的是对Kafka消息传递语义和幂等性等核心机制的理解。

在面试中,回答这个问题可以分层次进行,从浅入深,展示你的知识深度。

简短回答(一句话概括)

Kafka主要通过生产者端的幂等性(Idempotence)事务(Transaction) 两种机制来防止发送重复消息。


详细回答(建议按此层次展开)

1. 首先,明确问题根源:为何会产生重复消息?

在说明了解决方案前,最好先说明问题来源,这能体现你的思考全面性。

  • 生产者重试机制:这是最常见的原因。当生产者发送消息后,没有收到Broker的确认(ack),比如网络抖动、Broker短暂故障,生产者会尝试重新发送消息。如果第一次发送实际上已经成功,只是确认ACK丢失了,那么重试就会导致消息重复。
  • 消费者端处理:消费者处理完消息后,如果没有及时提交偏移量(offset),发生重启或再均衡(rebalance)后,可能会从上次提交的offset重新消费,导致消息被重复处理。(注意:这个问题是关于“发送”重复,但了解消费端重复也很有价值)

2. 核心机制一:幂等性(Idempotence)

  • 是什么:开启幂等性后,意味着生产者发送相同内容的消息多次,Broker端只会持久化一条。这解决了由生产者重试引起的“至少一次”语义下的重复问题。
  • 如何实现
    • 在生产者客户端配置中设置 enable.idempotence = true(新版Kafka默认即为true)。
    • 原理是Kafka为每个生产者会话分配一个唯一的PID(Producer ID),并为发送的每一条消息绑定一个序列号(Sequence Number)。
    • Broker端会维护<PID, Topic, Partition>对应的最新序列号。当收到一条消息时,它会检查其序列号:
      • 如果序列号正好比Broker记录的大1,接受消息。
      • 如果序列号等于或小于Broker记录的(即重复消息),丢弃该消息。
      • 如果序列号比Broker记录的大很多(即消息丢失),抛出OutOfOrderSequenceException异常。
  • 优点
    • 开销极小,性能几乎无影响。
    • 完全在Broker端实现,对客户端透明。
  • 局限性
    • 只能保证单分区上的幂等。即相同消息重复发送到同一个主题的同一个分区时,会被去重。
    • 只能保证单个生产者会话内的幂等。如果生产者客户端重启,新的会话会有新的PID,无法避免跨会话的重复。
    • 不跨多个分区或主题

代码示例(启用幂等性):

java

Properties props = new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());

// 关键配置:启用幂等性 (实际上,在Kafka 2.0+版本中,当acks=all时,它默认已开启)
props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, "true");
// 显式设置acks为all,这是启用幂等性的前提
props.put(ProducerConfig.ACKS_CONFIG, "all");

KafkaProducer<String, String> producer = new KafkaProducer<>(props);

3. 核心机制二:事务(Transactions)

  • 是什么:用于提供跨多个分区和多个生产者会话的“精确一次”(Exactly-Once)语义。它不仅能防止消息重复发送,还能保证“发送消息”和“消费消息后再发送”这种链式操作的原子性。
  • 如何实现
    • 生产者配置 transactional.id 并初始化事务。
    • 使用API:initTransactions(), beginTransaction(), commitTransaction(), abortTransaction()
    • 原理是事务协调器(Transaction Coordinator)会记录事务的状态(PrepareCommit/PrepareAbort),并与幂等性机制结合使用。只有在事务提交后,消息才对消费者可见。
  • 适用场景
    • 跨分区的幂等写入:需要原子性地向多个Topic-Partition发送消息。
    • “读-处理-写”模式(Consume-Transform-Produce):从主题A消费,处理,然后发送到主题B,这个全过程在一个事务里,要么全成功,要么全失败,避免重复处理。
  • 优点
    • 提供了最强的消息传递保证(精确一次)。
    • 作用范围广,跨分区、跨会话。
  • 缺点
    • 性能开销比仅开启幂等性要大。
    • 消费者需要配置 isolation.level=read_committed 来只读取已提交的事务消息。

代码示例(使用事务):

java

Properties props = new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, "true"); // 事务依赖于幂等性
props.put(ProducerConfig.ACKS_CONFIG, "all");
// 关键配置:设置唯一的事务ID
props.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, "my-transactional-id"); 

KafkaProducer<String, String> producer = new KafkaProducer<>(props);

// 初始化事务
producer.initTransactions();

try {
    producer.beginTransaction();
    // 发送多条消息,可能到不同的分区
    producer.send(new ProducerRecord<>("topic1", "key1", "value1"));
    producer.send(new ProducerRecord<>("topic2", "key2", "value2"));
    // ... 一些业务逻辑
    producer.commitTransaction(); // 只有提交后,消息才真正被写入并对消费者可见
} catch (Exception e) {
    producer.abortTransaction(); // 中止事务,所有消息都不会被写入
    // 处理异常
} finally {
    producer.close();
}

面试回答技巧总结

  1. 结构化:先总述(幂等性和事务),再分点详细解释。
  2. 深入原理:不要只停留在配置层面,解释PIDSequence Number机制,以及事务协调器。
  3. 对比分析:明确指出两者的区别(会话内/跨会话,单分区/多分区)和适用场景。
  4. 关联扩展:可以简要提及消费端的重复问题(offset提交),展示知识的完整性。
  5. 结合代码:如果面试官感兴趣,可以写出关键的配置和API,这是很大的加分项。

最终,你可以这样收尾:“因此,在实际项目中,如果只是要解决网络重试导致的单分区重复,开启幂等性就足够了,它是轻量级的首选。如果需要更强的、跨会话跨分区的原子性保证,比如金融交易场景,才会使用事务机制。”

posted on 2024-09-13 11:23  ~码铃薯~  阅读(133)  评论(0)    收藏  举报

导航