Kafka存储与日志结构#
一句话答案#
每个 Partition 是一个目录,物理上切成多个 Segment 段(
.log数据 +.index偏移索引 +.timeindex时间索引);通过稀疏索引 + 二分查找快速根据 offset 定位消息,配合顺序追加写实现高吞吐。
核心要点
目录与文件结构#
一个分区 = 一个目录:topic-0/
00000000000000000000.log ← 数据文件(消息本体,顺序追加)
00000000000000000000.index ← 偏移量索引(offset → 物理位置)
00000000000000000000.timeindex ← 时间戳索引(timestamp → offset)
00000000000000368769.log ← 下一个 segment,文件名=该段首条消息的 baseOffset
00000000000000368769.index
00000000000000368769.timeindex
leader-epoch-checkpoint ← leader epoch 记录plaintext- 文件名 = baseOffset:段文件名是该段第一条消息的 offset,左补 0 凑 20 位。
- 滚动切分:当前段写满
log.segment.bytes(默认 1G)或到达log.roll.ms(默认 7 天)就新建一个 segment。 - 只有最后一个段是 active segment(可写),其余只读。
稀疏索引 + 二分查找(高频追问)#
.index 不是每条消息都建索引,而是每写入约 log.index.interval.bytes
(默认 4KB)才记一条 → 稀疏索引,省空间、能整段加载进内存。
索引项:<相对offset, 物理position> (相对 baseOffset,省存储)
查 offset=368800 的消息:
1. 二分段文件名 → 落在 00...368769.log(baseOffset≤目标的最大段)
2. 相对 offset = 368800 - 368769 = 31
3. .index 中二分找到 ≤31 的最大索引项,拿到 .log 中的起始 position
4. 从该 position 顺序扫描 .log,直到精确匹配 offset=368800plaintext两次二分(先定段、再定段内位置)+ 一小段顺序扫描,定位是 O(log n)。
mmap 加速索引#
- 索引文件用 mmap(内存映射) 映射到内存,读写索引走页缓存,避免频繁系统调用。
- 数据文件
.log的读取走 零拷贝 sendfile(见 Kafka高性能原理),二者机制不同别混淆。
消息为什么不丢/读得到:HW 与刷盘#
- 消息先写入 PageCache 再由 OS 异步刷盘,Kafka 默认不每条 fsync(靠多副本而非刷盘保可靠),见 消息丢失与可靠性保证。
- 消费者只能读到 HW(高水位) 之前的消息,HW 之后是未完全同步的”脏数据”,见 Kafka-ISR机制。
与清理策略的关系#
段文件是清理的最小单位——无论是按时间/大小删除整段,还是 Log Compaction 按 key 保留最新值,都以 segment 为操作粒度,详见 Kafka日志清理与Compaction。
面试回答(2分钟版)
Kafka 的每个 Partition 在磁盘上是一个独立目录,目录里的数据并不是一个大文件,而是被切分成多个 Segment 段,每个段由三类文件组成:
.log存消息本体、.index是偏移量索引、.timeindex是时间戳索引。段文件名就是这个段第一条消息的 offset 也就是 baseOffset,左补零到 20 位。当前段写满 segment.bytes 默认 1G 或到达滚动时间就新建下一个段,只有最后一个 active segment 可写,其余只读,这样写入就是纯顺序追加。定位一条消息靠稀疏索引加二分查找:index 不是每条都建索引,而是每隔大约 4KB 记一条 offset 到物理位置的映射,所以索引很小能整段放进内存。查某个 offset 时,先用段文件名二分定位到哪个段,再在该段的 index 里二分找到不超过目标的最大索引项拿到起始物理位置,最后从这个位置顺序扫一小段精确命中,整体是 O(log n)。索引文件用 mmap 映射进内存加速,而数据文件的网络发送走零拷贝 sendfile,两个优化作用在不同文件上。这套”分段 + 稀疏索引 + 顺序写”是 Kafka 高吞吐和快速检索能并存的基础。
追问与易错
追问方向:
- “为什么用稀疏索引而不是稠密索引?”→ 稠密索引每条消息一项,索引文件会和数据一样大、放不进内存;稀疏索引用极小的空间换一次段内顺序小扫描,是空间与查找速度的折中
- “segment 滚动的触发条件有哪些?”→ 当前段大小达到
log.segment.bytes(默认 1G)、或段存活时间超过log.roll.ms/hours(默认 7 天)、或索引文件写满log.index.size.max.bytes - “
.timeindex有什么用?”→ 支持按时间戳查 offset,用于offsetsForTimes、按时间回溯消费、以及按时间的日志保留策略 - “段文件名为什么用 baseOffset 命名?”→ 这样定位 offset 时可以直接对文件名做二分快速找到目标段,无需额外的全局索引
易错点:
- ❌ “一个分区就是一个文件”——分区是一个目录,由多个 segment 组成
- ❌ “index 给每条消息建索引”——是稀疏索引,每隔
log.index.interval.bytes才建一条 - ❌ “靠 mmap 实现零拷贝发消息”——索引用 mmap,消息发送用 sendfile,是两回事