一、Kafka 是什么?—— 一座永不打烊的超级快递中转站
想象你家楼下有一座 巨大的快递中转站:
- 寄件人(Producer / 生产者):不停地把包裹(消息)送进来。
- 中转站(Broker):负责接收、存储和分发包裹。一座忙不过来?那就开多座,组成一个 集群(Cluster)。
- 收件人(Consumer / 消费者):主动来取走自己的包裹。
- 货架分类标签(Topic / 主题):包裹按类别分类存放,比如"电子产品""食品"等。
- 管理员(ZooKeeper):记录哪座中转站存了什么、谁是负责人,协调整个系统运作。
Kafka 就是这样一个 分布式消息系统,它每天能处理 千亿级别 的消息,像一座永不停歇的超级邮局。
1.1 Kafka 的架构全景
一句话概括:Producer(生产者) 发消息 → Broker(服务器集群) 存消息 → Consumer(消费者) 取消息,ZooKeeper 在旁边协调管理。
1.2 核心概念速览
| 概念 | 通俗解释 |
|---|---|
| Topic(主题) | 消息的分类标签,类似数据库的"表"。所有同类消息归属同一个 Topic。 |
| Partition(分区) | 一个 Topic 被拆成多个分区,每个分区像一条流水线,消息按先后顺序排列。目的:并行处理,提高吞吐量。 |
| Offset(偏移量) | 每条消息在分区中的唯一编号(从 0 递增),像排队号码。消费者靠它记住"读到哪了"。 |
| Broker(代理服务器) | Kafka 集群中的每一台服务器,负责存储和转发消息。 |
| Replica(副本) | 每个分区有多个副本,分布在不同 Broker 上,防止数据丢失。 |
| Leader / Follower | 一个分区的多个副本中,Leader 负责读写,Follower 只做同步备份。 |
| Consumer Group(消费组) | 多个消费者组成一组,组内分工(每人负责不同分区),组间独立(各自消费全部消息)。 |
| ZooKeeper | 集群的协调管理者,记录元数据,负责 Leader 选举等。 |
1.3 消息的组织方式
关键要点:
- 每个 Partition 内部消息 严格有序,每条消息都有唯一的 Offset
- 消息是 追加写入 的——像日记本只往后写,不修改前面的内容
- Kafka 不会消费后删除消息,而是根据 时间/空间策略 定期删除过期消息(默认保留 7 天)
- 一个 Partition 对应一个 Broker,一个 Broker 可管理多个 Partition
1.4 消费者与消费组
两种消费模式同时生效:
- 组内 = 队列模式:各消费者分工合作,每个 Partition 只被组内一个消费者处理 → 消息不会重复消费
- 组间 = 发布-订阅模式:不同消费组独立消费,互不影响 → 每条消息被每个组各消费一次
- ⚠️ 如果消费者数量 > 分区数量,多余的消费者会闲置
二、存储机制 —— 为什么 Kafka 这么快?
Kafka 每天处理千亿级消息,核心挑战是 “三高问题”:高并发、高可用、高性能。
2.1 为什么选择磁盘而不是内存?(反直觉的设计)
通常我们认为"内存比磁盘快",但实际测试表明:
磁盘的顺序 I/O 性能(53.2M values/s)甚至超过了内存的随机 I/O 性能(36.7M values/s)!
Kafka 正是利用了这一点 —— 它只做 顺序追加写入(像在笔记本上一行一行往下写),避免了磁盘最慢的随机读写。
| 对比维度 | 传统数据库(如 MySQL) | Kafka |
|---|---|---|
| 优化重点 | 提高读速度(用 B+ 树索引) | 提高写速度(顺序追加) |
| 写入方式 | 随机写入 + 维护索引 | 顺序追加写 |
| 查询方式 | 通过 B+ 树索引快速查找 | 稀疏索引 + 二分查找 |
| 适用场景 | 需要复杂查询的业务数据 | 海量日志流、消息队列 |
2.2 核心存储方案:顺序追加写 + 稀疏索引
Kafka 的存储方案可以概括为一句话:顺序追加写日志 + 稀疏哈希索引。
写入方面:顺序追加
- 新消息永远追加到文件末尾(日记只能往后写,不能插入或修改)
- 不维护复杂索引结构,写入速度极快,可达百万级 TPS
- 利用操作系统的 PageCache,数据先写入内存缓存,再异步刷盘
读取方面:稀疏索引 + 二分查找
不是给每条消息都建索引,而是 每隔一段距离 建一个索引指针——先通过索引快速定位到大致区域,再在小范围内顺序查找:
就像在一本厚书里找内容:先看目录(Segment 文件名)确定在哪一章 → 再看小标题(索引)缩小范围 → 最后在几页之内找到目标。
2.3 五层存储架构:主题 → 分区 → 副本 → 分段 → 索引
| 层级 | 说明 | 通俗比喻 |
|---|---|---|
| Topic | 消息的逻辑分类 | 图书馆的"分类" |
| Partition | 分区,水平扩展 | 分类下的"书架" |
| Replica | 每个分区的多个副本(Leader + Follower) | 书的"正本 + 副本",防丢失 |
| LogSegment | 将日志切分为多个小文件 | 一个大笔记本分成多个小本子 |
| Index | 索引文件,加速查找 | 每个小本子前面的"目录页" |
为什么要分区(Partition)?
- 水平扩展:消息分散到多台 Broker,避免单机瓶颈
- 并行处理:多个消费者可同时消费不同分区
- 分区内有序:每个分区内消息严格按写入顺序排列,但不保证跨分区的全局顺序
为什么要分段(Segment)?
- 一个分区的消息如果都在一个文件里,文件会越来越大,查找和清理都很困难
- 分段后每个文件大小可控(默认 1GB),历史数据清理时直接删除旧 Segment 即可
文件命名规则与目录布局
文件名是 20 位数字,代表该 Segment 第一条消息的 Offset(不足补零):
/kafka-logs/
├── topic-order-0/ ← Partition 0 的文件夹
│ ├── 00000000000000000000.log ← 数据文件(第1个 Segment,起始 Offset=0)
│ ├── 00000000000000000000.index ← 偏移量索引
│ ├── 00000000000000000000.timeindex ← 时间戳索引
│ ├── 00000000000000009014.log ← 第2个 Segment(起始 Offset=9014)
│ ├── 00000000000000009014.index
│ └── 00000000000000009014.timeindex
├── topic-order-1/ ← Partition 1
├── topic-order-2/ ← Partition 2
└── topic-order-3/ ← Partition 3
- 只有最后一个 Segment(activeSegment)可以写入,之前的全部"只读"
- 达到大小或时间条件时,activeSegment 被"封存",创建新的继续接收消息
消息查找的完整过程
假设消费者想读取 Offset = 5000 的消息:
步骤 1:根据 Segment 文件名(基准 Offset),二分法定位 Offset=5000 所在的 Segment
步骤 2:在 .index 文件中,找到 ≤ 5000 的最大索引条目(稀疏索引)
步骤 3:从索引指向的位置开始,在 .log 文件中顺序扫描,找到 Offset=5000 的消息
2.4 高性能的三大法宝
① 顺序追加写入
所有写操作都在文件末尾追加,避免磁盘随机寻道,写入速度接近磁盘理论带宽上限。
② PageCache(页缓存)
Kafka 大量利用操作系统的 PageCache —— 相当于免费获得了一层内存缓存:
- 写入时:数据先写入 PageCache(内存),OS 在合适时机批量刷盘 → 写入极快
- 读取时:数据如果还在 PageCache 中,直接从内存读取 → 不需要访问磁盘
- Kafka 不需要自己管理缓存,完全交给操作系统,实现简单且高效
③ 零拷贝(Zero-Copy)
传统数据传输需经历 4 次拷贝:磁盘 → 内核缓冲 → 用户缓冲 → Socket 缓冲 → 网卡。
零拷贝技术让数据 直接从磁盘文件传输到网卡,减少中间拷贝步骤,大幅降低 CPU 开销和内存带宽占用。
2.5 消息删除策略
Kafka 不会在消息被消费后立即删除。Kafka 没有"消费完"的概念,只有"过期"的概念。
| 维度 | 参数 | 说明 |
|---|---|---|
| 时间维度 | log.retention.hours/minutes/ms | 默认保存 7 天,超时自动删除 |
| 空间维度 | log.retention.bytes | 默认 -1(不限制),超过配置大小的旧消息被删除 |
三、生产者(Producer)—— 消息是怎么发出去的?
3.1 基本工作流程
1. 配置参数并创建 Producer 实例
2. 构建待发送的消息(指定 Topic、Key、Value)
3. 调用发送接口(异步发送)
4. 等待回调确认 / 处理发送结果
5. 使用完毕后关闭 Producer
关键点:
- 生产者自己决定消息发往哪个分区(可通过 Key 的 Hash 值选择)
- 同一个生产者可以向多个 Topic 发送消息
- 发送接口是 异步 的,不必等每条消息写入成功才发下一条
3.2 最重要的参数
① acks —— 消息确认策略(最关键!)
控制"发送消息后需要多少个副本确认成功",直接影响 可靠性 vs 性能 的平衡:
| acks 值 | 含义 | 可靠性 | 吞吐量 | 适用场景 |
|---|---|---|---|---|
| 0 | 发了就不管,不等任何确认 | 🔴 最低 | 🟢 最高 | 允许丢失(如日志统计) |
| 1 | Leader 写入成功就算成功 | 🟡 适中 | 🟡 适中 | 一般业务场景 |
| -1 / all | ISR 中所有副本都写入才算成功 | 🟢 最高 | 🔴 最低 | 不允许丢失(如支付订单、金融交易) |
💡 通俗理解:
acks=0= 寄信不要回执;acks=1= 快递送到站点签收;acks=all= 快递送到收件人手上并拍照确认。
② batch.size 和 linger.ms —— 批量发送控制
batch.size(默认 16KB):Producer 把发往同一分区的消息攒成一批(batch),满了再发linger.ms(默认 0ms):即使 batch 没满,最多等这么久就发出去
类比等公交:
batch.size= “车坐满就发车”,linger.ms= “最多等多久就发车”。增大这两个值可提高吞吐量,但会增加延迟。
③ retries 和 retry.backoff.ms —— 重试机制
retries(默认 0):发送失败后重试次数,建议设为 > 0retry.backoff.ms(默认 100ms):两次重试间隔
⚠️ 重试的副作用:
- 可能导致 消息重复(消息实际已写入但响应丢失,导致重试)
- 可能导致 消息乱序(第一批失败重试时,第二批已成功)
如需保证顺序,建议设置
max.in.flight.requests.per.connection = 1
④ compression.type —— 压缩方式
| 值 | 说明 |
|---|---|
none | 不压缩(默认) |
gzip | 压缩率高,但 CPU 开销大 |
snappy | 压缩率和速度的平衡 |
lz4 | 速度最快,推荐使用(性能:LZ4 > Snappy > GZIP) |
⑤ 其他关键参数
| 参数 | 默认值 | 说明 |
|---|---|---|
bootstrap.servers | 无 | 必填,Broker 地址列表 |
max.request.size | 1MB | 单条消息最大大小。需与 Broker 端 message.max.bytes 配合 |
buffer.memory | 32MB | 发送缓冲区大小。Producer 异步发送,消息先入缓冲区再由 I/O 线程发出。缓冲区满时 Producer 会阻塞 |
四、消费者(Consumer)—— 消息是怎么收到的?
4.1 基本工作流程
1. 配置参数并创建 Consumer 实例
2. 订阅一个或多个 Topic
3. 循环调用 poll() 拉取消息
4. 处理消息并提交 Offset(位移)
5. 使用完毕后关闭 Consumer
关键点:
- 消费者采用 Pull(拉取)模式,主动去 Broker 拉消息
- 消费者需要维护自己的消费进度(Offset),告诉 Kafka “我已经读到第几条了”
4.2 Offset 提交 —— 记住"我读到哪了"
消费者重启后能从上次位置继续消费,这靠 提交位移(Commit Offset)。
自动提交
enable.auto.commit = true # 开启自动提交
auto.commit.interval.ms = 5000 # 每 5 秒自动提交一次
- ✅ 简单省事
- ❌ 可能重复消费——提交后发生 Rebalance,中间消费的消息会被重新消费
手动提交
enable.auto.commit = false # 关闭自动提交
# 代码中在处理完消息后调用 commitSync() 或 commitAsync()
- 同步提交(commitSync):阻塞等待,直到成功或失败,可自动重试
- 异步提交(commitAsync):立即返回,不阻塞,但不自动重试(因为重试时 Offset 可能已过时)
🏆 最佳实践:日常使用
commitAsync()避免阻塞,消费者 关闭前 使用commitSync()确保最后的 Offset 正确保存。
4.3 消费者的关键参数
| 参数 | 默认值 | 说明 |
|---|---|---|
bootstrap.servers | 无 | 必填,Broker 地址列表 |
group.id | 无 | 必填,消费组 ID |
auto.offset.reset | latest | 无历史 Offset 时从哪开始消费 |
enable.auto.commit | true | 是否自动提交位移 |
fetch.min.bytes | 1B | 每次拉取的最小数据量,不够就等 |
fetch.max.bytes | 50MB | 每次拉取的最大数据量 |
fetch.max.wait.ms | 500ms | 数据不够 fetch.min.bytes 时最多等多久 |
max.poll.records | 500 | 每次 poll() 最多返回多少条消息 |
max.partition.fetch.bytes | 1MB | 每个分区每次最多返回多少数据 |
session.timeout.ms | 10s | 消费者存活检测超时 |
heartbeat.interval.ms | 3s | 心跳发送间隔 |
max.poll.interval.ms | 5min | 两次 poll 的最大间隔 |
auto.offset.reset 详解
| 值 | 行为 |
|---|---|
earliest | 从最早的消息开始消费(不一定是 Offset 0,旧消息可能已被删除) |
latest | 从最新的消息开始消费(忽略历史消息) |
none | 找不到历史 Offset 就直接报错 |
⚠️ 一旦消费组成功提交过 Offset,再次启动时 Kafka 会使用保存的 Offset,忽略
auto.offset.reset的设置。
五、Rebalance 机制 —— 消费者的"重新站位"
5.1 什么是 Rebalance?
当消费组发生变化时,Kafka 需要重新分配"谁消费哪个分区",这个过程叫 Rebalance(重新平衡)。
比喻:一个班级有 20 名学生(Consumer),打扫 100 间教室(Partition),正常每人 5 间。有学生请假或来了新同学,就需要重新分配。
5.2 触发条件
- 消费组成员变化 —— 新 Consumer 加入或有 Consumer 退出/崩溃
- 消费者处理超时 —— 在规定时间内未完成消息处理
- 订阅的 Topic 变化 —— Topic 数量改变
- 分区数变化 —— Topic 的 Partition 数增加
5.3 Rebalance 的代价(类似 JVM 的 “Stop The World”)
- 😰 消费暂停:Rebalance 期间,所有消费者停止消费
- 😰 全量重分配:所有分区重新分配(不是只调整变化部分)
- 😰 极度缓慢:大规模消费组(几百个 Consumer)可能需要数小时
5.4 如何避免不必要的 Rebalance?
最常见的问题是 消费者被误判为"已死亡",有两大原因:
推荐参数配置:
| 参数 | 推荐值 | 说明 |
|---|---|---|
session.timeout.ms | 6000(6秒) | Broker 判定 Consumer 死亡的超时时间 |
heartbeat.interval.ms | 2000(2秒) | Consumer 发送心跳的间隔 |
max.poll.interval.ms | 根据业务调整 | Consumer 两次 poll 的最大间隔 |
max.poll.records | 根据处理能力调整 | 每次 poll 最大消息数 |
黄金公式:
session.timeout.ms ≥ 3 × heartbeat.interval.ms
最佳策略:较小的 session.timeout.ms(快速检测崩溃)+ 较大的 max.poll.interval.ms(留足处理时间),既能发现真正崩溃的消费者,又不会误判正常的长耗时处理。
六、可靠性保证 —— 消息到底会不会丢?
这是使用消息系统时最关心的问题。Kafka 从 Producer、Broker、Consumer 三个维度保障可靠性。
6.1 ISR 机制 —— 可靠性的基石
ISR(In-Sync Replica) = 与 Leader 保持同步的副本集合
- 只有 ISR 中的副本才有资格被选为新 Leader
- 只有 ISR 中所有副本都收到了某条消息,该消息才算"已提交"
- Kafka 只保证"已提交"的消息不丢失,未成功提交的消息不做保障
- 落后太多的 Follower 会被"踢出"ISR,追上后重新加入
6.2 LEO 和 HW —— 消费者能看到什么?
| 概念 | 全称 | 含义 |
|---|---|---|
| LEO | Log End Offset | 每个副本已写入的最后一条消息的下一个位置 |
| HW | High Watermark(高水位) | ISR 中所有副本 LEO 的最小值,消费者最多消费到这里 |
📌 重要结论:消息发送成功 ≠ 消费者立刻能读到。只有当所有 ISR 副本同步完成,HW 才会推进,消费者才能看到新消息。
6.3 CAP 取舍:在一致性和可用性之间选择
Kafka 可以通过参数组合配置为不同类型的系统:
CP 系统(强一致性,宁可停服也不丢数据)
# Producer 端
acks = -1 # 所有 ISR 副本确认才算成功
# Broker 端
min.insync.replicas = N/2 + 1 # 至少超过半数副本在线
unclean.leader.election.enable = false # 禁止不在 ISR 中的副本当选 Leader
- 效果:要么消息成功写入所有副本,要么报错。不会出现消息丢失
- 代价:吞吐量下降,副本数不足时集群会停止服务
AP 系统(高可用,允许极小概率丢消息)
# Producer 端
acks = 1 # Leader 确认即可
# Broker 端
min.insync.replicas = 1
unclean.leader.election.enable = false
- 效果:Leader 写入成功就返回,性能好
- 代价:如果 Leader 在 Follower 同步前挂了,未同步的消息会丢失
6.4 消费者的三种投递语义
| 语义 | 英文 | 含义 | 实现方式 | 风险 |
|---|---|---|---|---|
| 最多一次 | At Most Once | 消息最多被消费一次(可能丢) | 自动提交 Offset | 提交后消费者挂了,消息丢失 |
| 至少一次 | At Least Once | 消息至少被消费一次(可能重复) | 手动提交 Offset | 处理完但提交前挂了,消息被重复消费 |
| 恰好一次 | Exactly Once | 消息恰好被消费一次 | 需要原子操作 | 实现极难,需消息存储和 Offset 提交是原子的 |
💡 实际建议:大多数业务使用 “至少一次”+ 业务侧幂等处理(同一消息处理多次结果相同),是最佳实践。
七、Broker 核心配置
| 参数 | 默认值 | 说明 |
|---|---|---|
broker.id | -1 | Broker 唯一标识,集群内不可重复。推荐从 0 开始递增 |
log.dirs | /tmp/kafka-logs | 消息存储目录。生产环境必须自定义! 可设多目录分散负载 |
zookeeper.connect | 无 | ZooKeeper 连接地址。建议配根目录隔离,如 zk1:2181/kafka_cluster |
listeners | — | Broker 监听地址,格式:协议://主机名:端口 |
num.network.threads | 3 | 网络请求处理线程数 |
num.io.threads | 8 | I/O 请求处理线程数 |
log.retention.hours | 168(7天) | 消息保留时间 |
log.retention.bytes | -1 | 日志最大大小,默认不限制 |
log.segment.bytes | 1GB | 单个 Segment 文件大小 |
min.insync.replicas | 1 | 最少同步副本数(配合 acks=-1) |
message.max.bytes | ~977KB | Broker 接受的最大消息大小 |
unclean.leader.election.enable | false | 是否允许非 ISR 副本选举为 Leader |
delete.topic.enable | true | 是否允许删除 Topic |
多目录提升吞吐量
log.dirs=/data/kafka1,/data/kafka2,/data/kafka3
N 块磁盘的 N 个磁头可同时并行写入,极大提升吞吐量。
注意:Kafka 是按分区数均匀分配目录,而不是按磁盘空间分配。
八、C++ 客户端开发要点(librdkafka)
8.1 生产者开发要点
创建 Kafka Conf → 设置参数 → 创建 Producer → 创建 Topic 对象 → 调用 produce() 发送
- 可设置自定义分区策略(如 Hash 分区)
- 同一个 Producer 实例可向多个 Topic 发送消息
- 发送接口是异步的,通过回调获取发送结果
- 重要回调:
DeliveryReportCb(投递报告)和EventCb(事件回调)
8.2 消费者开发要点
创建 Kafka Conf → 设置参数 → 创建 Consumer → 订阅 Topic → 循环 consume() 拉取消息
- 必须设置
bootstrap.servers、group.id、auto.offset.reset三个参数 - 可同时订阅多个 Topic
- 消费后需正确提交 Offset(可选同步/异步)
- 可设置
RebalanceCb回调感知 Rebalance 事件
8.3 常见错误
| 错误信息 | 原因 | 解决方案 |
|---|---|---|
Local: Queue full | 本地发送缓冲区满了 | 增大 buffer.memory,或降低发送速率 |
Message larger than max message size | 消息超过 Broker 允许的最大值 | 调大 message.max.bytes 和 max.request.size |
九、全景总结
🗺️ Kafka 知识全景图
📋 十大核心知识点
| # | 核心要点 | 一句话总结 |
|---|---|---|
| 1 | 架构设计 | Producer 推消息 → Broker 存储 → Consumer 拉消息,ZooKeeper 协调 |
| 2 | 磁盘哲学 | 磁盘顺序 I/O 可以超过内存随机 I/O,这是 Kafka 选择磁盘存储的理论基础 |
| 3 | 存储结构 | Topic → Partition → Segment(.log + .index + .timeindex),层层拆分 |
| 4 | 高性能秘诀 | 顺序写 + PageCache + 零拷贝,三大法宝实现百万级 TPS |
| 5 | 消息不删 | Kafka 不在消费后删除消息,而是根据时间/空间策略定期过期删除 |
| 6 | ACK 策略 | 0/1/all 三级控制可靠性与性能的权衡 |
| 7 | ISR 机制 | 只有 ISR 中的副本才有资格当 Leader,是数据安全的核心 |
| 8 | HW 控制 | 消费者只能读到 HW 之前的消息,写入成功 ≠ 消费者能立刻读到 |
| 9 | Rebalance | 代价很大(全员暂停),应通过合理参数配置避免不必要的触发 |
| 10 | CP vs AP | 通过 acks + min.insync.replicas + unclean.leader.election.enable 灵活切换 |
🎯 记住:Kafka 的设计哲学是 在分布式系统的各种约束中寻找最优平衡。它没有追求完美的一致性或极致的性能,而是通过灵活的参数配置,让用户根据自己的业务场景,在 可靠性、性能和可用性 之间做出最适合的选择。根据你的业务需求(能否容忍消息丢失?能否接受重复消费?),选择合适的参数配置,才是正确的使用方式。
3690

被折叠的 条评论
为什么被折叠?



