存储架构
定义与作用
Kafka 的存储架构是其高性能的核心。Kafka 并非使用 B-Tree 或 LSM-Tree 这类通用存储引擎,而是采用了追加写日志(Append-Only Log)+ 分段(Segment)+ 稀疏索引的专用设计。
这种设计利用了磁盘顺序 I/O 的速度接近内存随机 I/O 这一关键特性,将数据以文件的形式高效持久化。
核心原理
磁盘上的物理结构
文件命名规则:以该 Segment 的起始 Offset 为名(20 位数字,左补零)。
Segment 内部数据格式
| 文件 | 内容 | 作用 |
|---|---|---|
.log | 消息数据(二进制序列) | 实际存储 |
.index | Offset → 文件位置映射(稀疏) | 快速定位 |
.timeindex | 时间戳 → Offset 映射 | 按时间查找 |
.snapshot | Leader Epoch 信息 | 防止数据回滚 |
稀疏索引与二分查找
稀疏索引的设计哲学:不索引每条消息(那样索引文件会非常大),而是每 log.index.interval.bytes 字节(默认 4096)建一条索引。查找时先二分定位到最近的索引条目,再顺序扫描。这种设计在顺序 I/O 场景下效率极高。
完整示例
示例一:直接浏览 Kafka 磁盘文件
# 1. 查看 log.dirs 中的文件
ls -la /tmp/kraft-logs-1/topic-orders-0/
# -rw-r--r-- 1 user user 1048576000 Jun 12 10:00 00000000000000000000.log
# -rw-r--r-- 1 user user 10485760 Jun 12 10:00 00000000000000000000.index
# -rw-r--r-- 1 user user 10485760 Jun 12 10:00 00000000000000000000.timeindex
# 2. 用 kafka-dump-log.sh 查看日志文件内容
bin/kafka-dump-log.sh --files /tmp/kraft-logs-1/topic-orders-0/00000000000000000000.log
# Dumping /tmp/kraft-logs-1/topic-orders-0/00000000000000000000.log
# Starting offset: 0
# offset: 0 position: 0 CreateTime: 1686... keySize: 3 valueSize: 5
# payload: key1-value1
# offset: 1 position: 62 CreateTime: 1686... keySize: 3 valueSize: 5
# payload: key2-value2
示例二:观察 Segment 滚动
# 1. 创建一个低 Segment 大小的 Topic
bin/kafka-topics.sh --create --topic demo-segment \
--partitions 1 --replication-factor 1 \
--config segment.bytes=1048576 \ # 1MB
--bootstrap-server localhost:9092
# 2. 发送大量数据触发 Segment 滚动
for i in $(seq 1 10000); do
echo "message-$i with lots of padding xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
| bin/kafka-console-producer.sh --topic demo-segment --bootstrap-server localhost:9092
done
# 3. 观察 Segment 文件
ls -la /tmp/kraft-logs-1/demo-segment-0/
# 00000000000000000000.log (1MB)
# 00000000000000001234.log (1MB)
# 00000000000000002567.log (正在写入)
# 4. 查看 Segment 列表
bin/kafka-log-dirs.sh --bootstrap-server localhost:9092 --describe \
--topic-list demo-segment
操作前后对比:
| 阶段 | Segment 文件数 | 说明 |
|---|---|---|
| 初始 | 1 | 00000000000000000000.log |
| 写入 ~1300 条后 | 3 | 自动切成两个完整 Segment + 一个活跃 Segment |
易错场景
易错 1:log.segment.bytes 设太大
场景:log.segment.bytes=10GB,保留策略按大小。
后果:过期删除最小粒度为 Segment。10GB 的 Segment 意味着每次清理至少释放 10GB,无法精确控制磁盘使用。也影响 kafka-log-dirs.sh 的日志清理效率。
建议:保持在 512MB-1GB,最大不超过 2GB。
易错 2:不同分区共用同一磁盘目录
场景:多个高流量分区的数据存在同一块机械硬盘上。
后果:磁头在多个 Segment 文件的 Appending 间跳动(虽然每个文件内部是顺序 I/O,但文件间是随机的)。
解决:将 log.dirs 设为多个物理磁盘,Kafka 自动在不同磁盘间分布分区。
面试高频考点
Q:Kafka 的存储引擎如何实现 O(1) 的磁盘读写?
A:通过三个设计:
- 追加写:不修改已有数据,所有写入都是
O(1)的磁盘扇区定位 - 稀疏索引:内存中保存每个 Segment 的稀疏索引映射,查找时二分定位到最近索引 + 小范围顺序扫描
- Page Cache 依赖:不维护自己的内存缓存,直接利用 OS 的 Page Cache。最近写入的数据大概率在缓存中,读命中率极高
代价:随机读取特定 Offset(如回溯消费)可能需要多次磁盘寻道,但这是日志型系统的典型取舍。