Kafka快速入门

文章目录


一、前言

Kafka 作为 Apache 基金会的顶级项目,由 LinkedIn 于 2011 年开源,已从最初的消息队列演化为一个成熟的分布式事件流平台------既能做消息缓冲,也能做日志采集、实时数据管道和流处理。

1.消息队列作用

  • 缓冲 / 削峰:生产者瞬时流量远大于消费者处理能力时,Kafka 作为中间缓冲层,吸收峰值流量,防止下游系统被压垮。秒杀系统、日志采集、活动流量缓冲等场景都是典型应用。
  • 系统解耦:引入 Kafka 后,上游系统只需往 Kafka 写入消息,下游系统自行订阅感兴趣的 Topic。上下游独立演进,新增消费者不影响生产者,下游故障也不会直接拖垮上游。
  • 异步通信:用户注册等场景中,核心流程(写数据库)与耗时操作(发短信、发邮件)解耦,用户注册 → 写数据库 → 写 Kafka → 立即返回结果,非核心流程由消费者异步完成,接口响应速度显著提升。

2.消息队列的两种模型

  • 点对点模型:消息被一个消费者消费后,通常会被删除。一条消息只被一个消费者处理,更像传统的队列。
  • 发布订阅模型:消息按 Topic 分类,不同消费者组之间互不影响,每个消费者组都可以消费完整数据。Kafka 更接近发布订阅模型。

二、Kafka 核心概念

Kafka 本质上是一个分布式、高吞吐、可持久化、基于发布/订阅模型的事件流平台。其基本架构如下,图片来源

  • Broker(代理) :一台 Kafka 服务器即为一个 Broker,多个 Broker 组成 Kafka 集群。
  • Topic(主题) :消息的逻辑分类。
  • Partition(分区) :Topic 被切分为多个分区,每个分区是一个有序的提交日志文件,是 Kafka 实现水平扩展和并发的核心机制。
  • Replica(副本):每个 Partition 可以拥有多个副本,分布在不同的 Broker 上。副本分为 Leader(领导者)和 Follower(追随者)。Leader 负责所有读写请求,Follower 只被动地从 Leader 同步数据。当 Leader 故障时,某个 Follower 会被选举为新的 Leader。副本机制是 Kafka 高可用和数据不丢失的基础。
  • Producer(生产者) :负责将消息发布到 Topic 的客户端。
  • Consumer(消费者) :从 Topic 读取消息的客户端。
  • Consumer Group(消费者组) :多个消费者组成一个 Consumer Group,组内的每个消费者负责消费不同的 Partition,实现并行消费。同一个 Partition 只能被组内的一个消费者消费。
  • Zookeeper:保存集群的的元信息,来保证系统的可用性。
  • Offset(偏移量):消息在 Partition 中的位置标识。消费者消费消息后,会提交(Commit)已处理的 Offset,记录消费进度。如果消费者组重启,将从提交的 Offset 恢复消费。

分区的数量没有上限每个分区只有一个 Leader 副本负责读写,该 Leader 会落在某一个 Broker 上。其他 Broker 上可能存放该分区的 Follower 副本
分区的 Leader 是动态分配的,可以落在任意 Broker 上,不是固定为 Broker1。Kafka 会尽量均匀分布。
副本因子不能超过 Broker 节点数
在同一个消费者组内,一个分区只能被该组内的一个消费者消费,上图理解为Consumer0消费了TopicA-Partition0,那么Consumer1就不能消费了,但是另一个消费者组B的Consumer0可以消费
当副本因子(replication factor)设置为 1 时,每个分区只有唯一的一个副本 Leader,负责该分区的所有读写请求。不存在 Follower

1.基本操作

现在有kafka集群kafka:9091、kafka:9092、kafka:9093。

(1)创建Topic主题

  • kafka-topics.bat:管理 Topic 的工具。
  • --create:执行创建操作。
  • --bootstrap-server localhost:9091:告诉工具"集群入口在 localhost 的 9091 端口"。
  • --replication-factor 1:每个分区的副本数量为 1(即没有额外备份)。
  • --partitions 1:该 Topic 只包含 1 个分区。
  • --topic test:要创建的主题名称是 test。
bash 复制代码
kafka-topics.bat --create --bootstrap-server localhost:9091 --replication-factor 1 --partitions 1 --topic test

(2)生产消息到kafka

  • kafka-console-producer.bat:控制台生产者工具,从标准输入读取消息并发送到 Kafka。
  • --broker-list localhost:9091:指定至少一个 Broker 地址作为初始连接。注意:生产者的参数是 --broker-list(而不是 --bootstrap-server,新旧版本差异;新版也支持 --bootstrap-server,但这里按你的参考文章保留传统写法)。
  • --topic test:消息发送到的目标主题。
bash 复制代码
kafka-console-producer.bat --broker-list localhost:9091 --topic test

(3)从kafka消费

  • kafka-console-consumer.bat:控制台消费者工具,从 Kafka 拉取消息并打印到标准输出。
  • --bootstrap-server localhost:9091:消费者使用 --bootstrap-server 参数指定集群入口。
  • --topic test:订阅的主题。
  • --from-beginning:从该主题的最早消息开始消费(如果没有这个参数,只消费启动后产生的新消息)。
bash 复制代码
kafka-console-consumer.bat --bootstrap-server localhost:9091 --topic test --from-beginning

2.Kafka Tool

(1)搭建kafka集群

在本地或云服务器上搭建一个多节点的 Kafka 集群(至少 3 个 Broker)

文章参考:windows下kafka集群搭建

(2)创建Topic

