面试知识库
进阶

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=368800
plaintext

两次二分(先定段、再定段内位置)+ 一小段顺序扫描,定位是 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,是两回事