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的核心机制串起来了。你可以看到消息从生产者到消费者,中间经历了分区、副本、位移管理这些环节。
4.7 小结
Kafka的核心机制,说白了就是围绕分区和副本展开的。分区给了你并行能力,副本给了你容错能力。生产者和消费者设计时,核心就两个词——可靠性和性能的平衡。
我见过太多人只追求吞吐量,忽略了可靠性。也见过有人为了可靠,把性能压到惨不忍睹。其实没有银弹,关键是你得清楚自己的业务场景。核心交易数据,牺牲点性能也要保证不丢;日志采集,丢几条也无伤大雅。
嗯,这一章就到这里。记住一句话:理解分区,你就理解了Kafka的一半。