在 Offset Explorer 中,右键点击 Topics → Create Topic,弹出窗口需填写:

  • Name:主题名称,如 user_events。
  • Partition Count:分区数量。分区数越多,并行度越高,但也会增加 Leader 选举和元数据开销。一般根据预期吞吐量设置,可后期动态增加(但不能减少)。
  • Replica Count:副本因子(Replication Factor)。每个分区的副本总数(包括 Leader)。例如设置为 3,表示每个分区有 1 个 Leader + 2 个 Follower。副本数越多,数据可靠性越高,但存储成本和网络同步开销也越大。生产环境通常设为 2 或 3。

三、Java中操作Kafka

1.导入依赖

在 Maven 项目的 pom.xml 中添加 Kafka 客户端依赖(建议使用与集群版本一致的最新稳定版)

xml 复制代码
<dependency>
    <groupId>org.apache.kafka</groupId>
    <artifactId>kafka-clients</artifactId>
    <version>3.6.0</version>
</dependency>

2.导入log4j.properties

将 log4j.properties 文件放入 src/main/resources 文件夹中,便于查看 Kafka 客户端运行日志:

properties 复制代码
log4j.rootLogger=INFO,stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p - %m%n

3.生产者使用同步的方式发送消息

同步发送:调用 send() 方法后,通过 Future.get() 阻塞等待 Kafka 的确认响应,能立刻知道发送结果,但吞吐量较低。

  • bootstrap.servers:Kafka 集群地址(可写多个,逗号分隔)
  • acks 表示当生产者生产的数据发送到Kafka中,Kafka会以什么样的策略返回
    • 0:不等待确认(最快,可能丢消息)
    • 1:仅 Leader 确认(折中)
    • all 或 -1:所有 ISR 副本确认(最可靠)
  • key.serializer / value.serializer:指定消息 key 和 value 的序列化方式(字符串、JSON、Avro 等)。Kafka中的消息是以key、value键值对存储的,而且生产者生产的消息是需要在网络上传到Broker的,这里指定的是StringSerializer方式。
java 复制代码
public class KafkaProducerTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 1. 创建用于连接Kafka的Properties配置
        Properties props = new Properties();
        props.put("bootstrap.servers", "node1.itcast.cn:9092");
        props.put("acks", "all");
        props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");

        // 2. 创建一个生产者对象KafkaProducer
        KafkaProducer<String, String> kafkaProducer = new KafkaProducer<>(props);

        // 3. 发送1-100的消息到指定的topic中
        for(int i = 0; i < 100; ++i) {
            // 构建一条消息,直接new ProducerRecord
            ProducerRecord<String, String> producerRecord = new ProducerRecord<>("test", null, "hello world-" + i);
            // 利用Future<RecordMetadata>等待响应
            Future<RecordMetadata> future = kafkaProducer.send(producerRecord);
            // 调用Future的get方法等待响应
            future.get();
            System.out.println("第" + i + "条消息写入成功!");
        }

        // 4. 关闭生产者
        kafkaProducer.close();
    }
}

4.生产者使用异步的方式发送消息

异步发送时,send() 方法立即返回,不阻塞;Kafka 收到响应后回调 onCompletion() 方法,适合高吞吐场景。

使用匿名内部类实现 Callback 接口,该接口中表示Kafka服务器响应给客户端,会自动调用OnCompletion方法

  • metadata:消息的元数据(属于哪个topic,属于哪个partition,对应的offset是什么)
  • exception:这个对象对Kafka生产消息封装了出现的异常,如果为null,表示发送成功,如果不为null,表示出现异常。
java 复制代码
// 二、使用异步回调的方式发送消息
ProducerRecord<String, String> producerRecord = new ProducerRecord<>(topic:"test", key:null, value:i + "");
kafkaProducer.send(producerRecord, new Callback() {
    @Override
    public void onCompletion(RecordMetadata metadata, Exception exception) {
        // 1.判断发送消息是否成功
        if(exception == null) {
            // 发送成功
            // 主题
            String topic = metadata.topic();
            // 分区id
            int partition = metadata.partition();
            // 偏移量
            long offset = metadata.offset();
            System.out.println("topic:" + topic + " 分区id:" + partition + " 偏移量:" + offset);
        }
        else {
            // 发送出现错误
            System.out.println("生产消息出现异常!");
            // 打印异常消息
            System.out.println(exception.getMessage());
            // 打印调用栈
            System.out.println(exception.getStackTrace());
        }
    }
});

5.消费者拉取消息

消费者通过 poll() 不断拉取消息,自动或手动提交偏移量(offset)。

  • group.id:消费者组的概念,可以在一个消费者组中包含多个消费者。如果若干个消费者的group.id是一样的,表示它们就在一个组中,一个组中的消费者是共同消费Kafka中topic的数据。
  • Kafka是一种拉消息模式的消息队列,在消费者中会有一个offset,表示从哪条消息开始拉取数据
  • KafkaConsumer.poll:Kafka的消费者API是一批一批数据的拉取
