Kafka

Kafka 底层存储完整原理深度详解

·12 分钟阅读·4788 字
📋 目录

Kafka 底层存储完整原理深度详解

核心概念 Kafka 底层存储基于分区日志模型,通过顺序写入、页缓存、零拷贝实现高性能消息持久化。

1. 整体存储核心设计思想

Kafka 定位高吞吐消息队列,底层存储采用顺序写磁盘 + 页缓存零拷贝 + 分段日志架构,核心设计目标:

  1. 抛弃随机写,全程顺序写入磁盘:磁盘顺序写性能远超内存随机写,支撑百万级 TPS;
  2. 日志分段拆分:超大日志文件切分成小 segment,避免单文件过大删除、检索阻塞;
  3. 操作系统 PageCache 页缓存:读写全部依托内核缓存,减少用户态/内核态切换,几乎不占用JVM堆内存;
  4. 持久化不丢消息:消息落地磁盘,不依赖内存,宕机可恢复;
  5. 顺序读、批量读写、零拷贝传输,最大化磁盘与网卡吞吐。

1.1. 核心概念前置

  1. Topic:业务主题,逻辑分类;
  2. Partition 分区:Topic物理分片,一个分区对应一套独立日志存储目录,并行读写,水平扩容;
  3. Replica 副本:分区多副本存储,同步落盘,高可用防数据丢失;
  4. Segment 日志段:分区日志切分后的小文件,分为 .log 数据文件、.index 偏移索引、.timeindex 时间索引。

2. 分区(Partition)物理目录结构

每一个分区对应服务器磁盘上一个独立文件夹,命名格式:
{topic名称}-{分区编号}
示例:order-pay-0order-pay-1

目录内部三类核心文件:

  1. xxxx.log:消息主体日志文件,存储原始消息二进制;
  2. xxxx.index:偏移索引文件,offset → 文件物理位置;
  3. xxxx.timeindex:时间索引文件,时间戳 → offset;
    文件名前缀为该 segment 第一条消息的起始 offset,例如 00000000000000000000.log

3. Segment 分段日志机制(最核心存储设计)

3.1. 为什么要分段?

如果一个分区只有一个超大日志文件:

  • 删除过期数据需要截断大文件,IO阻塞;
  • 根据offset查找消息需要全量扫描文件,极慢;
  • 内存映射文件过大占用大量虚拟内存。

分段规则(两个阈值任一满足即新建segment):

  1. 单文件达到 log.segment.bytes(默认1G);
  2. 距离上一个segment创建超过 log.roll.ms(默认7天)。

3.2. Segment 文件生命周期

  1. 活跃segment(active):最新的segment,所有生产者写入全部追加到此文件,只能顺序追加,不可修改;
  2. 只读旧segment:写满后切换新文件,旧文件只读,仅用于消费读取;
  3. 过期删除:满足保留策略后整个segment直接删除(按文件整块删除,无需修改文件内容,性能极高)。

4. .log 消息日志文件结构

4.1. 写入模式:纯顺序追加 write-ahead-log(WAL)

生产者消息只会追加到文件末尾,绝不修改、插入、覆盖已有数据
每条消息是二进制结构化记录,批量存储(producer批量、server批量接收)。

4.2. 消息物理存储格式(V2 压缩记录批量格式,Kafka 0.11+ 默认)

一个segment由多条 RecordBatch(消息批次) 组成,一批包含多条Record:

  1. RecordBatch 头部:批次起始offset、批次长度、压缩算法、时间戳、生产者PID/序列号;
  2. Record 单条消息:
  • 变长offset增量(相对批次首offset)
  • 时间戳(创建时间)
  • key长度 + key二进制
  • value长度 + value二进制
  • headers 自定义标签

4.3. 消息持久化刷盘策略(fsync)

Kafka 不每条消息强制刷盘,依靠OS PageCache缓存,三种刷盘配置:

  1. log.flush.interval.messages:累积N条消息触发一次fsync;
  2. log.flush.interval.ms:最长间隔多久强制刷盘;
  3. 默认:不主动fsync,交给操作系统后台定时刷脏页。

数据安全保障不靠实时刷盘,依靠多副本同步复制;单节点断电PageCache脏页丢失,多副本可规避。

5. 索引文件:index / timeindex

5.1. 偏移索引 .index

解决问题:根据offset快速定位消息在log文件中的物理位置,避免顺序扫描整个日志。
存储结构:固定大小条目 (relative_offset, position)

  • relative_offset:相对于当前segment第一条offset的差值;
  • position:消息在log文件内字节偏移。

索引稀疏存储,默认每 log.index.interval.bytes=4k 写入一条索引,不会每条消息都存索引,节省磁盘空间;查询时二分查找索引,快速定位近似位置,再小范围扫描log。

5.2. 时间索引 .timeindex

结构:(timestamp, relative_offset)
根据时间范围查询消息时使用,二分查找得到对应offset,再通过offset索引定位数据。

5.3. 索引文件特点

index/timeindex 内存mmap映射,查询全部走内存,磁盘IO极少;segment删除时索引文件同步删除。

6. 页缓存 PageCache & 零拷贝 sendfile(高性能核心)

6.1. PageCache 工作机制

Kafka 所有log/index文件通过 mmap() 内存映射到内核PageCache:

  1. 生产者写入:数据写入内核缓存,用户进程JVM不缓存消息;
  2. 消费者读取:OS直接从PageCache返回数据,未命中才读取磁盘加载进缓存;
  3. Broker重启后缓存清空,首次消费会产生磁盘IO,后续重复消费走内存。

优势:

  • 避免JVM GC压力,海量消息不会堆溢出;
  • 热点消息常驻缓存,重复消费无磁盘访问。

6.2. 零拷贝 sendfile 消费传输

普通网络读取流程(四次拷贝):
磁盘 → 内核缓冲区 → JVM堆 → Socket缓冲区 → 网卡

Kafka 使用 Linux sendfile() 系统调用(零拷贝):
磁盘/PageCache → 内核Socket缓冲区 → 网卡
跳过用户态JVM内存,减少两次拷贝与上下文切换,吞吐提升数倍。

7. 副本存储与同步机制(高可用存储)

7.1. 多副本存储架构

每个Partition配置 replication-factor 副本数(通常3):

  • 1个Leader副本:所有生产者写入、消费者读取只走Leader;
  • 多个Follower副本:持续拉取Leader日志,完整复制segment文件,本地同等存储结构。

7.2. 副本同步流程

  1. Follower 发送FETCH请求,携带已同步最大offset;
  2. Leader 从PageCache读取日志数据批量返回;
  3. Follower 顺序写入本地segment日志,写入成功更新本地HW(高水位);
  4. Leader 记录每个Follower同步位置,更新全局HW。

7.3. HW 高水位 & LEO 日志末端偏移

  • LEO:Log End Offset,当前日志最新写入offset;
  • HW:High Watermark,所有副本已同步完成的最大offset;
    生产者消息只有被同步到至少ISR内所有副本,HW推进,消息才对外可见,消费者只能读到HW之前的数据,保证副本一致性。

7.4. ISR 同步副本集合

只有与Leader同步延迟在阈值内的副本才留在ISR;写入时必须等待ISR副本ACK,配置 acks=-1(all) 实现零丢失。

8. 日志清理与过期删除策略

磁盘不能无限膨胀,两种清理策略,按Topic配置:

8.1. 基于时间保留(默认)

log.retention.hours=168 保留7天,segment文件内最早消息过期则整个文件删除。

8.2. 基于磁盘大小保留

log.retention.bytes,分区总日志超过阈值,删除最旧segment。

8.3. 日志压缩 Log Compaction(特殊场景:状态变更消息)

普通删除是按时间删文件,压缩是保留每个key最新一条消息,删除旧重复key,用于存储数据库变更、配置快照等场景。
压缩后台单独线程执行,重写segment,丢弃过期key记录。

9. 读写完整流程串联

9.1. 生产者写入流程

  1. Producer批量消息发送到Partition Leader;
  2. Broker追加写入当前active segment末尾(PageCache);
  3. 更新index/timeindex稀疏索引;
  4. 等待ISR副本拉取同步完成(acks配置控制等待策略);
  5. 返回ACK给生产者,写入成功。

9.2. 消费者读取流程

  1. 消费者发起FETCH,指定offset;
  2. 二分查找对应segment的index索引,定位log文件字节位置;
  3. OS PageCache读取数据,sendfile零拷贝发给消费者;
  4. 消费者提交offset,保存到内部主题 __consumer_offsets

9.3. 偏移量存储:__consumer_offsets

消费者消费位点不存内存,持久化到系统内部Topic,同样采用分区segment日志存储结构,Broker重启offset不丢失。

10. 关键底层性能优化点总结

  1. 全程顺序IO:只追加不修改,磁盘性能拉满;
  2. 分段文件删除:过期整块删除,无文件截断开销;
  3. OS PageCache复用:消息不占JVM内存,热点消费纯内存读取;
  4. sendfile零拷贝:消费网络传输减少内存拷贝;
  5. 稀疏索引+二分查找:offset/时间快速定位消息;
  6. 批量读写:生产者批量、Broker批量同步副本,降低IO次数;
  7. 多副本ISR同步:持久化冗余,防止单盘数据丢失。

11. 常见底层故障与存储问题

  1. 磁盘IO飙升:新segment创建、首次消费缓存未命中、日志压缩运行;
  2. 副本同步延迟高:Follower磁盘慢、网络带宽不足,HW停滞,消费无法读取新消息;
  3. 磁盘满:retention保留时间过长,未自动清理segment;
  4. mmap虚拟内存占用高:大量segment同时映射,可调整分段大小;
  5. 断电丢失未刷盘消息:默认依赖PageCache,acks=-1多副本可规避业务数据丢失。

12. 面试速记核心要点

  1. 存储最小单元是Partition,物理为独立文件夹,内部由多个segment日志段组成;
  2. segment包含log(消息)、index(offset索引)、timeindex(时间索引),写满自动新建;
  3. 写入是WAL顺序追加,不修改旧数据;依靠OS PageCache缓存,不占用堆内存;
  4. 消费使用sendfile零拷贝,减少内核/用户态数据拷贝;
  5. 多副本ISR同步,HW高水位保证消费者只能读到已同步副本的数据;
  6. 日志清理两种模式:过期删除、log compaction压缩保留key最新值;
  7. 高性能三大基石:顺序磁盘写、页缓存、零拷贝传输;
  8. 消费者offset持久化存储在内部Topic __consumer_offsets,同样采用分段日志存储。

关联文档