4. Kafka深度解析:架构设计、副本机制、ISR与Leader选举、日志存储原理
各位同学,今天我们来啃一块硬骨头——Kafka。说实话,我在做消息中间件选型的时候,Kafka几乎是我绕不开的选项。它不像RabbitMQ那样轻巧,也不像RocketMQ那样功能全面,但它在高吞吐场景下的表现,确实让人服气。
这一章我会从四个核心维度来拆解Kafka:架构设计、副本机制、ISR与Leader选举、日志存储原理。嗯,每个点都是面试高频,也是生产环境踩坑的重灾区。
4.1 架构设计:Kafka到底长什么样?
先看一张我手绘的架构图,帮你建立整体认知。
这张图其实已经说清楚了Kafka的核心架构。Producer把消息推给Broker,Consumer从Broker拉取消息。中间那个ZooKeeper,负责做元数据管理和Controller选举。
我个人习惯把Kafka的架构总结成三句话:
- Producer:只管推,不管谁消费。它只关心消息能不能落到Leader分区。
- Broker:集群里的每个节点都是一个Broker。一个Topic被拆成多个Partition,分布在不同的Broker上。
- Consumer:按组消费,一个分区只能被同一个消费组里的一个消费者消费。
4.2 副本机制:数据安全的第一道防线
Kafka的副本机制,说白了就是给每个Partition做备份。Leader负责读写,Follower只负责同步。为什么这么设计?你想想看,如果所有副本都能写,那数据一致性就乱套了。
副本配置通过 replication.factor 控制。我建议生产环境至少设成3,别省这个资源。我曾经见过一个项目,为了省磁盘把副本数设成1,结果Broker宕机直接丢数据,那叫一个惨。
# 创建Topic时指定副本数
bin/kafka-topics.sh --create \
--bootstrap-server localhost:9092 \
--replication-factor 3 \
--partitions 6 \
--topic my-topic
副本的同步流程是这样的:
- Producer把消息写到Leader分区。
- Leader把消息追加到本地日志。
- Follower从Leader拉取消息,写入自己的日志。
- Follower向Leader发送ACK,确认同步完成。
这里有个关键点:Follower只拉取,不参与读写。Leader挂了,Follower才有机会上位。
4.3 ISR与Leader选举:谁说了算?
ISR的全称是In-Sync Replicas,翻译过来就是「同步中的副本集合」。说白了,ISR里装的是那些跟Leader保持同步的Follower。
为什么要有ISR?因为不是所有Follower都能跟上Leader的节奏。有些Follower网络延迟高,有些磁盘IO慢,它们可能落后Leader一大截。如果让这些「掉队」的Follower参与Leader选举,那数据一致性就崩了。
ISR的判断标准由两个参数控制:
| 参数 | 默认值 | 说明 |
|---|---|---|
replica.lag.time.max.ms |
30000 | Follower超过30秒没拉取消息,就被踢出ISR |
replica.lag.max.messages |
4000 | (旧版本)Follower落后超过4000条消息,就被踢出ISR |
嗯,这里要注意一下。新版本的Kafka已经废弃了 replica.lag.max.messages,只保留时间维度。为什么?因为消息条数这个指标太不稳定了。大消息和小消息的延迟完全不一样,用时间更合理。
Leader选举的逻辑是这样的:
- 如果Leader挂了,Controller从ISR里选一个Follower当新Leader。
- 如果ISR为空(所有副本都挂了),那就看
unclean.leader.election.enable配置。 - 这个配置默认是false,宁可不提供服务,也不丢数据。
- 如果设为true,就会从非ISR的副本里选一个当Leader,但会丢数据。
🔑 核心要点: ISR机制是Kafka保证数据一致性的关键。它确保只有「跟得上节奏」的副本才有资格成为Leader。我个人建议永远不要开启 unclean.leader.election.enable,数据一致性比可用性更重要。
4.4 日志存储原理:消息到底存在哪?
Kafka的日志存储,很多人觉得神秘。其实说白了,就是一堆文件。
每个Partition对应一个目录,目录名是 topic-partition 格式。比如 my-topic-0、my-topic-1。目录里放的是Segment文件,每个Segment由两个文件组成:
- .log 文件:存消息内容。
- .index 文件:存消息的偏移量索引。
Segment的命名规则是:以Segment里第一条消息的偏移量命名。比如 00000000000000000000.log 表示偏移量从0开始。
日志的写入流程是这样的:
// 伪代码示意
public void append(LogEntry entry) {
// 1. 获取当前活跃的Segment
Segment activeSegment = getActiveSegment();
// 2. 追加消息到.log文件
long offset = activeSegment.append(entry);
// 3. 更新.index文件
activeSegment.index.append(offset, position);
// 4. 如果Segment满了,滚动创建新Segment
if (activeSegment.isFull()) {
rollNewSegment();
}
}
Kafka的日志清理有两种策略:
| 策略 | 配置 | 说明 |
|---|---|---|
| 删除(delete) | log.cleanup.policy=delete |
超过保留时间或大小的消息会被删除 |
| 压缩(compact) | log.cleanup.policy=compact |
保留每个Key的最新一条消息,删除旧版本 |
我遇到过最典型的坑是日志清理不及时导致磁盘写满。有一次线上告警,Kafka Broker的磁盘使用率飙到95%。查了半天,发现是 log.retention.hours 设成了168小时(7天),但业务量太大,7天的数据量远超磁盘容量。
log.segment.bytes 默认是1GB,如果消息量不大,可以调小到512MB,这样清理更频繁,磁盘空间释放更快。
还有一个细节:Kafka的日志是顺序写的。为什么Kafka吞吐量高?就是因为顺序写磁盘比随机写快得多。机械硬盘的顺序写速度能到100MB/s以上,而随机写可能只有几MB/s。这个设计思路,说白了就是「用硬件特性来优化软件性能」。
好了,这一章的内容就到这里。Kafka的架构设计、副本机制、ISR与Leader选举、日志存储原理,每个点都值得深入理解。下次遇到Kafka性能问题,你至少知道该从哪里入手了。