java 复制代码
public class KafkaConsumerTest {
    public static void main(String[] args) {
        // 1.创建Kafka消费者配置
        Properties props = new Properties();
        props.setProperty("bootstrap.servers", "node1.itcast.cn:9092");
        // 消费者组(可以使用消费者组将若干个消费者组织到一起,共同消费Kafka中topic的数据)
        // 每一个消费者需要指定一个消费者组,如果消费者的组名是一样的,表示这几个消费者是一个组中的
        props.setProperty("group.id", "test");
        // 自动提交offset
        props.setProperty("enable.auto.commit", "true");
        // 自动提交offset的时间间隔
        props.setProperty("auto.commit.interval.ms", "1000");
        // 拉取的key、value数据的
        props.setProperty("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        props.setProperty("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");

        // 2.创建Kafka消费者
        KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<>(props);

        // 3. 订阅要消费的主题
        // 指定消费者从哪个topic中拉取数据
        kafkaConsumer.subscribe(Arrays.asList("test"));

        // 4. 使用一个while循环,不断从kafka的topic中拉取消息
        while(true) {
            // Kafka的消费者一次拉取一批的数据
            ConsumerRecords<String, String> consumerRecords = kafkaConsumer.poll(Duration.ofSeconds(5));
            // 5.将记录(record)的offset、key、value都打印出来
            for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
                // 主题
                String topic = consumerRecord.topic();
                // offset:这条消息处于Kafka分区中的哪个位置
                long offset = consumerRecord.offset();
                // key\value
                String key = consumerRecord.key();
                String value = consumerRecord.value();

                System.out.println("topic: " + topic + " offset:" + offset + " key:" + key + " value:" + value);
            }
        }
    }
}

四、Kafka的幂等性

幂等性

为了实现生产者的幂等性,Kafka引入了 Producer ID(PID)和 Sequence Number的概念。

  • PID:每个Producer在初始化时,都会分配一个唯一的PID,这个PID对用户来说,是透明的。
  • Sequence Number:针对每个生产者(对应PID)发送到指定主题分区的消息都对应一个从0开始递增的Sequence Number。

五、分区和副本策略

1.生产者分区写入策略

生产者写入消息到 Topic 时,Kafka 会根据不同的分区写入策略将消息分配到不同的分区。分区的设计直接影响到消息的顺序性、并行度以及消费者的负载均衡。

(1)轮询分区策略

Kafka 默认的分区策略。消息依次轮流发送到每个可用分区。适用于无特殊顺序要求、无 key 的普通消息。

示例:假设 Topic 有 3 个分区,消息 1 → 分区0,消息2 → 分区1,消息3 → 分区2,消息4 → 分区0,以此类推。

(2)随机分区策略(不使用)

当前版本已不再使用

(3)按key分区分配策略

当消息指定了 key 时,Kafka 对 key 进行哈希计算,根据哈希值将消息分配到固定分区。相同 key 的消息永远进入同一个分区,从而保证分区内有序。但如果某个 key 的消息量极大,可能导致数据倾斜。

  • 计算公式为:partition = murmur2(key_bytes) % numPartitions。murmur2表示一种非加密哈希函数,输入为 key 的字节数组,输出为 32 位整数。numPartitions是主题当前的分区总数。

(4)自定义分区策略

需要根据业务逻辑(如用户ID、地区、时间等)自行决定分区号。可以实现 Partitioner 接口,覆盖 partition() 方法。

java 复制代码
 public class CustomPartitioner implements Partitioner {
    @Override
    public int partition(String topic, Object key, byte[] keyBytes,
                         Object value, byte[] valueBytes, Cluster cluster) {
        // 自定义逻辑:例如将消息全部发到分区 0
        return 0;
    }
    // 省略 close() 和 configure()
}

使用时在生产者配置中指定:

java 复制代码
props.put("partitioner.class", "com.example.CustomPartitioner");
乱序问题
  • Kafka 中,一个分区内的消息是严格有序的(写入顺序即消费顺序)。
  • 跨分区的消息无法保证全局有序,因为不同分区可以并行写入/消费

如果业务要求消息严格按顺序处理(如订单状态流转),应将相关消息发送到同一个分区(使用相同的 key)。但这会牺牲一定的并行能力,Kafka 的分布式优势主要体现在高吞吐,而非全局顺序。

2.消费者组Rebalance机制(再均衡)

Rebalance 是 Kafka 保证消费者组内所有消费者公平分配订阅 Topic 的分区的核心机制。

Rebalance触发的时机有:

  • 消费者组内成员变化:新消费者加入、现有消费者主动离开或崩溃(心跳超时)。
  • 订阅的 Topic 数量变化:消费者组订阅的正则表达式匹配到了新 Topic,或 Topic 被删除。
  • 订阅的分区数变化:管理员删除/增加了 Topic 的分区数。

Rebalance 的影响

  • 发生 Rebalance 时,consumer group下的所有consumer都会协调在一起共同参与,Kafka使用分配策略尽可能达到最公平的分配。
  • Rebalance 过程会对 consumer group 产生非常严重的影响,Rebalance 的过程中所有的消费者都将停止工作,直到Rebalance 完成

3.消费者分区分配策略

(1)Range 范围分配策略(默认)

按 Topic 逐个分配。将每个 Topic 的分区按数字顺序排序,消费者按名称排序,然后计算每个消费者应得的分区数。

算法公式

  • n=分区数量/消费者数量。
  • m=分区数量%消费者数量。
  • 前m个消费者消费n+1个。
  • 剩余消费者消费h个。

如下例所示,在同一个Topic A 中,存在7个分区,计算 n = 7 / 4 = 1,m= 7 % 4 = 3,那么前 3 个消费者消费两个分区,剩余消费者消息1个分区。

(2)RoundRobin轮询策略

按照整体的Topic进行分配,将所有订阅 Topic 的所有分区排序后,轮询分配给消费者

(3)Stricky粘性分配策略(推荐)

从Kafka0.11.x开始,引入此类分配策略。目标是使分区分配尽量均匀,同时解决前两种策略在 Rebalance 后分配变动过大的问题,分区的分配尽可能与上一次分配保持相同。

Striky粘性分配策略,保留rebalance之前的分配结果。这样可以明显减少系统资源的浪费。

4.生产者的ACK机制

acks 参数控制生产者发送消息后,需要多少个副本确认才算成功。它直接影响可靠性和延迟。

  • ACK=0:不等待broker确认,直接发送下一条数据,性能最高,但可能会存在数据丢失的情况。
  • ACK=1等待leader副本确认接收后,才会发送下一条数据,性能中等。
  • ACK=-1或者all:等待所有副本已经将数据同步后。
    才会发送下一条数据,性能最慢。

根据业务情况来选择ack机制,是要求性能最高,一部分数据丢失影响不大,可以选择0。如果要求数据一定不能丢失,就得配置为-1/all。

六、高级API与低级API

Kafka 提供了两种消费者 API:高级 API低级 API。它们的主要区别在于对偏移量(offset)、分区分配等底层细节的控制能力不同。

1. 高级 API(High-Level Consumer API)

高级 API 由 Kafka 统一管理分区、副本以及偏移量(offset),开发者无需关注底层细节。

  • 不需要手动管理 offset,offset 会自动保存在 ZooKeeper 中。
  • 消费者会根据上一次在 ZooKeeper 中保存的 offset 继续拉取数据。
  • 不同的消费者组(group)消费同一个 Topic 时,ZooKeeper 会分别为每个组记录不同的 offset,互不影响。
  • 分区分配由 Kafka 的 Rebalance 机制自动完成。

优点

  • 开发简单,无需关注底层细节。
  • 自动管理 offset,无需手动记录。
  • 消费者组自动进行分区分配和 Rebalance。

缺点

  • 不能控制 offset:例如无法从指定的任意位置开始读取。
  • 不能细化控制:无法细化控制分区、副本、ZooKeeper 等底层细节。

2. 低级 API(Low-Level Consumer API)

低级 API 允许开发者自己控制 offset、连接分区、自定义负载均衡等。

  • 可以自己控制 offset:想从哪儿读,就从哪儿读。
  • 可以自己控制连接到哪个分区,并找到该分区的 Leader。
  • offset 可以不使用 ZooKeeper 存储,而是自己选择存储位置,例如:文件、MySQL、Redis,甚至是内存中。
  • 原有的 Kafka 自动分配策略会失效,需要自己实现消费机制。

特点

  • 优点:细粒度控制(offset、分区、负载均衡等),适合高级场景。
  • 缺点:开发复杂,需要手动处理 offset、分区连接、Leader 查找等。

3. 手动消费分区数据(低级 API 示例)

通常情况下,使用 subscribe() 方法让 Kafka 自动为消费者分配分区。但在某些场景下,我们需要指定要消费的分区,例如:

  • 程序将某个指定分区的数据保存到外部存储(如 Redis、MySQL),只需要消费该指定分区。
  • 程序具有高可用性,在故障重启时需要从指定的分区重新开始消费(如 Flink、Spark 应用)。

步骤:

  1. 不再使用 subscribe()方法订阅主题,而是使用 assign() 方法直接指定需要消费的分区。
  2. 指定分区后,像之前一样在循环中调用 poll() 方法消费消息。
java 复制代码
// 指定要消费的分区
String topic = "test";
TopicPartition partition0 = new TopicPartition(topic, 0);
TopicPartition partition1 = new TopicPartition(topic, 1);
consumer.assign(Arrays.asList(partition0, partition1));

// 然后正常 poll 消费
while (true) {
    ConsumerRecords<String, String> records = consumer.poll(Duration.ofSeconds(5));
    for (ConsumerRecord<String, String> record : records) {
        // 处理消息
    }
}

七、Leader和Follower

在Kafka中,每个topic都可以配置多个分区以及多个副本。每个分区都有一个 leader 以及0个或者多个follower,在创建topic时,Kafka会将每个分区的 leader 均匀地分配在每个broker上。

正常使用kafka是感觉不到leader、follower的存在的。但其实,所有的读写操作都是由leader处理,而所有的 follower 都复制 leader 的日志数据文件,如果 leader出现故障时,follower就会被选举为 leader。

  • Kafka中的leader负责处理读写操作,而follower只负责副本数据的同步
  • 如果leader出现故障,其他follower会被重新选举为leader;
  • follower像一个consumer一样,拉取leader对应分区的数据,并保存到日志数据文件中。

1.AR、ISR、OSR

