Files
zhuangang/topics/backend/mq/02-kafka.md
T

100 lines
5.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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 的区别。