3. Kafka消息模型与存储机制

Kafka 的高吞吐、可持久化和可回溯能力,本质上来自它独特的数据组织方式。理解消息格式、偏移量、日志段、索引、压缩与清理策略,是面试中区分“会用 Kafka”和“理解 Kafka”的关键分水岭。本节从一条消息写入磁盘开始,逐层拆解 Kafka 的存储模型。

一、消息格式:从嵌套结构到 Record Batch

Kafka 中生产者发送的每条消息并不是单独落盘的,而是以“批次”为单位组织。在较新的版本(0.11 之后)中,核心存储单元是 Record Batch,它由批次头和若干条 Record 组成。批次头包含 baseOffset、批次长度、magic 版本、CRC 校验、属性位、时间戳、生产者 ID、生产者纪元、序列号等信息;每条 Record 则包含消息长度、属性、时间戳增量、偏移量增量、key、value 和 headers。

这里有两个面试常考点:其一,偏移量增量和时间戳增量采用变长编码,只存储与批次基准值的差值,从而显著压缩体积;其二,CRC 校验作用于整个批次,而不是单条消息,这也是为什么 Kafka 能高效地做批量校验。属性位中记录了压缩类型、时间戳类型(CreateTime 或 LogAppendTime)、是否为事务消息、是否为控制消息等标志。

消息格式的版本演进(v0、v1、v2)也值得关注:v2 引入了变长编码和批次级 CRC,是当前主流格式。面试中若被问到“为什么 Kafka 消息要按批次存储”,可以从减少网络开销、提高压缩率、降低索引粒度三个角度回答。

二、偏移量与日志段:分区内的顺序写入

每个分区对应一个日志目录,目录下由多个日志段(Log Segment)文件组成。日志段文件名即该段的 baseOffset,例如 00000000000000000000.log 表示起始偏移量为 0 的段。当前正在写入的段称为 active segment,其余为只读段。

偏移量(Offset)是分区内消息的唯一递增编号,由 broker 在写入时分配。消费者通过 offset 记录自己的消费位置,从而实现“可回溯”。这里要区分几个概念:Log Start Offset(日志起始偏移,因清理或删除而前移)、Log End Offset / LEO(下一条待写入消息的偏移)、High Watermark / HW(已提交、对消费者可见的最高偏移)。消费者只能读到 HW 之前的消息,这是 Kafka 保证一致性的核心机制。

日志段滚动由两个参数控制:log.segment.bytes(默认 1GB)和 log.roll.ms(默认 7 天)。当段大小或时间达到阈值,就关闭当前段并创建新段。段文件除了 .log,还有 .index、.timeindex 和 .snapshot 等配套文件。

三、索引与稀疏索引:用空间换查找效率

如果要在 TB 级日志中按 offset 查找消息,逐条扫描显然不可行。Kafka 为每个日志段维护两类索引:偏移量索引(.index)和时间戳索引(.timeindex)。它们都是稀疏索引——并非每条消息都建索引,而是每隔 log.index.interval.bytes(默认 4KB)才写入一条索引项。

偏移量索引的每一项是 8 字节:4 字节相对偏移量 + 4 字节物理位置。查找时,Kafka 先用二分查找在索引文件中定位到“不大于目标 offset 的最大索引项”,再从这个物理位置开始顺序扫描,直到找到目标消息。时间戳索引结构类似,用于按时间查找消息,常见于 offsetsForTimes 这类 API。

稀疏索引的设计权衡很清晰:索引越密,查找越快但占用空间越大;索引越稀,空间省但扫描范围变大。默认 4KB 间隔是经验值,通常无需调整。面试中若被追问“为什么不用稠密索引”,答案就是:Kafka 追求的是顺序写 + 少量顺序读,稀疏索引已足够,稠密索引会浪费内存和磁盘。

四、消息压缩:批次级压缩

Kafka 的压缩是批次级的,生产者将整个 Record Batch 压缩后发送,broker 原样存储,消费者拉取后解压。支持的算法包括 gzip、snappy、lz4、zstd。压缩发生在生产者端,broker 不解压(除非需要做格式转换),因此对 broker CPU 压力小。

压缩率与算法、消息冗余度相关。通常 zstd 在压缩率和吞吐之间平衡最好,lz4 解压速度最快。需要注意的是,压缩会增大单条消息的延迟(因为要攒批),所以 linger.ms 和 batch.size 需要与压缩策略配合。若消息 key 相同且分区策略按 key 哈希,同一批次内 key 重复度高,压缩效果会更好。

五、清理策略:delete 与 compact

Kafka 提供两种日志清理策略,通过 log.cleanup.policy 配置,可选 delete、compact 或两者组合。

delete 策略按时间或大小删除旧段。关键参数有 log.retention.hours(默认 168 小时)、log.retention.bytes(默认 -1,即不限)。删除以段为单位,因此实际保留时间可能略长于配置值。此外还有 log.retention.check.interval.ms 控制检查频率。

compact 策略用于保留每个 key 的最新 value,适合 changelog、状态存储等场景。它不会删除整个段,而是通过后台清理线程重写段文件,丢弃被覆盖的旧记录。相关参数包括 min.cleanable.dirty.ratio(默认 0.5,脏数据比例达到该值才触发清理)、min.compaction.lag.ms(消息至少保留多久才可被压缩)、delete.retention.ms(tombstone 标记保留时长)。

下面给出一个典型的 compact 主题配置示例,可直接用于状态类主题:

# 创建 compact 主题
kafka-topics.sh --create \
  --topic user-state-changelog \
  --partitions 6 \
  --replication-factor 3 \
  --config cleanup.policy=compact \
  --config min.cleanable.dirty.ratio=0.3 \
  --config min.compaction.lag.ms=60000 \
  --config delete.retention.ms=86400000 \
  --config segment.bytes=268435456

# 查看清理状态
kafka-log-dirs.sh --describe --bootstrap-server localhost:9092

执行后可通过 kafka-log-dirs.sh 观察各分区的 size 和 offsetLag,若 offsetLag 持续增长,说明清理速度跟不上写入速度,需要调大清理线程数 log.cleaner.threads 或降低 dirty.ratio 阈值。

六、面试实战要点

综合来看,本节的高频问题包括:Record Batch 的结构与变长编码、HW 与 LEO 的区别、稀疏索引的查找流程、压缩发生在哪一端、delete 与 compact 的适用场景。回答时建议先给结论,再补充参数和权衡,最后结合一个实际场景说明。例如被问到“如何设计一个状态存储主题”,可以回答:使用 compact 策略,设置合理的 min.cleanable.dirty.ratio 和 segment.bytes,配合 tombstone 处理删除,并通过 offsetLag 监控清理进度。

掌握这些机制后,你不仅能回答“是什么”,还能解释“为什么这样设计”以及“生产环境怎么调”,这正是面试官真正想听到的深度。