选举之前,先要明确几个概念Kafka中,把follower可以按照不同状态分为三类AR、ISR、OSR

  • 分区的所有副本称为「AR」(Assigned Replicas------已分配的副本)
  • 所有与leader副本保持一定程度同步的副本(包括leader副本在内)组成[ISR」(In-Sync Replicas------在同步中的副本)。
  • 由于follower副本同步滞后过多的副本(不包括leader副本)组成[OSR」(Out-of-Sync Replias)
  • AR =ISR + OSR
  • 正常情况下,所有的follower副本都应该与leader副本保持同步,即AR=ISR,OSR集合为空。

2.Controller与Leader选举

因为 Kafka 的吞吐量很高、延迟很低,所以选举 Leader 必须非常快。

Kafka 启动时,会在所有的 Broker 中选择一个 Controller。注意:

  • 前面所说的 Leader 和 Follower 是针对 Partition 层面的;
  • Controller 是针对 Broker 层面的。

创建 Topic、添加分区、修改副本数量等管理任务,都是由 Controller 完成的。Kafka 分区 Leader 的选举,也是由 Controller 决定的。

(1)Controller 的选举过程

  • 在 Kafka 集群启动时,每个 Broker 都会尝试在 ZooKeeper 上注册成为 Controller(通过创建临时节点)。
  • 最终只有一个 Broker 竞争成功,成为 Controller;其他 Broker 会注册该节点的监视器(Watcher)。
  • 一旦该临时节点状态发生变化(例如 Controller 宕机导致节点消失),其他 Broker 会立即重新竞争,注册成为新的 Controller。
  • Controller 也是高可用的:一旦某个 Broker 崩溃,其他 Broker 会重新注册为 Controller。

(2)Partition Leader 的选举过程

优先选ISR中的,如果全部宕机就设置Leader 为 -1(表示无可用 Leader)

所有 Partition 的 Leader 选举都由 Controller 决定:

  • Controller 会将 Leader 的改变直接通过 RPC 的方式通知需要为此作出响应的 Broker。
  • Controller 读取当前分区的 ISR,只要还有一个 Replica 幸存,就选择其中一个作为 Leader;
  • 如果该 Partition 的所有 Replica 都已经宕机,则新的 Leader 为 -1(表示无可用 Leader)。

3.Leader的负载均衡

Leader选举后Topic1-Leader和Topic2-Leader都在同一个Broker ,此时需要进行负载均衡让Leader尽量均匀分配

在 ISR 列表中,第一个 Replica 就是 preferred-replica(优先副本)。

  • 第一个分区存放的 Broker,就是 preferred-replica。
  • Kafka 会尽量让 Leader 分布在不同的 Broker 上,并且可以通过工具触发优先副本的 Leader 选举,以实现 Leader 的负载均衡。

在生产环境中,定期执行 kafka-leader-election.sh 可以使 Leader 分布更加均匀,避免某个 Broker 承担过多的 Leader 角色。

八、生产消费数据的工作流程

1.生成数据流程

生产者向 Kafka 写入一条消息时,背后经历了以下步骤:

  1. 获取分区 Leader 信息

    生产者首先向任意一个 Broker 发送 元数据请求 (实际开发中,生产者配置了 bootstrap.servers,会先连接该地址获取集群元数据)。

    在旧版 Kafka(依赖 ZooKeeper)中,生产者可以从 ZooKeeper 的 /brokers/topics/{topic}/partitions/{partition}/state节点找到该分区的 Leader 所在的 Broker ID。

    (新版本中,元数据通过 Broker 内部的 Metadata 请求获取,不再直接访问 ZooKeeper。)

  2. 连接 Leader Broker

    生产者根据获取到的 Broker ID,连接到该 Broker 上的分区 Leader。

  3. Leader 写入本地日志

    Leader 将消息顺序追加到本地的分区日志文件(.log 文件)中。

  4. Follower 同步数据

    分区下的所有 Follower 副本(属于 ISR 集合中的)主动从 Leader 拉取新的消息,写入各自的本地日志文件中。

  5. Follower 发送 ACK

    每个 Follower 完成同步后,向 Leader 发送确认(ACK)。

  6. Leader 返回 ACK 给生产者

    当 Leader 接收到所有 ISR 副本的 ACK(具体数量由生产者 acks参数决定)后,才会向生产者返回成功响应。

简化理解:生产者找到分区的 Leader → Leader 存消息 → Follower 来复制 → 复制完成 → Leader 告诉生产者"成功"。

2.消费数据流程

Kafka 采用 拉取模型(Pull),消费者主动向 Broker 请求数据,而不是 Broker 主动推送。

  1. 确定消费分区

    消费者加入消费者组后,根据分区分配策略(默认 RangeAssignor)获得自己要消费的分区列表。

  2. 获取消费偏移量(offset)

    消费者从 内部主题 __consumer_offsets(旧版存在 ZooKeeper 中)读取自己对应的 offset。

    • 如果是首次消费,根据 auto.offset.reset 配置决定从最早(earliest)或最新(latest)开始。
    • 如果是续消费,则从上次提交的 offset 之后继续。
  3. 找到分区 Leader

    消费者通过元数据请求找到每个分区当前的 Leader Broker(因为 Leader 可能发生切换)。

  4. 拉取消息

    消费者向 Leader Broker 发送 Fetch 请求,拉取从指定 offset 开始的一批消息。

  5. 消费处理

    消费者收到消息后执行业务逻辑(例如打印、存储、计算等)。

  6. 提交 offset

    消费者消费完消息后,将当前已消费的 offset 提交到 __consumer_offsets 主题(自动或手动)。

    • 自动提交:按固定时间间隔提交(enable.auto.commit=true)。
    • 手动提交:调用 commitSync() 或 commitAsync() 精确控制提交时机。

重要特性:消费者可以任意重置 offset,从而重新消费历史消息或跳过某些消息。例如将 offset 重置为 0,即可从头开始消费。

九、kafka数据的存储格式

1.基本存储结构

  • 一个 Topic 可以包含多个 Partition(分区)
  • 每个Partition 在磁盘上对应一个目录,目录中包含多个 Segment(段)
  • 每个 Segment 由三个文件组成:
    • .log 文件:实际存储消息数据
    • .index 文件:稀疏索引,记录每条消息的 offset 在 .log 文件中的物理位置(偏移量)。方便根据 offset 快速定位消息。。
    • .timeindex 文件:按时间戳索引,用于根据时间查找消息

Segment 可以理解为一个「数据分块」。Kafka 不会把所有消息塞进同一个大文件,而是把分区内的消息切分成多个小文件,每个小文件就是一个 Segment。
每个 Segment 有大小限制(默认 log.segment.bytes = 1GB,即 1024×1024×1024 字节)。

当一个 Segment 写满 1GB 后,Kafka 会自动创建新的 Segment 文件继续写入。
删除过期消息时,直接删除整个 Segment 文件即可,便于消息清理 ,同时,根据 offset 可以快速定位到某个 Segment,再在 Segment 内查找具体消息,提高查找效率

2.文件名规则

每个 Segment 的文件名都以该 Segment 中第一条消息的 offset 作为文件名(固定 20 位数字,不足前补零)。例如:

bash 复制代码
00000000000000000000.log
00000000000000000123.log
00000000000000000456.log

因为每个分区的起始 offset 是 0,所以第一个 Segment 的文件名一定是 00000000000000000000。

当第一个 Segment 写满 1GB 后,第二个 Segment 的第一条消息的 offset 是 123,那么它的文件名就是 00000000000000000123.log。

这种命名方式可以直接根据 offset 大小定位到对应的 Segment:Segment 文件名 ≤ 目标 offset < 下一个 Segment 文件名。

3.读取消息

假设消费者要读取分区中 offset = 350 的消息。查找步骤如下:

  1. 定位 Segment

    由于每个 Segment 的文件名就是它的起始 offset,Kafka 会找到满足「起始 offset ≤ 350」的最大文件名。

    例如:存在 00000000000000000300.log(起始 300)和 00000000000000000400.log(起始 400),则目标 Segment 是 00000000000000000300。

  2. 在 Segment 内查找

    打开对应的 .index 文件,此时相对 offset = 50 ,每条消息大小不同,因此需要寻找在这个 .log 文件,二分查找 offset 350 在索引中的位置,获取该 offset 在 .log 文件中的物理偏移量(例如 2048 字节)。

  3. 读取消息

    跳转到 .log 文件的 2048 字节位置,读取一条完整的消息返回。

二分查找 offset 350 在索引中的位置可能不会明确定位,例如二分查找到了40,得到前面 40 条消息总长度为 4096 字节,此时就需要从 4096 字节处开始,依次读取消息直到找到第50个消息处
注意:这里的 offset(例如 350)是相对于整个分区的全局 offset ,而每个 Segment 内部的索引是基于文件内局部偏移的。

4. 日志清理(消息过期删除)

Kafka 不会无限存储消息。它会根据配置定期删除旧的 Segment 文件。

  • 删除策略:一次删除整个 Segment 文件,不会单独删除其中的某条消息。
  • 触发条件(由 server.properties 配置):
    • log.retention.hours:消息保留时间(默认 168 小时 = 7 天)
    • log.retention.bytes:分区总大小限制(默认 -1,无限制)
    • log.segment.bytes:单个 Segment 最大大小(默认 1GB)
  • 执行方式:Kafka 的日志管理器(LogManager)会定期扫描,删除那些创建时间超过保留期限的 Segment 文件,或者当总大小超过限制时删除最老的 Segment。

正是因为按 Segment 为单位删除,Kafka 才能在不影响其他消息的情况下高效清理旧数据。

十、消息不丢失

1.Broker数据不丢失------副本

Broker 层面通过 多副本机制保证数据不丢失:

  • 生产者将消息写入分区的 Leader 副本后,所有 ISR(In-Sync Replicas) 中的 Follower 会从 Leader 拉取数据并同步。
  • 只要 ISR 中至少有一个 Follower 保持同步,即使 Leader 崩溃,Kafka 也能从 ISR 中选举出新的 Leader,已写入的消息不会丢失。

关键配置:

  • min.insync.replicas:指定 ISR 的最小副本数(例如设为 2),配合生产者的 acks=all,确保消息至少被写入指定数量的副本后才算成功。
  • unclean.leader.election.enable=false:禁止 ISR 以外的副本被选为 Leader,防止数据不一致。

2.生产者数据不丢失------ACK机制

生产者通过 ACK 机制 控制消息可靠性,共有三个可选配置:

acks 值 含义 可靠性 性能
0 生产者只负责发送,不等待任何确认 最低(可能丢失) 最高
1 Leader 收到消息后即返回确认 中等(Leader 宕机会丢数据) 中等
all 或 -1 Leader 等待所有 ISR 副本确认后才返回 最高 较低

生产者可以采用同步或异步两种方式发送数据:

  • 同步发送:调用 send() 后等待 Future.get() 返回结果,能直接感知发送是否成功。
  • 异步发送:提供回调函数(Callback),发送完成后回调 onCompletion() 方法,在回调中处理成功或失败逻辑。

注意事项:

  • 如果 Broker 迟迟不返回 ACK(例如网络问题),生产者的缓冲区(buffer)可能填满。可以通过配置 max.block.msrequest.timeout.ms 来控制超时行为,也可以设置 retries 让生产者自动重试。
  • 开启幂等性(enable.idempotence=true)可避免因重试导致的消息重复。

3.消费者数据不丢失------offset管理

消费者通过管理 offset(偏移量)来保证数据不丢失。核心原则是:先处理消息,后提交 offset。

  • 消费者端的三种语义保障机制如下
语义 描述 实现方式
At most once(最多一次) 消息可能丢失,但不会重复处理 先提交 offset,后处理消息。若处理过程失败,消息不会重试。
At least once(最少一次) 消息不会丢失,但可能重复处理 先处理消息,后提交 offset。若提交前崩溃,重启后会重新消费。
Exactly once(精确一次) 消息既不丢失也不重复 需要结合幂等性、事务或外部存储的原子提交。

Exactly-Once 的实现

  • 消息丢失:消费者先提交 offset,再处理消息。处理过程中消费者崩溃,重启后将从已提交的 offset 之后继续消费,导致崩溃前已拉取但未处理的消息丢失。
  • 重复消费:消费者先处理消息,再提交 offset。若处理完成后、提交 offset 前消费者崩溃,重启后会重新消费该批消息,导致重复处理。

实现 Exactly-Once的方案如下:

  • 使用数据库事务:将消费到的消息处理结果和该消息的 offset 保存在同一个数据库事务中,实现简单,但会降低吞吐量:

    • 事务提交时,业务数据和 offset 同时持久化;
    • 事务回滚时,两者都不生效,消费者可重新拉取并重试。
  • 使用 Kafka 事务 API:该方案通常用于Kafka Streams 或需要同时读写 Kafka 的场景,配置复杂度较高。

    • 生产者端设置 transactional.id,使用 beginTransaction() / commitTransaction() 包裹消息发送;
    • 消费者端设置 isolation.level=read_committed,只读取已提交的事务消息。
  • 借助流处理框架:在 Flink、Spark Streaming 等流处理框架中,框架本身提供了 Exactly-Once 保障:

实践建议:对于大多数业务系统,数据库事务方案是最直观可靠的选择。如果对吞吐量要求极高且上下游都是 Kafka,可考虑 Kafka 事务 API。

十一、数据积压

数据积压(Message Backlog)指生产者发送消息的速度超过了消费者处理消息的速度,导致未消费的消息在 Kafka 中不断堆积,消费者 Lag(滞后)越来越大。

常见原因

  • 消费者处理能力不足:消费者业务逻辑复杂(如写数据库、调用外部接口),单条消息处理耗时过长。
  • 网络延迟:消费者与 Kafka Broker 之间的网络不稳定,拉取数据耗时增加。
  • 下游系统故障:例如消费者将数据写入 MySQL 时,MySQL 连接超时或写入失败,导致消费者阻塞或不断重试。
  • 分区数不足:消费者组内的消费者数量远小于分区数,单个消费者需要处理多个分区,负载过高。
  • 消费者 Rebalance 频繁:频繁的 Rebalance 会导致消费者短暂停止工作,加剧积压。

积压的解决办法

  • 增加消费者数量(不超过分区数):优化消费者业务逻辑,减少单条消息处理时间;
  • 增加 Topic 的分区数(注意:分区数只能增加不能减少);
  • 监控消费者 Lag 指标(如使用 Kafka 自带的 kafka-consumer-groups 命令或 Prometheus 等),设置阈值告警,提前干预。
  • 确保下游存储(如 MySQL、Redis)性能足够,或使用异步写入方式。

十二、数据清理

Kafka 的消息存储在磁盘中,为了控制磁盘占用空间,需要定期清理过期或不再需要的消息。清理的基本单位是 Segment(日志分段)。Kafka 提供了两种日志清理方式:

  • 日志删除(Log Deletion):按照时间或大小直接删除符合条件的 Segment 文件。
  • 日志压缩(Log Compaction):针对有相同 key 的消息,只保留最新的 value(适用于维护状态类数据,如配置变更)。

(1)基于时间的保留策略

Kafka 通过以下参数设置消息保留时间,超时后自动删除:

  • log.retention.hours 保留小时数(默认 168,即 7 天)
  • log.retention.minutes 保留分钟数
  • log.retention.ms 保留毫秒数(优先级最高)

优先级:log.retention.ms >log.retention.minutes > log.retention.hours。

如果同时配置了多个,Kafka 会使用优先级最高的那个。

(2)基于日志大小的保留策略

通过 log.retention.bytes 参数限制分区总大小(默认 -1,表示无限制)。当日志总大小超过该阈值时,Kafka 会删除最旧的 Segment 文件,直到总大小低于阈值。

注意:这里针对的是每个分区的大小(而非 Topic 总大小)。如果要限制 Topic 总大小,需要将 log.retention.bytes 乘以分区数。

(3)删除过程:当满足时间或大小条件时,Kafka 的日志清理定时任务会执行以下步骤:

  1. 标记删除:从日志文件对象维护的跳跃表(索引结构)中移除待删除的 Segment,确保没有线程正在读取这些文件。
  2. 重命名文件:将待删除的 .log、.index、.timeindex 文件添加 .deleted 后缀(例如 00000000000000000000.log.deleted)。
  3. 物理删除:Kafka 的后台线程会定期扫描 .deleted 后缀的文件,并根据 file.delete.delay.ms 参数(默认 60000 毫秒,即 1 分钟)延迟后真正删除文件。

这种先标记、后删除的机制保证了正在被读取的 Segment 不会突然消失,避免读取异常。

十三、面试问题

1.Kafka 和 RocketMQ、RabbitMQ的区别

对比维度 Kafka RocketMQ RabbitMQ
开发语言 Scala/Java Java Erlang
核心定位 高吞吐分布式流处理平台 金融级互联网消息中间件 企业级通用消息中间件
原生场景 海量日志/事件采集、实时流计算、大数据管道 电商/金融核心业务、事务消息、顺序消息 企业异构系统通信解耦、复杂路由
吞吐量 百万级 TPS(最高) 十万级 TPS 万级 TPS
延迟 毫秒级(约 2-5ms) 毫秒级 微秒级(最低延迟)
消息模型 发布/订阅(拉模式) 发布/订阅(拉模式+推模式) 点对点 + 发布/订阅(推模式为主)
顺序消息 分区内有序 支持严格全局有序 队列内有序
事务消息 支持(0.11+版本) 原生支持(金融级) 不支持
延迟消息 不支持(需插件) 原生支持 原生支持(插件增强)
死信队列 不支持原生 支持 支持
存储设计 分区顺序日志(磁盘) 顺序写 + 索引 内存 + 磁盘
海量堆积性能 无性能损耗 10亿级无性能下降 海量堆积性能急剧下降
扩展方式 水平扩展(增加分区/Broker) 水平扩展 镜像集群,横向扩容弱
协议支持 Kafka 自定义协议 兼容 RocketMQ 协议 + 部分 Kafka 协议 AMQP 协议(跨语言优势强)
生态集成 大数据生态完善(Flink、Spark、Hadoop 等) 阿里生态为主 多语言客户端成熟

2.Kafka 高吞吐的原因/原理?

  • 分区并行架构 :一个 Topic 可以拆分为多个 Partition,分布在不同 Broker 节点上。生产者和消费者可以并行地向不同分区写入和读取数据,吞吐量随分区数线性提升。同一消费者组内,每个分区只被一个消费者消费,避免了消费竞争。
  • 顺序写入 + 页缓存 :Kafka 采用追加写入 的方式,数据按到达顺序追加到日志文件末尾,磁盘顺序写入速度接近内存(比随机写入快 10 倍以上)。消息写入时先落入操作系统页缓存(Page Cache) ,由操作系统异步刷盘,生产者无需等待实际磁盘 I/O 完成,极大减少写入阻塞。读取时优先从页缓存命中,热点数据常驻内存,降低了磁盘物理 I/O 频率
  • 零拷贝技术:Kafka 利用操作系统 sendfile系统调用,实现数据直接从内核 Page Cache 传输到 Socket 缓冲区,绕过了「磁盘 → 内核 → 用户态 → 内核 → Socket」的多次数据拷贝,大幅减少 CPU 和内存开销,显著提升网络传输效率
  • 批量处理与压缩:生产者通过 batch.size(默认 16KB)积攒消息,批量发送到 Broker,减少网络请求次数;消费者通过 fetch.min.bytes 批量拉取数据,降低交互开销[reference:15]。同时,Kafka 支持 GZIP、Snappy、LZ4、ZSTD 等压缩算法,批量压缩后传输可减少 3-5 倍网络带宽和磁盘存储占用。

3.Kafka 如何保证顺序读取消息

  • Kafka 仅保证单分区内的消息有序,跨分区不保证全局顺序。
  • 生产者端确保消息顺序:为了保证消息写入同一分区从而确保顺序性,生产者需要将消息发送到指定分区。可以通过自定义分区器来实现,通过为消息指定相同的Key,保证相同Key的消息发送到同一分区。
  • 消费者端保证顺序消费:消费者在消费消息时,需要单线程消费同一分区的消息,这样才能保证按顺序处理消息。如果使用多线程消费同一分区,就无法保证消息处理的顺序性。

4.RocketMQ相较于Kafka的核心区别?

RocketMQ诞生于阿里巴巴双十一高并发场景,核心目标是高可靠性和复杂业务逻辑支撑(支付、订单、库存等对数据一致性要求极高的业务)

  • 消息确认机制的核心差异
    • 生产者确认机制:两者都支持配置,但Kafka的acks参数本质上是异步刷盘的多种等待策略 ,不提供同步刷盘选项;RocketMQ则提供同步刷盘和同步双写两种更强力的选择,将消息真正写入磁盘或复制到从节点后才返回确认。
    • 消费者端的确认机制:Kafka基于Offset机制 。消费者消费后需提交Offset,但Kafka不支持消费者主动发送ACK;RocketMQ支持显式ACK确认机制。消费者处理完消息后需向Broker发送ACK,Broker才认为消息已成功消费;否则触发消息重试,消息会重新投递给其他消费者。
  • Broker架构差异
    • 元数据管理:Kafka早期依赖ZooKeeper管理集群元数据,功能强大但较重,Kafka 2.8开始引入KRaft模式 逐步替代ZooKeeper。RocketMQ采用自研的NameServer,一个极其轻量、无状态的组件,节点间不直接通信,通过Producer和Consumer定期上报信息更新元数据。
    • 存储模型(最核心):Kafka每个Partition对应一组独立的日志文件(Segment),多Topic多Partition时磁盘上文件数量爆炸,写入不同Topic的Partition可能变成随机写,造成性能下降。RocketMQ单个Broker下所有Topic的消息全部写入一个统一的CommitLog文件,从根本上将随机写变为单一文件顺序追加,所有Topic共享一个文件,数据刷盘时没有文件切换开销。(Kafka多Topic多Partition多Segment的时候文件爆炸,RocketMQ单个Broker下所有Topic写入统一的文件)
  • 高可用机制:Kafka副本基于Partition维度,Leader可自动切换到Follower。 RocketMQ副本基于Broker维度。Master宕机时,读请求可路由到Slave,但写请求会被路由到该Topic的其他Broker,不会自动将Slave升级为Master。
相关推荐
逻极4 小时前
RabbitMQ 从入门到精通:构建高可用、高性能的消息中间件系统
分布式·rabbitmq·消息中间件
懂AI的老郑4 小时前
OpenClaw:高效管理分布式Agent开发团队
分布式·ai编程
来自星星的谢广坤4 小时前
OpenClaw做分布式合适吗?
分布式·openclaw
元拓数智15 小时前
智能分析落地卡壳?先补好「数据关系+语义治理」这层技术基建
大数据·分布式·ai·spark·数据关系·语义治理
GIS数据转换器19 小时前
农村生活污水治理智慧管控平台
大数据·人工智能·分布式·数据分析·生活·智慧城市
Trouvaille ~21 小时前
【Redis篇】初识 Redis:特性、应用场景与版本演进
数据结构·数据库·redis·分布式·缓存·中间件·持久化
米高梅狮子1 天前
Ceph 分布式存储 部署
linux·运维·数据库·分布式·ceph·docker·华为云
郭龙_Jack1 天前
跨境电商 平台 - ERP - 内部子系统 交互方式总图
分布式·教育电商
喝醉酒的小白1 天前
Kafka 集群应急故障排查手册
分布式·kafka