中间件核心机制解析 - 第3节
一、Kafka 整体架构:从一次“消息投递”说起
先问大家一个问题:如果你要设计一个能扛住百万级 TPS 的消息系统,你会怎么做?
我个人第一次接触 Kafka 时,就被它的架构设计震撼到了。说白了,Kafka 的核心思路就八个字:顺序写入、批量处理。它不像传统消息队列那样追求“实时推送”,而是把消息当成日志来存——你写日志的时候,会一条一条地追加到文件末尾,对吧?Kafka 就是这么干的。
Kafka 的完整架构包含四个核心角色:Producer(生产者)、Broker(服务节点)、Consumer(消费者),以及ZooKeeper(协调服务)。我画了一张图,帮你快速建立整体认知:
1.1 Producer:消息的“投递员”
Producer 负责把消息发到 Broker。这里有个关键点:Producer 不直接跟 Consumer 通信。消息先到 Broker 落盘,Consumer 再来拉取。这种“解耦”设计,让生产者和消费者可以各自独立扩缩容。
我在项目中遇到过一个问题:某个业务方把 Producer 的 acks 参数设成了 0,结果消息丢了都不知道。嗯,这里要提醒大家——acks 参数决定了消息的可靠性:
| acks 值 | 含义 | 可靠性 | 性能 |
|---|---|---|---|
| 0 | Producer 发完就认为成功,不等待确认 | 最低 | 最高 |
| 1 | Leader 副本写入成功即返回 | 中等 | 中等 |
| all(或 -1) | 所有 ISR 副本都写入成功才返回 | 最高 | 最低 |
acks=all,日志类业务用 acks=1。至于 acks=0,除非你能接受丢消息,否则别碰。
1.2 Broker:消息的“存储仓库”
Broker 是 Kafka 的服务节点。一个 Kafka 集群通常有多个 Broker。每个 Broker 上会存储多个 Partition 的副本。
你想想看,如果只有一个 Broker,那它挂了整个系统就瘫了。所以生产环境至少 3 个 Broker 起步。我见过有些团队为了省钱只搭 2 个 Broker,结果一次机房断电,数据全丢——教训啊。
1.3 Consumer 与 Consumer Group:消息的“取件人”
Consumer 从 Broker 拉取消息。但 Kafka 引入了一个非常巧妙的概念——Consumer Group(消费组)。
同一个消费组内的消费者,共同消费一个 Topic 的消息。每条消息只会被组内的一个消费者处理。这就像一群人分吃一个蛋糕,每人分一块,但不会有人吃到同一块。
不同消费组之间是独立的。每个消费组都能完整地消费 Topic 的所有消息。这就像两个家庭各自看同一场电影,互不影响。
1.4 ZooKeeper:集群的“大管家”
ZooKeeper 在 Kafka 中负责三件事:
- Broker 注册与发现:新 Broker 加入时,在 ZK 上注册节点;挂掉时,ZK 通知其他组件。
- Topic 元数据管理:记录每个 Topic 有哪些 Partition,每个 Partition 的 Leader 是谁。
- Consumer 偏移量管理(旧版本):记录每个 Consumer Group 消费到了哪个位置。
不过,从 Kafka 2.8 开始,Kafka 逐步“去 ZooKeeper 化”,把元数据管理搬到了 Broker 内部。但直到今天,很多生产环境还在用 ZK 版本。我个人建议:如果你是新项目,可以考虑 KRaft 模式;如果是老项目,别急着升级,稳定第一。
二、Topic 与 Partition:Kafka 的“分而治之”之道
Topic 是消息的逻辑分类。比如订单消息发到 order_topic,日志消息发到 log_topic。但一个 Topic 如果只存在一个 Broker 上,那它的吞吐量就受限于单台机器的磁盘和网络。
Kafka 的解决方案是:把一个 Topic 拆成多个 Partition。每个 Partition 是一个有序的、不可变的消息序列。Partition 分布在不同的 Broker 上,这样就能并行读写。
每个 Partition 内部的消息是有序的,通过 offset(偏移量) 来标识位置。但不同 Partition 之间不保证顺序。所以,如果你要求消息严格有序,可以把分区数设为 1——但代价是吞吐量下降。
key 哈希到同一个分区才解决。记住:相同 key 的消息一定进入同一个 Partition。
三、消息存储与索引:Kafka 为什么这么快?
Kafka 的存储设计是它高性能的核心。说白了,就是利用了磁盘顺序读写的特性。机械硬盘顺序写能达到 200MB/s,而随机写只有几 MB/s。Kafka 把随机写变成了顺序写,性能自然就上去了。
3.1 存储结构
每个 Partition 在磁盘上对应一个目录。目录下包含多个 Segment(段文件)。每个 Segment 由两个文件组成:
- .log 文件:存储消息数据
- .index 文件:存储偏移量到物理位置的索引
Segment 的命名规则是:起始偏移量.log。比如 00000000000000000000.log 表示从 offset 0 开始。
3.2 索引机制
Kafka 的索引是稀疏索引。什么意思呢?就是不会为每条消息都建索引,而是每隔一定字节数(默认 4KB)建一个索引条目。这样索引文件很小,可以常驻内存。
查找消息时,先二分查找索引文件,找到目标 offset 所在的索引条目,然后到对应的 .log 文件中顺序扫描。因为索引条目间隔很小,扫描范围有限,所以速度很快。
四、高可用与副本机制:ISR 的智慧
单机总有挂掉的风险。Kafka 通过 副本机制 来保证高可用。每个 Partition 可以有多个副本,分布在不同的 Broker 上。
副本分为两种角色:
- Leader:负责处理读写请求。每个 Partition 只有一个 Leader。
- Follower:只负责从 Leader 同步数据。不对外提供服务。
如果 Leader 挂了,Follower 中会选出一个新的 Leader。这个选举过程依赖 ISR(In-Sync Replicas,同步副本集合)。
4.1 ISR 是什么?
ISR 是“跟 Leader 保持同步”的副本集合。Follower 会定期从 Leader 拉取数据。如果某个 Follower 落后太多(超过 replica.lag.time.max.ms 配置的时间,默认 10 秒),就会被踢出 ISR。
只有 ISR 中的副本才有资格成为新的 Leader。为什么?因为只有它们的数据跟 Leader 是一致的,不会丢消息。
min.insync.replicas 设大一些(比如 2),这样写入时至少要有 2 个副本确认。但代价是写入延迟会增加。
4.2 副本的同步流程
当一个 Producer 发送消息时:
- 消息先写入 Leader 副本
- Follower 从 Leader 拉取消息,写入本地日志
- Follower 向 Leader 发送确认
- 当 ISR 中所有副本都确认后,Leader 向 Producer 返回成功
如果 Follower 挂了,Leader 会等待 replica.lag.time.max.ms 时间,然后将其踢出 ISR。等 Follower 恢复后,它会先追上进度,再重新加入 ISR。
replica.lag.time.max.ms 设成了 30 秒,结果某个 Follower 因为网络抖动,反复进出 ISR。每次进出都会触发 Leader 选举,导致整个集群抖动。后来我改成 60 秒,问题就解决了。所以,这个值别设太短,给网络波动留点余量。
4.3 副本数与可靠性
副本数(replication.factor)一般设为 3。为什么是 3?
- 副本数为 1:没有容错能力,Broker 挂了数据就丢了
- 副本数为 2:可以容忍 1 个 Broker 挂掉,但挂掉 2 个就完了
- 副本数为 3:可以容忍 2 个 Broker 挂掉,安全性更高
当然,副本数越多,写入延迟越大,磁盘占用也越大。3 是一个比较平衡的选择。
好了,这一章的内容就到这里。Kafka 的架构设计其实处处体现着“权衡”的智慧——性能与可靠性、一致性与可用性、存储与索引。理解了这些权衡,你才能真正用好 Kafka。
可可精品资料库