中间件核心机制解析 - 第3节

一、Kafka 整体架构:从一次“消息投递”说起

先问大家一个问题:如果你要设计一个能扛住百万级 TPS 的消息系统,你会怎么做?

我个人第一次接触 Kafka 时,就被它的架构设计震撼到了。说白了,Kafka 的核心思路就八个字:顺序写入、批量处理。它不像传统消息队列那样追求“实时推送”,而是把消息当成日志来存——你写日志的时候,会一条一条地追加到文件末尾,对吧?Kafka 就是这么干的。

Kafka 的完整架构包含四个核心角色:Producer(生产者)、Broker(服务节点)、Consumer(消费者),以及ZooKeeper(协调服务)。我画了一张图,帮你快速建立整体认知:

Producer 生产者 发送消息 Broker 集群 Broker 1 Broker 2 Broker 3 Topic → Partition 0,1,2... 拉取消息 Consumer 消费者 Consumer Group ZooKeeper 协调服务 管理元数据 图例: 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 的所有消息。这就像两个家庭各自看同一场电影,互不影响。

核心要点:Consumer Group 是实现“发布-订阅”和“点对点”两种模式的关键。组内是点对点,组间是发布-订阅。

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 上,这样就能并行读写。

Topic: orders Partition 0 Leader: Broker 1 Follower: Broker 2,3 消息 0 | 消息 1 | 消息 2 offset: 0, 1, 2 ... Partition 1 Leader: Broker 2 Follower: Broker 1,3 消息 0 | 消息 1 | 消息 2 offset: 0, 1, 2 ... Partition 2 Leader: Broker 3 Follower: Broker 1,2 消息 0 | 消息 1 | 消息 2 offset: 0, 1, 2 ...

每个 Partition 内部的消息是有序的,通过 offset(偏移量) 来标识位置。但不同 Partition 之间不保证顺序。所以,如果你要求消息严格有序,可以把分区数设为 1——但代价是吞吐量下降。

我曾经踩过的坑:有个业务要求“同一个用户的消息必须有序”。我一开始没注意,把分区数设成了 10,结果同一个用户的消息散落在不同分区,消费端处理顺序全乱了。后来用 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 文件中顺序扫描。因为索引条目间隔很小,扫描范围有限,所以速度很快。

性能数据:在普通机械硬盘上,Kafka 单 Partition 的写入吞吐量可以达到 100MB/s 以上。如果换成 SSD,还能再翻倍。

四、高可用与副本机制: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 是一致的,不会丢消息。

我的经验:ISR 机制是 Kafka 在“可用性”和“一致性”之间做的平衡。如果要求强一致性,可以把 min.insync.replicas 设大一些(比如 2),这样写入时至少要有 2 个副本确认。但代价是写入延迟会增加。

4.2 副本的同步流程

当一个 Producer 发送消息时:

  1. 消息先写入 Leader 副本
  2. Follower 从 Leader 拉取消息,写入本地日志
  3. Follower 向 Leader 发送确认
  4. 当 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。

可可精品资料库