4. 消息队列选型:Kafka核心概念、分区与副本机制、生产者与消费者设计

说到高可用数据流水线,消息队列是绕不开的环节。市面上可选的产品不少,但Kafka几乎成了事实标准。我这些年经手过的流水线项目,十有八九底层都跑着Kafka。为什么大家都选它?说白了,就三个字——扛得住。

这一章我们聚焦Kafka的核心机制。不扯虚的,直接聊分区、副本、生产者和消费者这些你每天都要打交道的概念。

4.1 Kafka核心概念:从一条消息说起

先理清几个基础概念。你想想看,一条数据从产生到被消费,在Kafka里经历了什么?

  • Producer(生产者):负责把数据扔进Kafka。谁都可以当生产者,只要它按协议发消息。
  • Consumer(消费者):从Kafka拉取数据。一个消费者可以属于一个消费者组。
  • Topic(主题):消息的逻辑分类。好比数据库里的表,同类数据放同一个Topic。
  • Partition(分区):Topic的物理分片。一个Topic可以拆成多个Partition,分布在不同的Broker上。
  • Broker(代理节点):Kafka集群中的一台服务器。每个Broker可以托管多个Partition。
  • Offset(偏移量):消息在Partition内的唯一序号。消费者靠它知道自己读到哪了。

一句话总结:Producer往Topic发消息,Topic按Partition分散存储,Consumer按Offset顺序消费。就这么简单。

我记得刚接触Kafka时,总觉得概念太多。后来发现,只要抓住「分区」这个核心,其他都是围绕它转的。

4.2 分区机制:Kafka高可用的基石

分区是Kafka实现水平扩展和高可用的关键。没有分区,Kafka就是一个单机消息队列,跟Redis List没什么区别。

4.2.1 分区的作用

  • 并行处理:每个分区可以被一个消费者独立消费。分区越多,并行度越高。
  • 数据冗余:分区可以设置副本,分布在不同的Broker上。一台机器挂了,其他副本顶上。
  • 顺序保证:Kafka只保证分区内的消息有序。跨分区不保证顺序。

注意:分区数不是越多越好。每个分区对应一个日志文件,过多的分区会增加文件句柄消耗和Leader选举的开销。我见过有人把分区设到上千个,结果集群元数据同步都成了瓶颈。

4.2.2 分区分配策略

当生产者发送消息时,它需要决定消息落到哪个分区。Kafka提供了几种策略:

策略 说明 适用场景
轮询(Round Robin) 消息均匀分布到所有分区 无键消息,负载均衡
按键哈希(Key Hash) 相同Key的消息进入同一分区 需要保证某类数据有序
自定义分区器 实现Partitioner接口,自定义规则 特殊业务逻辑

我个人习惯用按键哈希。比如订单数据,用订单ID做Key,同一个订单的所有事件就能保证顺序。这在处理状态机变更时特别有用。

4.3 副本机制:数据不丢的底气

副本是Kafka高可用的另一条腿。每个分区可以有多个副本,其中一个被选为Leader,其余是Follower。

4.3.1 Leader与Follower

  • Leader:负责处理读写请求。所有生产者和消费者只跟Leader打交道。
  • Follower:只做一件事——从Leader拉取数据,保持同步。
  • ISR(In-Sync Replicas):与Leader保持同步的副本集合。只有ISR里的副本才有资格成为新Leader。

为什么会设计成读写分离?你想想看,如果每个副本都能写,数据一致性就乱套了。Leader统一写,Follower异步同步,既保证了强一致性,又降低了复杂度。

4.3.2 副本配置参数

参数 默认值 说明
replication.factor 1 副本数。生产环境建议3
min.insync.replicas 1 最小同步副本数。配合acks=all使用
unclean.leader.election.enable false 是否允许非ISR副本成为Leader

我的建议:生产环境把replication.factor设为3,min.insync.replicas设为2。这样即使一台Broker挂了,数据也不会丢。我曾经遇到过一台机器磁盘损坏,就是因为副本数够,数据毫发无损。

4.4 生产者设计:怎么发消息才靠谱

生产者看起来简单——不就是发消息吗?但实际踩坑的人不少。

4.4.1 发送模式

