4、Kafka深入:Topic与Partition机制、生产者与消费者API、消息可靠性保证(ACK、ISR)、Offset管理

Kafka 这东西,说白了就是一个超级能扛的分布式消息队列。很多初学者觉得它就是个「发消息、收消息」的工具,但真正用起来,坑不少。我最早接触 Kafka 是在一个日志采集项目里,那时候对 Topic 和 Partition 的理解很浅,结果线上出了好几次数据倾斜的问题。嗯,今天咱们就把这块彻底聊透。

4.1 Topic 与 Partition 机制

Topic 是 Kafka 里消息的逻辑分类。你往 Kafka 里写数据,必须先指定一个 Topic。但真正承载数据的,是 Partition。

每个 Topic 可以分成多个 Partition。Partition 是一个有序的、不可变的消息序列。每条消息在 Partition 里都有一个唯一的偏移量(Offset)。

核心要点:Partition 是 Kafka 并行度的基本单位。一个 Partition 只能被同一个消费组里的一个消费者线程消费。所以,Partition 数量决定了消费端的最大并行能力。

我在项目中遇到过一个问题:某个 Topic 只设了 3 个 Partition,但消费端起了 10 个线程。结果呢?只有 3 个线程在工作,剩下 7 个闲着。浪费资源不说,处理速度还上不去。

Partition 的数据存储是有规律的。每个 Partition 对应磁盘上的一个目录,目录名就是 Topic名称-Partition编号。比如 order-topic-0、order-topic-1。

Partition 内部又分成多个 Segment 文件。Kafka 不会一直保留所有数据,它会根据配置的保留策略(按时间或按大小)自动清理旧的 Segment。

我的建议:Partition 数量不是越多越好。每个 Partition 对应一个 Leader 副本,Leader 越多,集群的元数据管理压力越大。一般建议 Partition 数量不超过集群总磁盘数的 10 倍。比如你有 10 台机器,每台 10 块盘,那 Partition 总数控制在 1000 以内比较稳妥。

4.2 生产者 API 与分区策略

生产者是往 Kafka 写数据的客户端。Kafka 提供了 Java 版的 Producer API,用起来很直接。

Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");

Producer<String, String> producer = new KafkaProducer<>(props);
producer.send(new ProducerRecord<>("my-topic", "key1", "value1"));
producer.close();

这段代码看着简单,但有个关键点:消息到底发到哪个 Partition?

Kafka 的分区策略有三种:

  • 指定分区:你在 ProducerRecord 里直接指定 Partition 编号。适合需要严格顺序的场景。
  • 按 Key 哈希:不指定 Partition,但指定 Key。Kafka 对 Key 做哈希,相同 Key 的消息永远进同一个 Partition。这能保证同一个 Key 的消息有序。
  • 轮询:Key 和 Partition 都不指定。Kafka 用轮询方式把消息均匀分布到各个 Partition。这是默认策略。

注意:如果你既没指定 Partition,也没指定 Key,Kafka 2.4 之前用的是轮询,2.4 之后改成了黏性分区(Sticky Partitioner)。黏性分区会先把一批消息发到同一个 Partition,满了再换下一个。这样做能减少网络连接开销,提升吞吐量。

我曾经踩过一个坑:某个业务要求同一个用户的消息必须有序消费。我用了默认的轮询策略,结果同一个用户的消息被分散到不同 Partition,消费端处理时顺序全乱了。后来改成按用户 ID 哈希分区,问题解决。

4.3 消费者 API 与消费组

消费者从 Kafka 拉取数据。Kafka 的消费模式是拉模式(Pull),不是推模式(Push)。消费者主动去 Broker 拉数据,拉多少自己控制。

Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("group.id", "my-group");
props.put("key.deserializer", "org.apache.common.serialization.StringDeserializer");
props.put("value.deserializer", "org.apache.common.serialization.StringDeserializer");

Consumer<String, String> consumer = new KafkaConsumer<>(props);
consumer.subscribe(Arrays.asList("my-topic"));

while (true) {
    ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
    for (ConsumerRecord<String, String> record : records) {
        System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
    }
}

消费组是 Kafka 实现高可用的关键。同一个消费组里的消费者,共同消费一个 Topic。每个 Partition 只能被组内的一个消费者消费。

你想想看,如果消费组里有 3 个消费者,Topic 有 6 个 Partition,那每个消费者分到 2 个 Partition。如果其中一个消费者挂了,剩下的 2 个消费者会重新分配那 2 个 Partition。这就是再均衡(Rebalance)。

再均衡的代价:Rebalance 期间,整个消费组会停止消费。如果频繁 Rebalance,消费延迟会很高。我建议你把 session.timeout.ms 设得合理一些,别让 Kafka 误判消费者挂了。

