01Kafka 是什么?
一句话:Kafka 是一个分布式的、可持久化的、高吞吐的「提交日志」(Commit Log) 系统。它不删掉消费过的数据,而是像磁带一样不断往后追加 —— 这是理解后面所有机制的起点。
📡
发布 / 订阅模型
生产者把消息写到 Topic,消费者从 Topic 读取。一条消息可以被多个不同的组各消费一次,互不影响。
📼
追加式日志
消息只能追加到分区末尾,不可修改。数据按保留策略(时间/大小)过期删除,消费 ≠ 删除,这是它和 MQ 最大的区别。
🏢
分布式集群
数据分散在多个 Broker 上,每份数据有多个副本。Broker 宕机不丢数据,集群可水平扩容。
🎯 先建立全局视角:一条消息的一生
🏭业务系统
📦Producer
按 Key 路由
⚡Topic 分区
追加 + 副本
👥Consumer
按 offset 拉取
下面每一节,就是把这个流程中的一环拆开,让你亲手操作。
02Topic · Partition · Offset
Topic 是逻辑上的消息通道;物理上被切成若干 Partition(分区),每个分区是一个不可修改的追加日志;消息在分区内的位置编号就是 Offset(偏移量),从 0 开始递增。分区是 Kafka 并行度的最小单位。
点击「生产一条消息」,观察消息落在哪个分区、拿到什么 Offset。
🎯 试一试:勾选「携带 Key」后连发几条 —— 你会发现 user-42 永远进同一个分区。这就是 按 Key 哈希路由:保证同一个 Key 的消息有序(比如同一订单的状态流转)。注意:Kafka 只保证分区内有序,不保证 Topic 全局有序。
📐 三个概念一句话
- 🏷️ Topic — 逻辑分类,如「订单事件」「用户行为日志」
- ✂️ Partition — Topic 的物理分片,是并行与有序性的边界
- 📍 Offset — 消息在分区内的唯一身份证,消费进度就靠它记录
🤔 为什么每个分区只能有一个消费者?
因为分区是有序追加的日志。若两个消费者同时读同一分区,消费顺序、进度提交都会混乱。所以 Kafka 规定:一个分区在同一时刻只能被组内一个消费者消费 —— 反过来说,想提高消费速度就得加分区。这个约束在下一节会亲眼看到。
03Consumer Group 与 Rebalance
消费者组是 Kafka 实现负载均衡和广播的机制:组内分区分摊(每条消息只被组内一个消费者处理),组间互不影响(每个组都能拿到全量消息)。成员变化时自动重新分配分区,这就是 Rebalance。
♻️ REBALANCING — 检测到成员变化,正在重新分配分区…
Topic: order-events,共 3 个分区 × 10 条消息
🎯 试一试:开始消费后,点「加一个消费者」触发 Rebalance —— 分区被重新瓜分。加到 4 个消费者试试:多出来的那个只能闲着(分区数 3 < 消费者数 4)。这就是「消费者数超过分区数纯属浪费」的直观含义。
04副本 · Leader · ISR —— 高可用
每个分区有 1 个 Leader 副本和若干 Follower 副本。读写都走 Leader,Follower 异步拉取同步数据。与 Leader 保持同步的副本集合叫 ISR(In-Sync Replicas)。Leader 挂了,从 ISR 里选出新 Leader —— 数据不丢,服务不停。
acks =
--:--:--集群就绪:3 个 Broker,Topic payments 3 个分区,副本因子=2
🎯 试一试:① acks 选 all 发消息,观察消息先写 Leader、再同步 Follower、最后才返回 ack;② 点「宕掉一个 Broker」,观察持有 Leader 的分区如何秒级完成 Leader 选举;③ 再用不同 acks 发消息,对比可靠性差异。注意:acks=all + min.insync.replicas≥2 才能真正保证不丢。
acks=0速度优先
发出即忘,不等确认。吞吐最高,但 Broker 宕机时消息可能直接丢失。适合日志采集等允许丢失的场景。
acks=1折中
Leader 落盘就返回。若恰好在 Follower 同步前 Leader 宕机,新 Leader 上没有这条消息 → 丢失。
acks=all可靠优先
等 ISR 内所有副本写入才返回。配合 min.insync.replicas 和重试 + 幂等,可做到生产端不丢不重。金融/订单类场景标配。
05Kafka 为什么这么快?
单机百万级 TPS 不是玄学,是四个工程决策的叠加:顺序写磁盘、零拷贝、批量 + 压缩、分区并行。磁盘顺序写速度可达数百 MB/s,接近内存随机写 —— 这是一切的物理基础。
📼 ① 顺序追加写(Sequential I/O)
顺序写:磁头像流水线一样一路刷过去 ✅
随机写:磁头来回寻道,大部分时间在"赶路" ❌
Kafka 分区就是一段只追加的日志文件,写满一个再开下一个。配合页缓存(Page Cache):写操作先进操作系统页缓存,由 OS 异步刷盘,读写几乎不经过 JVM 堆 —— 还顺带规避了 GC 影响。
🚀 ② 零拷贝(sendfile) — 消费路径
传统方式:4 次拷贝 · 4 次内核/用户态切换
磁盘
→
内核缓冲
→
用户空间
→
Socket 缓冲
→
网卡
copy ×4 · context switch ×4 · 数据在内核态和用户态之间来回搬
零拷贝:数据全程不离开内核态
copy ×1 · 无用户态切换 · CPU 几乎不用参与搬运
消费者读的是「老数据」,大概率还躺在页缓存里。Kafka 用 sendfile() 让数据从页缓存直接进网卡,吞吐提升数倍,GC 压力为零。
📦 ③ 批量 + 压缩
Producer 不是一条条发,而是在内存攒一个 batch(按 batch.size/linger.ms 控制),整批压缩(gzip/snappy/lz4/zstd)后发送。Broker 原样存储压缩块,不解压 —— 网络、磁盘、CPU 三头省。
🧮 ④ 分区并行 + 稀疏索引
分区分布在不同磁盘/Broker 上,读写并行度 = 分区数。查找消息时,每个日志段(.log)配一个稀疏索引(.index),记录部分 offset → 物理位置,二分定位后小范围顺序扫,按 Offset 随机读取也是 O(log n)。
06投递语义:消息会丢吗?会重吗?
问题的本质是「处理消息」和「提交 offset」这两步的顺序。消费者崩溃的时机不同,结果就不同。选一种语义,注入一次崩溃,看结果。
语义:
💥 消费者崩溃!(业务处理与 offset 提交只完成了一部分)
📋 三种语义对比速查
| 语义 | 实现方式 | 消息丢失 | 重复消费 | 典型场景 |
| At-most-once | 先提交 offset 再处理 | 可能丢 | 不会 | 监控指标、日志(丢了无所谓) |
| At-least-once | 先处理再提交 offset(默认) | 不丢* | 可能重 | 绝大多数业务(下游做幂等) |
| Exactly-once | 事务 + 2PC:处理与提交原子完成 | 不丢 | 不重 | 金融转账、流式计算(Kafka Streams) |
* 前提是生产端 acks=all 且 min.insync.replicas≥2。工程上更常用的组合是:At-least-once + 消费端幂等(用业务主键去重),比强依赖 Exactly-once 事务更简单可控。
07一图带走:核心概念地图
分享结束前,把这 6 块拼图合起来。
📦 存储模型
- Topic → Partition → Offset
- 追加写,不可修改
- 消费 ≠ 删除,按保留策略过期
- 只保证分区内有序
👥 消费模型
- 组内分区分摊,组间广播
- 一个分区只归一个消费者
- Rebalance 自动再分配
- offset 由消费者提交管理
🛡️ 高可用
- Leader 读写,Follower 同步
- ISR = 跟得上的副本集合
- Leader 故障 → ISR 秒级选举
- acks=all 保证写入不丢
🚀 高性能
- 磁盘顺序写 + 页缓存
- 零拷贝 sendfile
- 批量发送 + 压缩
- 分区并行 + 稀疏索引
🎯 语义保证
- at-most / at-least / exactly
- 本质:处理与提交的顺序
- 工程首选:至少一次 + 幂等
🧩 扩展生态
- 经典 MQ 场景:削峰解耦
- 流处理:Kafka Streams
- 数据管道:Connect
- 生态:Schema Registry 等
给初学者的一句话:把 Kafka 理解成「一堆可以并行读写的、分片保存的、有多个备份的追加日志」,再回头看任何一个特性 —— 分区、副本、offset、Rebalance —— 都只是这句话的推论。