100 lines
5.0 KiB
Markdown
100 lines
5.0 KiB
Markdown
---
|
||
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 的区别。
|