4.4 消息可靠性保证:ACK 与 ISR

消息可靠性是 Kafka 的核心能力。Kafka 通过 ACK 机制和 ISR 机制来保证消息不丢。

4.4.1 ACK 机制

生产者发送消息时,可以设置 acks 参数。这个参数决定了生产者要等多少个副本确认,才算发送成功。

ACK 值 含义 可靠性 性能
acks=0 生产者发完就认为成功,不等待任何确认 最低,可能丢消息 最高
acks=1 等待 Leader 副本确认 中等,Leader 挂了可能丢 中等
acks=all 等待所有 ISR 副本确认 最高,不会丢消息 最低

我在项目中处理金融交易数据时,用的就是 acks=all。虽然性能差一些,但数据不能丢。如果是日志采集场景,用 acks=1 就够了,丢几条日志影响不大。

4.4.2 ISR 机制

ISR 的全称是 In-Sync Replicas,也就是「与 Leader 保持同步的副本集合」。

Kafka 的每个 Partition 有多个副本,其中一个叫 Leader,其余叫 Follower。Follower 会从 Leader 拉取数据,保持同步。只有那些跟 Leader 差距不大的 Follower,才会被列入 ISR。

如果某个 Follower 同步太慢,或者挂了,Leader 会把它踢出 ISR。当 acks=all 时,生产者只需要等待 ISR 列表里的所有副本确认即可。

避坑指南:我曾经把 min.insync.replicas 设成了 2,但集群里每个 Partition 只有 2 个副本。结果一个 Broker 挂了,ISR 里只剩 1 个副本,小于 2,生产者直接报错。后来我把副本数改成 3,min.insync.replicas 设成 2,这样挂一个 Broker 还能正常工作。

4.5 Offset 管理

Offset 是消息在 Partition 里的位置编号。消费者消费完一条消息后,需要提交 Offset,告诉 Kafka「我已经消费到这儿了」。这样下次重启时,消费者能从上次的位置继续消费。

Offset 的提交方式有两种:

  • 自动提交:默认开启。消费者每隔 auto.commit.interval.ms(默认 5 秒)自动提交 Offset。简单,但可能重复消费或丢失消息。
  • 手动提交:你自己控制提交时机。可以同步提交(commitSync)或异步提交(commitAsync)。

注意:自动提交有个问题:如果消费者拉了一批消息,处理到一半挂了,Offset 还没提交。重启后,消费者会从上次提交的 Offset 开始消费,那批没处理完的消息就丢了。所以,对数据一致性要求高的场景,一定要用手动提交。

手动提交的典型做法是「先处理,后提交」:

while (true) {
    ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
    for (ConsumerRecord<String, String> record : records) {
        // 处理消息
        process(record);
    }
    // 处理完再提交
    consumer.commitSync();
}

Offset 的存储位置也值得一提。老版本 Kafka 把 Offset 存在 ZooKeeper 里,但 ZooKeeper 不适合高并发写入。从 0.9 版本开始,Kafka 把 Offset 存在一个内部 Topic 里,叫 __consumer_offsets。这个 Topic 默认有 50 个 Partition,按消费组 ID 哈希存储。

你可以用 kafka-consumer-groups 命令查看消费组的 Offset 情况:

kafka-consumer-groups --bootstrap-server localhost:9092 --group my-group --describe

输出结果会显示每个 Partition 的当前 Offset、Log End Offset 和 Lag(滞后量)。Lag 是衡量消费延迟的关键指标。如果 Lag 持续增长,说明消费速度跟不上生产速度。

我的经验:监控 Lag 是 Kafka 运维的必修课。我一般会在 Prometheus 里配一个告警规则:如果某个消费组的 Lag 超过 10000,就发告警。这样能及时发现消费瓶颈。

4.6 本章知识体系总览

下面这张图把本章的核心知识点串起来了。你可以看到 Topic、Partition、生产者、消费者、ACK、ISR、Offset 之间的关系。

Kafka 核心机制知识体系 Topic(逻辑分类) Partition(物理存储) 生产者 Producer 消费者 Consumer ACK 确认机制 ISR 同步副本集 Offset 偏移量 消费组 Group Partition 是并行度基础,ACK+ISR 保证不丢消息 Offset 记录消费位置,消费组实现水平扩展

这张图把本章的核心脉络理清楚了。Topic 是逻辑入口,Partition 是物理存储单元。生产者通过 ACK 和 ISR 保证写入可靠性,消费者通过 Offset 和消费组实现有序、可扩展的消费。

嗯,Kafka 这块内容确实不少,但只要你把 Topic、Partition、ACK、ISR、Offset 这几个概念吃透了,后面搭建实时流水线就会顺畅很多。我在实际项目中,每次遇到 Kafka 问题,基本都能从这几个维度找到根因。