--- id: backend.mq.kafka title: Kafka:主题、分区、消费组、位移和顺序 type: lesson domain: backend stack: [mq] tags: [Kafka, 分区, 消费组, offset, 顺序消息, ISR] level: intermediate prerequisites: [backend.mq.rabbitmq] est_hours: 3 related: [backend.mq.reliability, backend.mq.interview] week: [14] order: 2 --- # Kafka:主题、分区、消费组、位移和顺序 ## 解释说明 **模型**:Kafka 是一个**分布式的追加日志**。消息写入**主题(Topic)**,主题分成多个**分区(Partition)**,每个分区是一个只追加的有序日志,消息有递增的**位移(offset)**。消息被消费后**不会删除**,按保留时间或大小清理,所以可以回溯重新消费。 - **生产者**:消息带 key 时,同一个 key 总是进入同一个分区(按 key 哈希);没有 key 时分散写入。 - **消费组(Consumer Group)**:同一个组里,一个分区同一时间只分配给一个消费者;不同组各自独立消费全部消息。所以**消费者数量超过分区数时,多出来的消费者会空闲**——分区数决定了组内并行度上限。 - **位移提交**:消费者记录“处理到哪里了”。先处理再提交 = 至少一次(可能重复);先提交再处理 = 至多一次(可能丢)。业务一般选至少一次 + 幂等消费。 - **副本**:每个分区有多个副本,一个 leader 负责读写,follower 同步。和 leader 保持同步的副本集合叫 **ISR**。 - **生产者 acks**:`acks=0` 不等确认;`acks=1` leader 写入即确认;`acks=all` ISR 都写入才确认(配合 `min.insync.replicas` 保证不丢)。 - **顺序**:只保证**单个分区内**有序。需要“同一订单的消息有序”时,用订单 ID 作为 key。 **和 RabbitMQ 怎么选** | | RabbitMQ | Kafka | | --- | --- | --- | | 模型 | 消息被消费确认后删除 | 日志保留,可回溯 | | 优势 | 路由灵活、延迟 / 死信 / 优先级等功能多、单条消息时延低 | 吞吐量高、可回放、适合流处理 | | 典型场景 | 业务异步任务、订单流程 | 日志采集、埋点、数据同步、事件流 | 新版本 Kafka 已用 KRaft 取代 ZooKeeper 管理元数据(Kafka 4.0 起移除 ZooKeeper),部署细节以官方文档为准:https://kafka.apache.org/documentation/ ## 案例 - 埋点数据:前端事件写入 `events` 主题(几十个分区),实时计算和离线入库两个消费组各自消费。 - 订单状态变化事件:用订单 ID 作为 key,保证同一订单的“创建 → 支付 → 发货”按顺序被消费。 - 消费者 bug 导致数据处理错误:修复后把消费组位移重置到出问题前的时间点,重新消费。 ## 代码示例 Python 客户端(`confluent-kafka`,示意代码,需要运行中的 Kafka;客户端参数以官方文档为准:https://docs.confluent.io/kafka-clients/python/current/overview.html ): ```python import json from confluent_kafka import Consumer, Producer producer = Producer({"bootstrap.servers": "localhost:9092", "acks": "all", "enable.idempotence": True}) def on_delivery(err, msg): if err is not None: print("发送失败:", err) # 这里要记录并重试或告警 for status in ("created", "paid", "shipped"): # 同一订单用同一个 key → 进入同一分区 → 有序 producer.produce("order-events", key="order-1001", value=json.dumps({"status": status}), callback=on_delivery) producer.flush() consumer = Consumer({ "bootstrap.servers": "localhost:9092", "group.id": "points-service", "auto.offset.reset": "earliest", "enable.auto.commit": False, # 处理成功后手动提交,实现“至少一次” }) consumer.subscribe(["order-events"]) try: while True: msg = consumer.poll(1.0) if msg is None: continue if msg.error(): print("消费错误:", msg.error()) continue event = json.loads(msg.value()) print(msg.key(), msg.partition(), msg.offset(), event) # 业务处理要幂等 consumer.commit(message=msg, asynchronous=False) finally: consumer.close() ``` ## 易错点 - **消费者比分区多**:多出来的消费者空闲,加机器也提不了速度;要先加分区(分区只能增加不能减少,增加后 key 的分布会变,影响顺序)。 - **自动提交位移**:消息还没处理完就提交,崩溃后丢消息;或者处理完还没提交就崩溃,重复消费。业务里建议手动提交 + 幂等。 - **以为整个主题有序**:只有分区内有序。 - **单条消息处理太慢**:超过 `max.poll.interval.ms` 会被踢出消费组并触发再均衡,导致重复消费。 - **`acks=1` 又要求不丢**:leader 宕机且 follower 未同步时会丢;要 `acks=all` + `min.insync.replicas>=2` + 副本数 >=3。 ## 面试常问 见[消息队列面试题](interview.md):Kafka 为什么吞吐高、分区和消费组的关系、怎么保证顺序、怎么不丢消息、和 RabbitMQ 的区别。