Kafka 基础回顾:核心概念与实战要点
说实话,很多同学一上来就急着写 Delta Lake 到 Kafka 的管道,结果连 Topic 和 Partition 的关系都没搞明白。我见过太多因为基础不牢导致的线上事故了。这一章咱们就把 Kafka 的核心概念过一遍,重点讲那些你在实战中一定会踩的坑。
Topic 与 Partition:消息的「文件夹」和「分片」
Kafka 里的 Topic,你可以把它想象成一个消息的「文件夹」。生产者往里面扔数据,消费者从里面读数据。但光有文件夹还不够——单机存不下海量消息,读写也扛不住高并发。所以 Kafka 把每个 Topic 切成了多个 Partition(分区)。
每个 Partition 其实就是一个有序的、不可变的日志文件。消息进来就追加到尾部,每个消息都有一个唯一的偏移量(Offset)。
核心要点:Partition 是 Kafka 并行度的基本单位。一个 Partition 只能被同一个 Consumer Group 里的一个消费者线程消费。这意味着,你的消费者数量如果超过 Partition 数量,多出来的消费者就会闲着。
我在项目中遇到过这么个事:有个团队把 Topic 设了 3 个 Partition,然后起了 10 个消费者实例。结果一跑,7 个实例一直在空转,数据吞吐量根本没上去。你想想看,这不是浪费资源吗?
Consumer Group:消费的「团队协作」机制
Consumer Group 是 Kafka 实现「一条消息只被消费一次」的关键。同一个 Group 里的消费者共同消费一个 Topic,每条消息只会被其中一个消费者处理。
举个例子:假设 Topic 有 4 个 Partition,你的 Group 里有 2 个消费者。Kafka 会做负载均衡,每个消费者分到 2 个 Partition。如果 Group 里有 4 个消费者,那就一人一个 Partition。
| 消费者数量 | Partition 数量 | 分配结果 |
|---|---|---|
| 2 | 4 | 每个消费者 2 个 Partition |
| 4 | 4 | 每个消费者 1 个 Partition |
| 5 | 4 | 4 个消费者各 1 个,1 个空闲 |
注意:Consumer Group 的 Rebalance 是个很重的操作。我曾经在生产环境遇到过频繁 Rebalance 导致消费延迟飙升的情况。原因是一个消费者处理太慢,心跳超时被踢出 Group,然后触发 Rebalance,其他消费者重新分配 Partition,整个过程可能持续几十秒。所以,一定要合理设置 session.timeout.ms 和 max.poll.interval.ms。
生产者 API:发消息的「三种模式」
Kafka 生产者发送消息有三种确认模式,我习惯叫它们「火、水、土」——开个玩笑,其实就是 acks 参数的三个值。
- acks=0:生产者发出去就不管了。最快,但可能丢消息。适合日志、监控等可以容忍少量丢失的场景。
- acks=1:Leader 分区收到消息就返回确认。这是默认值,性能和可靠性比较均衡。
- acks=all:所有 ISR(In-Sync Replicas)副本都写入成功才返回。最安全,但延迟最高。
来看一段生产者的核心代码:
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");
props.put("acks", "all");
props.put("retries", 3);
props.put("enable.idempotence", true); // 开启幂等性,防止重复
Producer<String, String> producer = new KafkaProducer<>(props);
for (int i = 0; i < 100; i++) {
producer.send(new ProducerRecord<>("my-topic", Integer.toString(i), "message-" + i),
(metadata, exception) -> {
if (exception != null) {
System.err.println("发送失败: " + exception.getMessage());
}
});
}
producer.close();
我的经验:生产环境我一般用 acks=all + enable.idempotence=true。虽然慢一点,但能保证「至少一次」语义。如果你对延迟特别敏感,可以试试 acks=1,但要做好丢消息的心理准备。
消费者 API:拉模式与偏移量管理
Kafka 消费者用的是「拉模式」——消费者主动去 Broker 拉数据。这和传统的消息队列(比如 ActiveMQ 的推模式)不一样。拉模式的好处是消费者可以自己控制消费速度,不会把消费者压垮。
偏移量管理是消费者最头疼的问题。Kafka 默认把偏移量提交到一个叫 __consumer_offsets 的内部 Topic 里。你可以选择自动提交(默认每 5 秒一次)或者手动提交。
我个人建议:生产环境一定要用手动提交。为什么?自动提交有个问题:你拉了一批消息,还没处理完,偏移量就提交了。这时候消费者挂了,重启后就会跳过这批消息,造成数据丢失。
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("group.id", "my-group");
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("enable.auto.commit", "false"); // 关闭自动提交
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
consumer.subscribe(Arrays.asList("my-topic"));
try {
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());
}
// 处理完一批消息后再手动提交
consumer.commitSync();
}
} finally {
consumer.close();
}
避坑指南:我曾经犯过一个错误——在循环里每处理一条消息就提交一次偏移量。结果提交太频繁,把 __consumer_offsets Topic 写爆了,导致整个集群性能下降。正确的做法是:批量拉取,批量处理,批量提交。
消息可靠性保证:从生产到消费的全链路
消息可靠性是个系统工程,不是靠一个参数就能搞定的。我把它拆成三个阶段来看:
- 生产端:用
acks=all+ 重试 + 幂等性,保证消息不丢、不重复。 - Broker 端:设置
min.insync.replicas=2,确保至少有两个副本同步。配合replication.factor=3,即使一台 Broker 挂了,数据还在。 - 消费端:手动提交偏移量,处理完业务逻辑再提交。如果处理失败,不要提交,让消费者重新拉取。
下面这张图把整个链路串起来了:
嗯,这里要注意一点:Kafka 的「Exactly-Once」语义是有条件的。它只能在生产者到单个 Partition 的范围内保证。如果你要跨 Partition 或者跨 Topic 保证 Exactly-Once,那就得靠事务 API 了。这个我们在后面讲 Delta Lake 集成的时候会用到。
总结一下:Kafka 的核心概念其实不复杂,但每个细节都藏着坑。Topic 和 Partition 决定了你的并行度,Consumer Group 决定了你的消费模式,生产者和消费者的 API 参数直接决定了消息的可靠性。把这些基础打牢了,后面做 Delta Lake 实时写入才能游刃有余。