本章目标:能讲清 Kafka 的定位、分区与消费组模型,能本地起一个单机 Kafka,跑通生产与消费,并回答“为什么快、怎么不丢、怎么不乱序”三个高频问题。 前置知识:会使用命令行,了解基本的程序运行方式;本章是“概念必会 + 本地模拟”,不要求生产集群经验。
1. 为什么需要它
1.1 一个没有 Kafka 的埋点系统
假设 App 每产生一个行为就调用一次后端接口,后端直接把事件写进业务库。会出现三个问题:
- 耦合:埋点字段一改,所有下游系统都要跟着改。
- 扛不住峰值:晚上八点流量是平时的十倍,数据库连接被打满。
- 下游故障会拖垮上游:推荐服务挂了,埋点接口跟着超时。 Kafka 把“写”和“读”拆开:上游只负责把事件写进 Kafka,下游按自己的节奏读。它相当于数据链路里的缓冲带和高速公路。
1.2 它解决的四类问题
| 作用 | 具体表现 | 例子 |
|---|---|---|
| 解耦 | 生产者不知道消费者是谁 | 埋点系统只管发事件,实时大屏和数据仓库各自订阅 |
| 削峰 | 峰值先写入,下游按能力消费 | 大促一分钟 100 万条,Flink 每秒处理 20 万条 |
| 异步 | 上游不用等下游处理完 | 下单成功后发一条事件,积分、推荐、风控各自处理 |
| 重放 | 消息保留一段时间,可重新消费 | 新上线一个指标,从三天前的数据开始算 |
1.3 它在数据链路里的位置
业务库 MySQL ──CDC──┐
App 埋点 ────日志采集──┤
↓
Kafka(缓冲 + 分发)
↓
┌─────────────┼─────────────┐
↓ ↓ ↓
Flink 实时计算 离线落湖/仓 其他订阅方
↓
Doris / ClickHouse / 大屏
记住一句话:Kafka 不负责计算,也不负责长期存储,它负责可靠地传和缓冲。
2. 核心概念
2.1 broker、topic、partition、offset
- broker:一个 Kafka 服务进程。三台机器各起一个 broker,就是一个三节点集群。
- topic:消息的逻辑分类,例如
order_events、review_events。相当于一个文件夹名。 - partition:topic 的物理分片。一个 topic 可以拆成多个 partition,分布在不同 broker 上。
- offset:消息在某个 partition 内的递增编号,从 0 开始。跨 partition 的 offset 没有可比性。
topic: order_events
├── partition 0: [0][1][2][3][4] ...
├── partition 1: [0][1][2] ...
└── partition 2: [0][1][2][3] ...
partition 是最重要的概念,因为:
- 它决定并行度:一个 partition 同一时刻只能被同一个消费者组里的一个消费者消费。
- 它决定顺序范围:Kafka 只保证单 partition 内有序。
- 它是副本的基本单位:每个 partition 有 1 个 leader 和若干 follower。
面试高频:topic 是逻辑概念,partition 是物理分片;offset 只在 partition 内有意义。
2.2 replica、leader、follower、ISR
每个 partition 可以配置多个副本(replication factor)。例如副本数为 3:
partition 0
leader (broker 1) ← 生产者和消费者都找它
follower (broker 2) ← 持续从 leader 拉取
follower (broker 3)
- leader:处理这个 partition 的读写请求。
- follower:同步 leader 的数据,leader 挂了可以顶上。
- ISR(In-Sync Replicas):当前与 leader 保持同步的副本集合,包含 leader 自己。落后太多的副本会被踢出 ISR。
acks=all 的含义不是“等所有副本”,而是“等当前 ISR 里的副本都确认”。所以还要配合 min.insync.replicas:
replication.factor = 3
min.insync.replicas = 2
acks = all
含义是:至少 2 个副本在 ISR 里,写入才算成功。坏掉一台机器仍然可写;坏掉两台就拒绝写入,避免把数据只写到一个随时会丢的副本上。
还有一个参数 unclean.leader.election.enable,默认 false。如果打开,允许非 ISR 副本当 leader,可能丢数据,一般不要为了可用性打开它。
2.3 controller:谁来管集群
controller 是集群里的一个特殊 broker,负责:
- 监听 broker 上下线;
- 为 partition 选举新的 leader;
- 把元数据变更同步给其他 broker。
老版本 Kafka 依赖 ZooKeeper 存元数据。新版本使用 KRaft 模式,controller 由一组 broker 内部选举产生,不再需要 ZooKeeper。KRaft 在 2.8 是预览,3.3 起达到生产可用,4.0 起只支持 KRaft。本章 Docker 示例用 Kafka 3.7 的 KRaft 单机模式。
2.4 生产者:消息怎么落到 partition
生产者发送消息时可以带一个 key。默认分区策略是:
- 有 key:对 key 做哈希,落到固定 partition;
- 无 key: newer 版本使用粘性分区,一批消息先集中写一个 partition,攒够批次再换,提升吞吐。
关键参数:
| 参数 | 作用 | 常见设置 |
|---|---|---|
acks |
需要多少副本确认 | all 最安全 |
retries |
发送失败重试次数 | 大于 0,并配合幂等 |
enable.idempotence |
生产者幂等,避免重试造成重复 | true |
linger.ms |
为了攒批最多等多久 | 5–20 ms |
compression.type |
压缩算法 | lz4、zstd |
max.in.flight.requests.per.connection |
单连接未确认请求数 | 开幂等时可到 5 |
幂等生产者的原理:broker 给每个生产者分配 PID,生产者给每个 partition 的消息带递增序列号。broker 发现重复序列号就丢弃,从而避免“生产者重试导致重复”。它只保证单个生产者会话、单个 partition 内不重复,不能解决下游重复消费。
事务用于“读 Kafka → 处理 → 写 Kafka”这种跨 partition 的原子写。配置 transactional.id,通过 initTransactions、beginTransaction、commitTransaction 控制。消费者设置 isolation.level=read_committed,只读已提交事务的消息。
2.5 消费者与消费者组
消费者从 partition 拉取消息,并提交 offset 表示“我处理到哪里了”。
消费者组(consumer group) 的规则:
- 同一个组内,一个 partition 只能分给一个消费者;
- 不同组之间互不影响,各自有各自的 offset;
- 消费者数超过 partition 数时,多出来的消费者空闲。
topic 有 3 个 partition,组 g1 有 2 个消费者:
C1 -> partition 0, 1
C2 -> partition 2
如果组 g1 再加一个 C3:
C1 -> partition 0
C2 -> partition 1
C3 -> partition 2
再加 C4:C4 空闲
消费位置存在 Kafka 的内部 topic __consumer_offsets 里,不是存在消费者本地。
auto.offset.reset 只在“这个组没有已提交 offset”时生效:
earliest:从最早的消息开始;latest:只消费新消息;none:找不到 offset 就报错。
2.6 rebalance:分区重新分配
当以下情况发生时,消费组会触发 rebalance:
- 消费者加入或离开(包括心跳超时、进程被杀);
- 订阅的 topic 变化;
- partition 数量增加。
rebalance 期间,分区会先全部收回再重新分配(eager 模式),消费会短暂停止。老版本是“全场暂停”,新版本支持 cooperative-sticky,尽量只迁移需要迁移的分区。
常见调优方向:
- 合理设置
session.timeout.ms和heartbeat.interval.ms; - 保证单条消息处理时间小于
max.poll.interval.ms,否则消费者会被认为死了; - 用
group.instance.id配置静态成员,滚动重启时减少无谓 rebalance; - 消费者数量不要远大于 partition 数。
注意:rebalance 不是“故障”,它是消费组维持负载均衡的机制;但频繁 rebalance 会让消费延迟飙升,要排查。
2.7 顺序性、acks、幂等与事务的关系
顺序性:
- Kafka 只保证单 partition 内按写入顺序消费;
- 想保证同一订单的事件有序,就用
order_id作为 key,让同一订单进同一个 partition; - 不同订单之间没有全局顺序,也不需要。
不丢、不重、有序的常用组合:
| 目标 | 生产者/ broker 配置 | 消费者配置 |
|---|---|---|
| 不丢 | acks=all、retries>0、enable.idempotence=true、副本数 3、min.insync.replicas=2、unclean.leader.election.enable=false |
关闭自动提交,处理完再手动提交 |
| 不重 | 幂等生产者解决“生产端重试重复”;跨系统重复要靠事务或下游幂等 | 下游按业务主键去重,或事务写 |
| 有序 | 相同 key 进同一 partition;开幂等后 max.in.flight<=5,不开则设为 1 |
单 partition 内单线程处理 |
要诚实理解边界:幂等生产者不等于端到端 exactly-once;消费者“处理成功但提交 offset 前挂了”,重启后会重复消费,这是至少一次语义;真正的端到端精确一次需要“可重放的数据源 + 状态/offset 一起做 checkpoint + 事务或幂等写出”,这部分在 Flink 进阶 展开。
2.8 为什么快
- 顺序写磁盘:消息只追加到日志末尾,不做随机写。机械盘顺序写也能到很高吞吐。
- 页缓存(page cache):Kafka 写入时先写操作系统页缓存,由操作系统批量刷盘,不自己维护大对象缓存。
- 零拷贝:消费时通过
sendfile把页缓存直接送到网卡,减少用户态与内核态之间的数据复制。 - 批处理 + 压缩:生产者攒批,broker 按批存储,网络和磁盘都更省。
- 分区并行:多个 partition 分布到多台 broker,读写并行扩展。
面试高频:不要只背“零拷贝”。完整的答案是“顺序写 + 页缓存 + 零拷贝 + 批量压缩 + 分区并行”。
提示:Kafka 只负责传输和缓冲,不擅长复杂 SQL 聚合。计算交给 Flink,查询服务交给 Doris / ClickHouse,T+1 校准交给离线数仓。
3. 动手示例
3.1 Docker 起一个单机 Kafka
先确认 Docker Desktop 已经启动。使用 KRaft 单机模式,不需要 ZooKeeper:
docker run -d --name kafka -p 9092:9092 apache/kafka:3.7.0
docker logs -f kafka
看到 broker 启动完成的日志后,按 Ctrl+C 退出日志查看。再确认容器在运行:
docker ps --filter name=kafka
注意:如果 9092 端口被占用,把
-p 9092:9092换成-p 19092:9092,后面所有客户端地址都要相应改成localhost:19092。如果镜像拉取失败,先检查网络或镜像源;两小时内解决不了就只保留概念,不要卡在环境上。
3.2 常用命令
创建 3 个分区、1 个副本的 topic,并查看详情:
docker exec -it kafka /opt/kafka/bin/kafka-topics.sh --bootstrap-server localhost:9092 --create --topic order_events --partitions 3 --replication-factor 1
docker exec -it kafka /opt/kafka/bin/kafka-topics.sh --bootstrap-server localhost:9092 --describe --topic order_events
用控制台生产消息,key 和 value 用冒号分隔:
docker exec -it kafka /opt/kafka/bin/kafka-console-producer.sh --bootstrap-server localhost:9092 --topic order_events --property parse.key=true --property key.separator=:
在提示符后依次输入:
O1001:{"order_id":"O1001","event_type":"pay","amount":2999}
O1002:{"order_id":"O1002","event_type":"pay","amount":59}
O1001:{"order_id":"O1001","event_type":"ship","amount":2999}
另开一个终端消费,并打印分区、offset、key:
docker exec -it kafka /opt/kafka/bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic order_events --from-beginning --property print.partition=true --property print.offset=true --property print.key=true
你会看到两条 O1001 落在同一个 partition,且 offset 递增;O1002 可能落在另一个 partition。查看消费组积压:
docker exec -it kafka /opt/kafka/bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --group g1 --describe
重置消费位置到最早(先停掉该组消费者):
docker exec -it kafka /opt/kafka/bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --group g1 --topic order_events --reset-offsets --to-earliest --execute
3.3 Python 生产与消费 demo
安装客户端后,写生产者脚本 realtime_demo/kafka_producer.py:
pip install kafka-python
import json
import time
from kafka import KafkaProducer
producer = KafkaProducer(
bootstrap_servers="localhost:9092",
acks="all",
retries=5,
linger_ms=10,
key_serializer=lambda k: k.encode("utf-8"),
value_serializer=lambda v: json.dumps(v, ensure_ascii=False).encode("utf-8"),
)
for i in range(1000):
order_id = f"O{100000 + i}"
event = {
"order_id": order_id,
"event_type": "pay",
"category": ["手机", "图书", "家电"][i % 3],
"amount": round(50 + (i % 20) * 10.5, 2),
"ts": int(time.time() * 1000),
}
producer.send("order_events", key=order_id, value=event)
producer.flush()
producer.close()
print("sent 1000 events")
消费者脚本 realtime_demo/kafka_consumer.py,手动提交 offset:
import json
from kafka import KafkaConsumer
consumer = KafkaConsumer(
"order_events",
bootstrap_servers="localhost:9092",
group_id="g1",
auto_offset_reset="earliest",
enable_auto_commit=False,
key_deserializer=lambda b: b.decode("utf-8"),
value_deserializer=lambda b: json.loads(b.decode("utf-8")),
)
for msg in consumer:
print(msg.partition, msg.offset, msg.key, msg.value)
consumer.commit()
先运行 python realtime_demo/kafka_producer.py,再运行消费者脚本,按 Ctrl+C 停止。观察同一个 key 的消息是否始终在同一个 partition。再连续发送 10 条 O1001 和 10 条 O2001,验证每组 key 只出现在一个 partition 内。
提示:partition 数一旦增加,key 到 partition 的映射会变化,历史 key 可能落到新 partition。所以“增加分区”会破坏跨时间的同 key 顺序,生产上要提前规划。
4. 常见坑
- 以为 Kafka 保证全局有序。它只保证单 partition 有序。需要业务顺序就设计 key,不要依赖单分区。
- 消费者数超过 partition 数。多出来的消费者不会提升吞吐,只会空闲。
- 开了自动提交就以为不会丢。自动提交可能“消息还没处理完,offset 已经提交”,进程崩溃后这条消息就丢了。关键链路手动提交。
- 只开
acks=all,不配min.insync.replicas。副本都掉线时仍可能写成功但不可靠;三个副本至少两个同步。 - 用
order_id做 key 后,发现某个订单的数据倾斜。大促时超级大客户可能产生海量事件,需要单独拆分或加随机后缀再在计算层合并。 - 频繁重试但有非幂等副作用。生产者重试可能重复写;下游写库要按主键 upsert 或用事务。
- 把 Kafka 当永久存储。它有保留时间,默认 7 天左右;需要长期保存要落湖或落仓。
- Docker 单机只设了 1 个副本,却照抄生产参数。本地 demo 的
replication-factor=1不具备高可用,只是方便练习。
5. 练习
练习 1:分区数与消费者数量
一个 topic 有 3 个 partition,消费者组 g1 先启动 2 个消费者,再启动第 3 个、第 4 个。分别说明分区怎么分配、第 4 个消费者的状态。
参考答案
2 个消费者时,通常一个分到 2 个 partition,另一个分到 1 个;第 3 个消费者加入后触发 rebalance,最终每人 1 个 partition;第 4 个消费者没有可分配的分区,处于空闲状态,不会提升消费能力。
结论:同一消费组的并行度上限等于 partition 数。要提升消费能力,先增加 partition,再增加消费者。
练习 2:怎么保证同一订单的事件有序
订单事件有 pay、ship、complete 三种,要求同一订单的事件按发生顺序被处理。给出 key 设计和至少两个注意事项。
参考答案
用 order_id 作为消息 key,相同 key 经过哈希后进入同一个 partition,Kafka 保证单 partition 内有序。注意四点:不要改成随机 key;增加 partition 前要规划,因为映射会变化;生产者开启幂等并把 max.in.flight.requests.per.connection 控制在 5 以内,不开幂等则设为 1,避免重试乱序;消费端对同一 partition 串行处理。
练习 3:设计“不丢”的配置
三台 broker,要求任意一台宕机时不丢已确认的消息,且仍能写入。写出副本、ISR 和生产者配置,并解释坏两台时为什么应该拒绝写入。
参考答案
replication.factor = 3
min.insync.replicas = 2
acks = all
retries > 0
enable.idempotence = true
unclean.leader.election.enable = false
坏一台时还剩 2 个同步副本,满足 min.insync.replicas=2,可以继续写。坏两台时只剩 1 个副本,ISR 不足,broker 拒绝写入(NOT_ENOUGH_REPLICAS)。这是用“暂时不可写”换“不丢数据”,比让一个落后副本当 leader 更安全。消费者侧还要处理完再提交 offset,否则“Kafka 没丢,消费端自己丢了”。
练习 4:为什么开了幂等生产者还会重复
生产者已经配置 enable.idempotence=true,为什么下游数据库里仍可能出现重复订单?给出两种解决方案。
参考答案
幂等生产者只解决“生产者重试导致的 broker 端重复”。它不能解决:消费者处理成功、提交 offset 前崩溃,重启后从旧 offset 重新消费;下游写入超时,但数据库其实已经写入成功,应用层再次重试。
方案:下游按业务主键做 upsert 或唯一索引;用事务把“业务写入 + offset 提交”放在一起,或使用 Flink 的 checkpoint + 两阶段提交;建立去重表,用 event_id 唯一键记录已处理事件。
练习 5:消费积压排查
监控显示某个消费者组的 lag 持续上涨,写出你的排查顺序。
参考答案
- 看 lag 是全局上涨还是个别 partition 上涨。个别上涨优先怀疑 key 倾斜。
- 看消费者是否频繁 rebalance,检查日志里的
JoinGroup、SyncGroup和超时。 - 看单条消息处理耗时,是否超过
max.poll.interval.ms;看下游数据库或外部接口是否变慢,线程是否卡在 I/O。 - 看消费者数量和 partition 数是否匹配,是否有消费者空闲。
- 看生产端流量是否突增,确认是“生产变快”还是“消费变慢”。
短期扩容消费者或增加 partition;长期优化处理逻辑、批量写、异步 I/O。回答时不要只说“加机器”,要说明先定位再扩容。
练习 6:命令行操作与 offset
用命令行完成:创建 6 分区 topic order_events_demo;发送 3 条带 key 的消息;用组 g_demo 消费;查看该组 offset;把 offset 重置到最早。
参考答案
docker exec -it kafka /opt/kafka/bin/kafka-topics.sh --bootstrap-server localhost:9092 --create --topic order_events_demo --partitions 6 --replication-factor 1
docker exec -it kafka /opt/kafka/bin/kafka-console-producer.sh --bootstrap-server localhost:9092 --topic order_events_demo --property parse.key=true --property key.separator=:
# 依次输入 u1:{"event_type":"login"}、u2:{"event_type":"view"}、u1:{"event_type":"cart"}
docker exec -it kafka /opt/kafka/bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic order_events_demo --group g_demo --from-beginning
docker exec -it kafka /opt/kafka/bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --group g_demo --describe
docker exec -it kafka /opt/kafka/bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --group g_demo --topic order_events_demo --reset-offsets --to-earliest --execute
重置 offset 前必须先停止该组的消费者,否则会报“组内有活跃消费者”。
6. 面试延伸
面试题:Kafka 为什么快?
是什么:Kafka 是把消息追加到分区日志的分布式发布订阅系统,写入和读取都以顺序 I/O 为主。
解决什么问题:在高吞吐场景下,随机写磁盘、频繁数据复制和逐条网络传输会成为瓶颈。Kafka 用顺序写、页缓存、零拷贝、批量压缩和分区并行,把单机吞吐做高。
我的实操边界:我在本地 Docker 单机跑过生产消费,观察过同一 key 落在同一 partition;了解零拷贝是 sendfile 减少用户态复制,但没有在生产集群做过磁盘、网络和 JVM 参数调优。
面试题:如何保证消息不丢?
是什么:不丢要覆盖生产、存储、消费三段,任何一段断开都可能丢。
解决什么问题:生产端网络超时重试可能丢或重;broker 副本不足可能丢;消费端提前提交 offset 会丢。
我的实操边界:配置层面我掌握 acks=all、副本 3、min.insync.replicas=2、关闭自动提交、处理完再 commit;也理解幂等生产者只解决生产端重试重复。真实集群的磁盘故障演练我没有做过。
答法分三段:生产端用 acks=all、重试和幂等生产者;存储端副本数至少 3、min.insync.replicas=2、不允许非 ISR 副本选主;消费端手动提交、处理幂等、失败可重放。
面试题:rebalance 是什么,怎么减少影响?
是什么:消费组内 partition 所有权重新分配的过程。
解决什么问题:消费者增减或 topic 变化时,需要重新均衡负载,保证一个 partition 只被组内一个消费者处理。
我的实操边界:我理解触发条件和 eager/cooperative 两种模式,本地用命令行观察过消费者加入后分区变化;没有处理过生产环境大规模 rebalance 风暴。
减少影响的办法:合理设置心跳和 max.poll.interval.ms、控制单批处理时间、使用静态成员、消费者数量与 partition 匹配、避免频繁上下线。
面试题:怎么保证同一订单的消息有序?
是什么:Kafka 的顺序保证范围是单 partition。
解决什么问题:订单状态机要求 pay → ship → complete 按顺序处理,跨 partition 无法保证。
我的实操边界:我用 order_id 做 key 验证过同 key 同分区,了解幂等生产者与 max.in.flight 的关系;没有在生产环境处理过扩容导致 key 映射变化的迁移。
答案是:order_id 做 key,保证同订单进同一 partition;生产者开启幂等并控制未确认请求数;消费者对同一 partition 串行处理。如果业务要求全局顺序,代价是只能单 partition,吞吐会受限,通常不应该这样设计。
7. 本章验收
- 能画出 broker、topic、partition、offset、leader/follower 的关系图,并解释 offset 为什么只在 partition 内有意义。
- 能说出消费者组的分配规则,并能计算 3 个 partition、4 个消费者时的空闲消费者数量。
- 能写出一组“不丢”的配置,并解释
acks=all与min.insync.replicas的关系。 - 能解释为什么
order_id做 key 可以保证同一订单有序,以及扩容 partition 的风险。 - 能在本地启动单机 Kafka,完成创建 topic、生产、消费、查看消费组、重置 offset。
- 能用自己的话回答“Kafka 为什么快”,至少说出顺序写、页缓存、零拷贝、批处理中的三个。
- 能说清幂等生产者与端到端 exactly-once 的区别。