Kafka生产者支持三种发送模式:

  • 发后即忘(Fire-and-Forget):不关心发送结果。性能最高,但可能丢消息。
  • 同步发送:等待Broker确认。可靠性高,但吞吐量低。
  • 异步发送+回调:发送后立即返回,通过回调处理结果。兼顾性能和可靠性。

我一般用异步发送+回调。既能压满带宽,又能在发送失败时做补偿处理。

4.4.2 acks参数

这个参数决定了生产者等待多少个副本确认才算发送成功。

acks值 行为 可靠性 性能
0 不等待确认 最低 最高
1 Leader确认即可 中等 中等
all(-1) 所有ISR副本确认 最高 最低

避坑指南:我曾经为了追求吞吐量,把acks设为0。结果有一次Broker宕机,丢了几十万条日志,排查了半天才发现是acks的问题。从那以后,核心数据我全用acks=all,配合min.insync.replicas=2,再没丢过数据。

4.4.3 生产者代码示例

Properties props = new Properties();
props.put("bootstrap.servers", "broker1:9092,broker2: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("batch.size", 16384);
props.put("linger.ms", 5);

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

producer.send(new ProducerRecord<>("orders", orderId, orderJson), (metadata, exception) -> {
    if (exception != null) {
        // 记录失败消息,后续补偿
        log.error("发送失败: {}", orderId, exception);
    }
});

注意batch.size和linger.ms的配合。batch.size控制批量大小,linger.ms控制等待时间。适当调大这两个值,吞吐量能翻倍。

4.5 消费者设计:怎么消费才不重复

消费者设计比生产者更复杂。因为你要处理再均衡、位移提交、消费滞后等问题。

4.5.1 消费者组与分区分配

消费者组是Kafka实现消费负载均衡的手段。一个分区只能被同一个组内的一个消费者消费。

分配策略有三种:

  • Range:按Topic分区范围分配。容易造成分配不均。
  • RoundRobin:轮询分配。比较均匀。
  • Sticky:粘性分配。尽量保持已有分配不变,减少再均衡时的变动。

我个人推荐Sticky策略。再均衡时它只移动必要的分区,对系统冲击最小。

4.5.2 位移提交

消费者消费完消息后,需要提交位移(Offset),告诉Kafka「我读到这了」。提交方式有两种:

方式 说明 风险
自动提交 每隔一段时间自动提交 可能重复消费或丢失消息
手动提交 业务处理完成后主动提交 代码复杂度高,但可控

我的做法:使用手动提交,并且在业务逻辑处理完、数据持久化之后再提交位移。这样即使消费者宕机,重启后最多重复消费一次,不会丢数据。

4.5.3 消费者代码示例

Properties props = new Properties();
props.put("bootstrap.servers", "broker1:9092,broker2:9092");
props.put("group.id", "order-consumer-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");
props.put("max.poll.records", 500);

KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
consumer.subscribe(Arrays.asList("orders"));

while (true) {
    ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
    for (ConsumerRecord<String, String> record : records) {
        // 处理业务逻辑
        processOrder(record.value());
    }
    // 处理完成后手动提交
    consumer.commitSync();
}

这里有个细节:max.poll.records控制每次拉取的最大条数。设得太大会导致处理时间过长,触发再均衡。我一般设500左右,根据消息大小和业务处理耗时调整。

4.6 知识体系总览

下面这张图把Kafka的核心机制串起来了。你可以看到消息从生产者到消费者,中间经历了分区、副本、位移管理这些环节。

Kafka核心机制总览 Producer Partition 0 Partition 1 Partition 2 副本 0 (Leader) 副本 1 (Follower) 副本 2 (Follower) Consumer Group Consumer 0 Consumer 1 关键说明 • 生产者按分区策略发送消息 • 每个分区有多个副本保证高可用 • 消费者组内每个消费者 负责不同分区

4.7 小结

Kafka的核心机制,说白了就是围绕分区和副本展开的。分区给了你并行能力,副本给了你容错能力。生产者和消费者设计时,核心就两个词——可靠性和性能的平衡。

我见过太多人只追求吞吐量,忽略了可靠性。也见过有人为了可靠,把性能压到惨不忍睹。其实没有银弹,关键是你得清楚自己的业务场景。核心交易数据,牺牲点性能也要保证不丢;日志采集,丢几条也无伤大雅。

嗯,这一章就到这里。记住一句话:理解分区,你就理解了Kafka的一半。