第四十二章:Kafka LogSegment 源码深度解析——Segment滚动、Index建立与磁盘文件管理
前言
上一章,我们已经深入分析了 Kafka 的日志系统:
Producer
│
▼
KafkaApis
│
▼
ReplicaManager
│
▼
Partition
│
▼
UnifiedLog
我们知道:
一个 Partition:
对应一个 UnifiedLog。
但是:
UnifiedLog 并不真正保存消息。
真正负责管理磁盘文件的是:
kafka.log.LogSegment
一个 Partition:
实际上是:
UnifiedLog
│
┌────┴────┐
│ │ │
▼ ▼ ▼
Segment Segment Segment
Kafka 能做到:
- TB 级日志存储
- 秒级 Broker 恢复
- 快速 Offset 查询
- 快速删除历史数据
背后全部依赖:
LogSegment + Index 文件体系。
本章深入:
- LogSegment源码结构
- Segment文件组成
- FileRecords设计
- OffsetIndex原理
- TimeIndex原理
- Segment创建
- Segment滚动(Roll)
- Segment恢复
- Segment删除
- Kafka磁盘文件管理机制
一、什么是 LogSegment?
源码:
kafka.log.LogSegment
一句话:
LogSegment 是 Kafka 一个日志段(Segment)的完整抽象。
一个 Segment:
对应磁盘上的一组文件。
例如:
00000000000000000000.log
00000000000000000000.index
00000000000000000000.timeindex
00000000000000000000.txnindex
所有文件:
拥有相同的 Base Offset。
二、为什么需要 Segment?
假设:
一个 Topic:
每天:
100GB。
一年:
36TB。
如果:
只有一个:
order.log
问题:
- 文件太大
- 删除困难
- 恢复缓慢
- Index巨大
所以:
Kafka:
切分日志。
例如:
Segment0
0~999999
↓
Segment1
1000000~
↓
Segment2
2000000~
每个 Segment:
独立管理。
三、Segment文件组成
一个 Segment:
包括:
00000000000000000000.log
↓
消息内容
00000000000000000000.index
↓
Offset Index
00000000000000000000.timeindex
↓
Time Index
00000000000000000000.txnindex
↓
事务索引(可选)
还有:
leader-epoch-checkpoint
记录:
Leader Epoch。
四、LogSegment源码结构
源码简化:
class LogSegment {
val log: FileRecords
val offsetIndex: OffsetIndex
val timeIndex: TimeIndex
val txnIndex: TransactionIndex
}
四个核心成员:
| 成员 | 作用 |
|---|---|
| FileRecords | 消息存储 |
| OffsetIndex | Offset索引 |
| TimeIndex | 时间索引 |
| TransactionIndex | 事务索引 |
五、FileRecords是什么?
源码:
org.apache.kafka.common.record.FileRecords
一句话:
FileRecords 管理
.log文件。
底层:
使用:
FileChannel
实现:
顺序追加。
流程:
MemoryRecords
↓
FileRecords.append()
↓
FileChannel.write()
↓
Page Cache
六、消息如何追加到 Segment?
调用链:
UnifiedLog
↓
LogSegment.append()
↓
FileRecords.append()
↓
FileChannel.write()
整个过程:
完全:
顺序写。
没有:
随机修改。
七、OffsetIndex是什么?
源码:
kafka.log.OffsetIndex
作用:
快速定位:
Offset。
否则:
如果:
100GB日志。
读取:
Offset=9000000。
需要:
扫描整个文件。
效率极低。
八、OffsetIndex存储结构
Kafka:
不会:
每条消息:
建立索引。
而是:
稀疏索引(Sparse Index)。
例如:
Offset
0
1000
2000
3000
对应:
Position
0
8192
16384
24576
读取:
Offset=2500。
先找到:
2000
然后:
顺序扫描。
因此:
Index很小。
九、为什么采用稀疏索引?
假设:
100亿条消息。
如果:
每条:
建立Index。
Index:
可能:
几十GB。
采用:
稀疏索引:
Index:
通常:
只有几十MB。
节省:
大量内存。
十、TimeIndex是什么?
源码:
kafka.log.TimeIndex
作用:
按时间:
查找消息。
例如:
Producer:
发送:
2026-08-01 12:00
Kafka:
保存:
Timestamp
↓
Offset
Consumer:
可以:
按时间:
定位Offset。
例如:
offsetsForTimes()
十一、TransactionIndex
事务Topic:
维护:
BeginTxn
CommitTxn
AbortTxn
EOS:
读取:
依赖:
TransactionIndex。
普通Topic:
没有:
TxnIndex。
十二、LogSegment.append()
核心方法:
append()
流程:
收到MemoryRecords
↓
写.log
↓
更新OffsetIndex
↓
更新时间索引
↓
更新大小
十三、什么时候更新Index?
Kafka:
不是:
每条消息:
都更新Index。
由:
log.index.interval.bytes
控制。
默认:
4096 Bytes。
例如:
写:
4KB。
建立:
一条Index。
因此:
Index:
非常小。
十四、Segment什么时候Roll?
Kafka:
当前:
Active Segment。
满足:
任意条件:
Roll。
包括:
文件大小
log.segment.bytes
默认:
1GB。
时间
log.roll.ms
例如:
24小时。
时间戳变化
日志时间:
超过限制。
十五、Segment Roll源码流程
调用:
UnifiedLog.roll()
流程:
关闭旧Segment
↓
Flush
↓
创建新Segment
↓
更新activeSegment
例如:
旧:
00000000000000000000.log
新:
00000000001000000000.log
Base Offset:
就是:
下一条消息Offset。
十六、为什么Base Offset作为文件名?
例如:
00000000003000000000.log
说明:
第一条消息:
Offset:
3000000000
因此:
Kafka:
可以快速:
定位:
Segment。
例如:
请求:
Offset=3200000000
直接:
找到:
对应Segment。
无需:
遍历所有文件。
十七、Segment读取流程
Consumer:
请求:
Offset=2500
流程:
UnifiedLog
↓
找到Segment
↓
OffsetIndex.lookup()
↓
FileRecords.search()
↓
读取消息
时间复杂度:
约:
O(logN)+顺序扫描。
十八、Segment恢复
Broker:
异常退出。
启动:
执行:
LogSegment.recover()
流程:
扫描.log
↓
校验CRC
↓
重建Index
↓
更新LEO
如果:
发现:
坏消息。
Kafka:
截断。
十九、Segment删除
Retention:
到期。
例如:
log.retention.hours=168
Kafka:
删除:
整个:
Segment。
例如:
SegmentA
×
删除
原因:
Segment:
独立。
无需:
移动数据。
效率:
极高。
二十、为什么Kafka删除很快?
传统数据库:
删除:
DELETE
需要:
修改数据。
Kafka:
删除:
整个:
文件。
例如:
rm 00000000000000000000.log
因此:
速度:
极快。
二十一、Segment恢复为什么快?
假设:
Broker:
宕机。
恢复:
不用:
扫描:
全部Partition。
只恢复:
最后几个:
Active Segment。
历史:
Closed Segment:
无需:
重新计算。
因此:
启动:
非常快。
二十二、Segment生命周期
完整流程:
创建
↓
Active
↓
不断Append
↓
Roll
↓
Closed
↓
Retention删除
二十三、Segment状态
主要:
两种:
Active Segment
当前:
写入。
Closed Segment
历史:
只读。
Consumer:
可以:
读取:
所有Segment。
Producer:
只能:
写:
Active。
二十四、源码核心方法
append()
追加消息。
read()
读取消息。
recover()
恢复。
flush()
刷盘。
close()
关闭。
deleteIfExists()
删除。
二十五、LogSegment调用关系
Producer
↓
Partition
↓
UnifiedLog
↓
LogSegment
↓
FileRecords
↓
FileChannel
读取:
Consumer
↓
UnifiedLog
↓
LogSegment
↓
OffsetIndex
↓
FileRecords
二十六、源码核心类总结
| 类 | 作用 |
|---|---|
| LogSegment | 一个日志段 |
| FileRecords | .log文件 |
| OffsetIndex | Offset索引 |
| TimeIndex | 时间索引 |
| TransactionIndex | 事务索引 |
| LazyIndex | Index懒加载 |
二十七、面试题
1、为什么Kafka使用Segment?
避免超大文件。
支持快速删除。
支持快速恢复。
2、为什么文件名是Base Offset?
方便快速定位Segment。
3、OffsetIndex为什么是稀疏索引?
减少Index大小。
降低内存占用。
4、什么时候Roll?
文件大小。
时间。
特殊条件。
5、Kafka删除为什么快?
直接删除整个Segment文件。
6、Broker恢复为什么快?
主要恢复活跃Segment。
历史Segment无需重新构建。
二十八、本章总结
LogSegment:
是真正管理磁盘文件的核心对象。
整体结构:
LogSegment
├── FileRecords
├── OffsetIndex
├── TimeIndex
└── TransactionIndex
写入流程:
MemoryRecords
↓
LogSegment.append()
↓
FileRecords.write()
↓
Index更新
↓
Page Cache
读取流程:
Offset
↓
OffsetIndex
↓
Position
↓
FileRecords
↓
Message
牢记:
Kafka 并不是一个巨大日志文件,而是由多个 LogSegment 组成;每个 Segment 又由消息文件和多个索引文件组成,借助稀疏索引、顺序写和按段管理,实现了高吞吐、快速恢复和高效删除。
下一章预告
第四十三章:Kafka FileRecords 源码深度解析——消息格式、RecordBatch 与磁盘存储布局
下一章深入:
- FileRecords源码结构
- MemoryRecords 与 FileRecords
- RecordBatch设计
- Record格式(V2)
- CRC校验
- 压缩消息存储
- FileChannel写入流程
- 零拷贝读取流程
彻底理解:
Kafka 一条消息在磁盘文件中到底是如何组织和存储的。