Kafka 底层存储完整原理深度详解
核心概念 Kafka 底层存储基于分区日志模型,通过顺序写入、页缓存、零拷贝实现高性能消息持久化。
1. 整体存储核心设计思想
Kafka 定位高吞吐消息队列,底层存储采用顺序写磁盘 + 页缓存零拷贝 + 分段日志架构,核心设计目标:
- 抛弃随机写,全程顺序写入磁盘:磁盘顺序写性能远超内存随机写,支撑百万级 TPS;
- 日志分段拆分:超大日志文件切分成小 segment,避免单文件过大删除、检索阻塞;
- 操作系统 PageCache 页缓存:读写全部依托内核缓存,减少用户态/内核态切换,几乎不占用JVM堆内存;
- 持久化不丢消息:消息落地磁盘,不依赖内存,宕机可恢复;
- 顺序读、批量读写、零拷贝传输,最大化磁盘与网卡吞吐。
1.1. 核心概念前置
- Topic:业务主题,逻辑分类;
- Partition 分区:Topic物理分片,一个分区对应一套独立日志存储目录,并行读写,水平扩容;
- Replica 副本:分区多副本存储,同步落盘,高可用防数据丢失;
- Segment 日志段:分区日志切分后的小文件,分为
.log数据文件、.index偏移索引、.timeindex时间索引。
2. 分区(Partition)物理目录结构
每一个分区对应服务器磁盘上一个独立文件夹,命名格式:
{topic名称}-{分区编号}
示例:order-pay-0、order-pay-1
目录内部三类核心文件:
xxxx.log:消息主体日志文件,存储原始消息二进制;xxxx.index:偏移索引文件,offset → 文件物理位置;xxxx.timeindex:时间索引文件,时间戳 → offset;
文件名前缀为该 segment 第一条消息的起始 offset,例如00000000000000000000.log。
3. Segment 分段日志机制(最核心存储设计)
3.1. 为什么要分段?
如果一个分区只有一个超大日志文件:
- 删除过期数据需要截断大文件,IO阻塞;
- 根据offset查找消息需要全量扫描文件,极慢;
- 内存映射文件过大占用大量虚拟内存。
分段规则(两个阈值任一满足即新建segment):
- 单文件达到
log.segment.bytes(默认1G); - 距离上一个segment创建超过
log.roll.ms(默认7天)。
3.2. Segment 文件生命周期
- 活跃segment(active):最新的segment,所有生产者写入全部追加到此文件,只能顺序追加,不可修改;
- 只读旧segment:写满后切换新文件,旧文件只读,仅用于消费读取;
- 过期删除:满足保留策略后整个segment直接删除(按文件整块删除,无需修改文件内容,性能极高)。
4. .log 消息日志文件结构
4.1. 写入模式:纯顺序追加 write-ahead-log(WAL)
生产者消息只会追加到文件末尾,绝不修改、插入、覆盖已有数据。
每条消息是二进制结构化记录,批量存储(producer批量、server批量接收)。
4.2. 消息物理存储格式(V2 压缩记录批量格式,Kafka 0.11+ 默认)
一个segment由多条 RecordBatch(消息批次) 组成,一批包含多条Record:
- RecordBatch 头部:批次起始offset、批次长度、压缩算法、时间戳、生产者PID/序列号;
- Record 单条消息:
- 变长offset增量(相对批次首offset)
- 时间戳(创建时间)
- key长度 + key二进制
- value长度 + value二进制
- headers 自定义标签
4.3. 消息持久化刷盘策略(fsync)
Kafka 不每条消息强制刷盘,依靠OS PageCache缓存,三种刷盘配置:
log.flush.interval.messages:累积N条消息触发一次fsync;log.flush.interval.ms:最长间隔多久强制刷盘;- 默认:不主动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:
- 生产者写入:数据写入内核缓存,用户进程JVM不缓存消息;
- 消费者读取:OS直接从PageCache返回数据,未命中才读取磁盘加载进缓存;
- 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. 副本同步流程
- Follower 发送FETCH请求,携带已同步最大offset;
- Leader 从PageCache读取日志数据批量返回;
- Follower 顺序写入本地segment日志,写入成功更新本地HW(高水位);
- 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. 生产者写入流程
- Producer批量消息发送到Partition Leader;
- Broker追加写入当前active segment末尾(PageCache);
- 更新index/timeindex稀疏索引;
- 等待ISR副本拉取同步完成(acks配置控制等待策略);
- 返回ACK给生产者,写入成功。
9.2. 消费者读取流程
- 消费者发起FETCH,指定offset;
- 二分查找对应segment的index索引,定位log文件字节位置;
- OS PageCache读取数据,sendfile零拷贝发给消费者;
- 消费者提交offset,保存到内部主题
__consumer_offsets。
9.3. 偏移量存储:__consumer_offsets
消费者消费位点不存内存,持久化到系统内部Topic,同样采用分区segment日志存储结构,Broker重启offset不丢失。
10. 关键底层性能优化点总结
- 全程顺序IO:只追加不修改,磁盘性能拉满;
- 分段文件删除:过期整块删除,无文件截断开销;
- OS PageCache复用:消息不占JVM内存,热点消费纯内存读取;
- sendfile零拷贝:消费网络传输减少内存拷贝;
- 稀疏索引+二分查找:offset/时间快速定位消息;
- 批量读写:生产者批量、Broker批量同步副本,降低IO次数;
- 多副本ISR同步:持久化冗余,防止单盘数据丢失。
11. 常见底层故障与存储问题
- 磁盘IO飙升:新segment创建、首次消费缓存未命中、日志压缩运行;
- 副本同步延迟高:Follower磁盘慢、网络带宽不足,HW停滞,消费无法读取新消息;
- 磁盘满:retention保留时间过长,未自动清理segment;
- mmap虚拟内存占用高:大量segment同时映射,可调整分段大小;
- 断电丢失未刷盘消息:默认依赖PageCache,acks=-1多副本可规避业务数据丢失。
12. 面试速记核心要点
- 存储最小单元是Partition,物理为独立文件夹,内部由多个segment日志段组成;
- segment包含log(消息)、index(offset索引)、timeindex(时间索引),写满自动新建;
- 写入是WAL顺序追加,不修改旧数据;依靠OS PageCache缓存,不占用堆内存;
- 消费使用sendfile零拷贝,减少内核/用户态数据拷贝;
- 多副本ISR同步,HW高水位保证消费者只能读到已同步副本的数据;
- 日志清理两种模式:过期删除、log compaction压缩保留key最新值;
- 高性能三大基石:顺序磁盘写、页缓存、零拷贝传输;
- 消费者offset持久化存储在内部Topic __consumer_offsets,同样采用分段日志存储。
关联文档