为什么高吞吐选 Kafka

系统解耦、异步削峰、日志采集、实时埋点,这些场景都离不开消息队列。RabbitMQ 功能全、路由灵活,但单机吞吐有限;Kafka 用“把消息追加到磁盘顺序日志”的方式,把高吞吐做到了百万级每秒,非常适合日志、行为、订单这类大数据量场景。

和 RabbitMQ 的“消息被消费完就删”不同,Kafka 把消息按分区持久化保存一段时间,消费位置靠 offset 记录。这意味着同一条消息可以被多个消费组独立消费、可以回溯重放,这正是它适合做数据管道和流处理的原因。

本文核心知识点
  • 分区topic 拆成多个 partition,是并行与扩展的基础。
  • 消费组同一组内分区被分着消费,不同组各收全量。
  • offset消费位点,决定从哪继续读。
  • 副本分区有 leader 和 follower,保证宕机不丢。
Kafka 消息平台配图:蓝色渐变背景上生产者方块汇入横向堆叠的日志块序列,右侧分出三组消费者方块,数据沿管道流动,扁平科技插画
左侧生产者把消息追加进持久化日志,右侧多个消费组各自独立读取。

topic、分区与消费组

几个名词先理清:topic 是一类消息的主题(如“订单事件”);一个 topic 拆成多个 partition(分区),分布在不同 broker 上实现并行;每条消息进分区后按顺序追加,带一个递增编号叫 offset。

消费组(consumer group)是 Kafka 最巧妙的设计:同一个消费组里,各分区只会被组内一个消费者消费,从而横向扩容;而不同的消费组彼此独立,每个组都能收到全量消息。于是“订单服务”和“数据分析”可以挂在同一 topic 上各取所需,互不影响。

生产者与消费者代码

生产者把消息发到 topic,可按 key 决定进哪个分区(相同 key 进同一分区,保证有序):

props.put("bootstrap.servers", "localhost:9092");
props.put("key.serializer", "StringSerializer");
props.put("value.serializer", "StringSerializer");

Producer<String,String> producer = new KafkaProducer<>(props);
producer.send(new ProducerRecord<>("order-events", "order-1001", "paid"));
// 消费者:指定消费组,拉取并处理
props.put("group.id", "order-service");
props.put("auto.offset.reset", "earliest");
consumer.subscribe(Arrays.asList("order-events"));
while (true) {
  ConsumerRecords<String,String> rs = consumer.poll(Duration.ofMillis(100));
  for (ConsumerRecord<String,String> r : rs) { handle(r); }
}

消费完是否自动提交 offset,要根据业务定:自动提交简单但可能“处理失败却已提交”,生产上常用手动提交,处理成功再提交位点。

分区数与并行度

分区数直接决定这个 topic 最多能并行多少消费者:一个分区同一时刻只能被消费组内一个线程消费,所以消费并行度上限就是分区数。想让消费扩容,就得加分区。

但分区不是越多越好:分区越多,broker 上文件句柄和副本开销越大。经验上按预期峰值吞吐预估,并给未来留余量——注意分区数只能加、不能随意减少,建 topic 时就要想好。

投递语义与重试

Kafka 提供三种投递语义:至多一次(可能丢)、至少一次(可能重复、不丢)、恰好一次(不丢不重,配置复杂)。大多数业务选“至少一次”,然后让消费端幂等——同一消息处理两遍结果一样,重复就无害了。

消费失败别直接丢:记日志、重试几次,仍失败就写进死信队列(DLQ),人工或后续补偿处理。配合退避重试,避免一条脏消息把整个分区卡死。

集群、副本与高可用

Kafka 集群由多台 broker 组成,每个分区有一个 leader(负责读写)和若干 follower(同步副本)。leader 宕机时,控制器从 follower 里选一个新 leader,业务几乎无感。副本因子通常配 3,保证一台机器挂了数据还在。

生产者这边用 acks 参数控制“写成功”的严格程度:acks=all 表示等所有同步副本都写好才算成功,吞吐略降但最可靠,关键事件(如付款)建议用它。