Kafka 作为高性能分布式消息队列,其存储架构设计是其高性能的关键。Kafka 将每个主题(Topic)的分区(Partition)映射到服务器上的一个目录,每个分区对应一个日志目录(Log Directory)。分区中的日志文件被划分为多个段(Segment),每个段由两个核心文件组成:日志文件(.log)和索引文件(.index)。
Segment 是 Kafka 日志存储的基本单元,每个 Segment 文件都有一个唯一的 baseSequenceNumber 标识。当 Segment 文件大小达到配置的阈值(log.segment.bytes)或保留时间达到 log.segment.ms 时,将创建一个新的 Segment 文件。这种设计不仅便于日志管理,还能提高读写性能。
日志文件的结构非常简单,顺序追加写入,采用二进制格式存储消息。每条消息包含以下关键信息:
- 8字节的偏移量(Offset):消息在分区中的唯一标识
- 4字节的消息大小
- 4字节的 CRC32 校验码
- 1字节的魔术字节(magic)
- 1字节的属性标志
- 8字节的时间戳(timestamp)
- 变长消息键(key)
- 变长消息值(value)
Segment 文件的组织采用顺序写入、随机读取的模式,这种设计充分利用了顺序写盘的高效性,同时通过索引机制支持快速查找。
Kafka 使用稀疏索引来加速消息查找,而不是为每条消息都建立索引。索引文件是一个稀疏索引,只记录部分消息的偏移量与在日志文件中的物理位置映射关系,显著减少索引文件大小和内存占用。
索引文件格式简单,每条索引记录包含:
- 8字节的消息偏移量
- 4字节的日志文件中物理位置(相对段文件起始位置的偏移)
当消费者或生产者需要查找特定偏移量的消息时,Kafka 会执行以下步骤:
稀疏索引的精度由 log.index.interval.bytes 参数控制,表示每隔多少字节创建一个索引项。较小的值提供更精确的索引,但会增加索引文件大小和索引构建时间。
Kafka 还使用内存索引(MMap)来加速查找,将索引文件映射到内存中,减少磁盘I/O。内存索引采用哈希表结构,存储最近的偏移量与物理位置的映射,进一步提高查找速度。
Kafka 提供两种日志清理策略:基于时间的保留策略(基于时间)和基于大小的清理策略(基于大小)。这两种策略可以同时启用,Kafka 会根据条件先满足其中一个策略。
3.1 基于时间的保留策略
基于时间的保留策略通过 log.retention.hours、log.retention.minutes 和 log.retention.ms 参数配置,决定日志数据保留的时间长度。当检测到日志文件的修改时间早于当前时间减去保留时间时,该文件将被删除或标记为可删除。
Kafka 定期检查日志段的修改时间,并删除过期的日志段。检查周期由 log.retention.check.interval.ms 参数控制,默认为 5 分钟。
3.2 基于大小的清理策略
基于大小的清理策略通过 log.retention.bytes 参数配置,当分区总大小超过该阈值时,将删除最旧的日志段,直到分区大小低于阈值。
此外,Kafka 还提供 log.segment.bytes 参数控制单个日志段的最大大小,log.roll.ms 和 log.roll.hours 控制日志段滚动(创建新段)的时间阈值。
3.3 压缩策略
对于启用压缩的主题,Kafka 还提供基于偏移量的压缩策略。当消费者组已完成某个偏移量的消费时,Kafka 可以删除该偏移量之前的日志数据,即使这些数据尚未达到时间或大小阈值。
// 示例:Kafka 日志清理配置
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("group.id", "test-group");
props.put("enable.auto.commit", "false");
props.put("auto.offset.reset", "earliest");
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
// 设置日志清理策略
props.put("log.retention.hours", "72"); // 保留72小时
props.put("log.retention.bytes", "1073741824"); // 保留1GB
props.put("log.segment.bytes", "1073741824"); // 单个日志段最大1GB
props.put("log.cleanup.policy", "delete,compact"); // 启用删除和压缩
4.1 Segment 大小配置
Segment 大小是 Kafka 日志存储的重要参数,直接影响读写性能和磁盘利用率。较大的 Segment 减少了文件数量,有利于文件系统缓存,但会增加单次操作的时间成本。较小的 Segment 提高灵活性,但会增加文件系统元数据开销。
推荐配置:
- 对于高吞吐量场景:log.segment.bytes=1GB
- 对于低延迟场景:log.segment.bytes=100-500MB
4.2 索引优化
索引文件大小直接影响内存占用和查找性能。log.index.interval.bytes 参数控制索引精度:
- 较小值(例如 128B):提高查找精度,增加索引文件大小
- 较大值(例如 4KB):减少索引文件大小,降低查找精度
推荐根据实际查询模式调整索引间隔,平衡内存使用和查询性能。
4.3 清理策略调优
清理策略应根据业务需求合理配置:
- 对于短期数据应用:设置较短保留时间(如 24-72 小时)
- 对于长期数据应用:设置保留时间的同时限制总大小
- 对于压缩主题:合理配置压缩策略,避免频繁压缩影响性能
4.4 监控与维护
定期监控 Kafka 日志存储指标,包括:
- 分区大小和数量
- 日志段数量和大小
- 索引文件大小和内存使用
- 清理操作频率和耗时
及时发现存储异常,进行必要的参数调整或扩容。
以下是一个简单的 Kafka 日志存储监控脚本示例:
#!/bin/bash
# Kafka 日志存储监控脚本
KAFKA_HOME=/path/to/kafka
TOPIC_NAME="test-topic"
# 获取分区信息
PARTITIONS=$($KAFKA_HOME/bin/kafka-topics.sh –bootstrap-server localhost:9092 –describe –topic $TOPIC_NAME | grep -v "Topic:" | awk '{print $1}')
# 监控每个分区
for PARTITION in $PARTITIONS; do
LOG_DIR=$($KAFKA_HOME/bin/kafka-topics.sh –bootstrap-server localhost:9092 –describe –topic $TOPIC_NAME | grep $PARTITION | awk '{print $6}')
LOG_SIZE=$(du -sb $LOG_DIR | cut -f1)
SEGMENT_COUNT=$(ls -1 $LOG_DIR/*.log | wc -l)
echo "Partition: $PARTITION, Log Size: $LOG_SIZE bytes, Segment Count: $SEGMENT_COUNT"
done
4.5 注意事项
Kafka 日志存储流程图
#publish-mermaid-1788279541544-0{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#publish-mermaid-1788279541544-0 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#publish-mermaid-1788279541544-0 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#publish-mermaid-1788279541544-0 .error-icon{fill:#552222;}#publish-mermaid-1788279541544-0 .error-text{fill:#552222;stroke:#552222;}#publish-mermaid-1788279541544-0 .edge-thickness-normal{stroke-width:1px;}#publish-mermaid-1788279541544-0 .edge-thickness-thick{stroke-width:3.5px;}#publish-mermaid-1788279541544-0 .edge-pattern-solid{stroke-dasharray:0;}#publish-mermaid-1788279541544-0 .edge-thickness-invisible{stroke-width:0;fill:none;}#publish-mermaid-1788279541544-0 .edge-pattern-dashed{stroke-dasharray:3;}#publish-mermaid-1788279541544-0 .edge-pattern-dotted{stroke-dasharray:2;}#publish-mermaid-1788279541544-0 .marker{fill:#333333;stroke:#333333;}#publish-mermaid-1788279541544-0 .marker.cross{stroke:#333333;}#publish-mermaid-1788279541544-0 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#publish-mermaid-1788279541544-0 p{margin:0;}#publish-mermaid-1788279541544-0 .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#publish-mermaid-1788279541544-0 .cluster-label text{fill:#333;}#publish-mermaid-1788279541544-0 .cluster-label span{color:#333;}#publish-mermaid-1788279541544-0 .cluster-label span p{background-color:transparent;}#publish-mermaid-1788279541544-0 .label text,#publish-mermaid-1788279541544-0 span{fill:#333;color:#333;}#publish-mermaid-1788279541544-0 .node rect,#publish-mermaid-1788279541544-0 .node circle,#publish-mermaid-1788279541544-0 .node ellipse,#publish-mermaid-1788279541544-0 .node polygon,#publish-mermaid-1788279541544-0 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#publish-mermaid-1788279541544-0 .rough-node .label text,#publish-mermaid-1788279541544-0 .node .label text,#publish-mermaid-1788279541544-0 .image-shape .label,#publish-mermaid-1788279541544-0 .icon-shape .label{text-anchor:middle;}#publish-mermaid-1788279541544-0 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#publish-mermaid-1788279541544-0 .rough-node .label,#publish-mermaid-1788279541544-0 .node .label,#publish-mermaid-1788279541544-0 .image-shape .label,#publish-mermaid-1788279541544-0 .icon-shape .label{text-align:center;}#publish-mermaid-1788279541544-0 .node.clickable{cursor:pointer;}#publish-mermaid-1788279541544-0 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#publish-mermaid-1788279541544-0 .arrowheadPath{fill:#333333;}#publish-mermaid-1788279541544-0 .edgePath .path{stroke:#333333;stroke-width:1px;}#publish-mermaid-1788279541544-0 .flowchart-link{stroke:#333333;fill:none;}#publish-mermaid-1788279541544-0 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#publish-mermaid-1788279541544-0 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#publish-mermaid-1788279541544-0 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#publish-mermaid-1788279541544-0 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#publish-mermaid-1788279541544-0 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#publish-mermaid-1788279541544-0 .cluster text{fill:#333;}#publish-mermaid-1788279541544-0 .cluster span{color:#333;}#publish-mermaid-1788279541544-0 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#publish-mermaid-1788279541544-0 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#publish-mermaid-1788279541544-0 rect.text{fill:none;stroke-width:0;}#publish-mermaid-1788279541544-0 .icon-shape,#publish-mermaid-1788279541544-0 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#publish-mermaid-1788279541544-0 .icon-shape p,#publish-mermaid-1788279541544-0 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#publish-mermaid-1788279541544-0 .icon-shape .label rect,#publish-mermaid-1788279541544-0 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#publish-mermaid-1788279541544-0 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#publish-mermaid-1788279541544-0 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#publish-mermaid-1788279541544-0 .node .neo-node{stroke:#9370DB;}#publish-mermaid-1788279541544-0 [data-look=\”neo\”].node rect,#publish-mermaid-1788279541544-0 [data-look=\”neo\”].cluster rect,#publish-mermaid-1788279541544-0 [data-look=\”neo\”].node polygon{stroke:#9370DB;filter:drop-shadow(1px 2px 2px rgba(185, 185, 185, 1));}#publish-mermaid-1788279541544-0 [data-look=\”neo\”].swimlane.cluster rect{filter:none;}#publish-mermaid-1788279541544-0 [data-look=\”neo\”].node path{stroke:#9370DB;stroke-width:1px;}#publish-mermaid-1788279541544-0 [data-look=\”neo\”].node .outer-path{filter:drop-shadow(1px 2px 2px rgba(185, 185, 185, 1));}#publish-mermaid-1788279541544-0 [data-look=\”neo\”].node .neo-line path{stroke:#9370DB;filter:none;}#publish-mermaid-1788279541544-0 [data-look=\”neo\”].node circle{stroke:#9370DB;filter:drop-shadow(1px 2px 2px rgba(185, 185, 185, 1));}#publish-mermaid-1788279541544-0 [data-look=\”neo\”].node circle .state-start{fill:#000000;}#publish-mermaid-1788279541544-0 [data-look=\”neo\”].icon-shape .icon{fill:#9370DB;filter:drop-shadow(1px 2px 2px rgba(185, 185, 185, 1));}#publish-mermaid-1788279541544-0 [data-look=\”neo\”].icon-shape .icon-neo path{stroke:#9370DB;filter:drop-shadow(1px 2px 2px rgba(185, 185, 185, 1));}#publish-mermaid-1788279541544-0 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}是否是否
生产者发送消息
消息写入当前Segment
Segment是否已满
创建新Segment
更新索引文件
检查是否需要清理
执行日志清理
消费者读取消息
删除过期或已消费的Segment
消费者提交偏移量
Kafka 日志存储关键参数对比
| 参数 | 默认值 | 说明 |
|——|——–|——|
| log.segment.bytes | 1073741824 | 单个Segment文件的最大大小,默认为1GB |
| log.index.interval.bytes | 4096 | 索引项间隔,默认为4KB |
| log.retention.hours | 168 | 日志保留时间,默认为168小时(7天) |
| log.retention.bytes | -1 | 日志保留大小,-1表示不限制 |
| log.roll.ms | 86400000 | Segment滚动时间,默认为24小时 |
| log.cleanup.policy | delete | 日志清理策略,delete或compact |
通过深入理解 Kafka 日志存储格式、Segment 组织、稀疏索引机制和日志清理策略,可以更好地优化 Kafka 集群性能,满足不同业务场景的需求。
最小示例:
// 创建一个简单的 Kafka 生产者,展示消息写入流程
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("acks", "all");
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
Producer<String, String> producer = new KafkaProducer<>(props);
for (int i = 0; i < 100; i++) {
producer.send(new ProducerRecord<>("test-topic", "key" + i, "value" + i));
}
producer.close();
注意事项:

