一、日志文件结构
1、在磁盘的组织形式
从上图可以看到segment的文件组成:
- 以 *.log 结尾的日志文件
- 以 *.index 结尾的 offset 索引 文件
- 以 *.timeindex 结尾的 time offset 时间索引 文件
2、SEGMENT
日志文件达到一定的条件的时候需要进行切分,其对应的索引文件也会进行切分,日志文件满足以下条件之一就会进行切分。
- 当前日志文件的大小超过了 broker 端参数 log.segment.bytes 配置的值,默认值为 1073741824 ,即 1GB.
- 当前日志中第一条消息的时间戳与当前系统的时间戳差值大于 log.roll.ms 或者 log.roll.hours参数配置的值。如果同时配置了 log.roll.ms 和 log.roll.hours ,那么以log.roll.ms为准。默认只配置了 log.roll.hours 参数,其值为168,即 7天。
- 偏移量索引文件或时间戳索引文件的大小达到broker 端参数 log.index.size.max.bytes配置的值,默认值为 10485760, 即 10MB.
- 追加的消息的偏移量与当前日志分段的 偏移量之间的差值 大于 Integer.MAX_VALUE, 即(offset - baseOffset )> Integer.MAX_VALUE 。
3、索引文件
Kafka中的索引文件以稀疏索引的方式构造消息的索引,它并不保证每个消息在索引文件中都有对应的索引项。每当写入一定量 (broker参数 log.index.interval.bytes指定),默认为 4096 ,即 4KB 的消息时,偏移量索引文件和时间戳索引文件分别增加一个偏移量索引项和时间戳索引项。
- 偏移量索引
建立消息偏移量offset到物理地址之间的映射关系,方便快速定位消息所在的物理文件位置,如下图: - 每个索引项占用8个字节,分为2个部分。
relativeOffset : 相对偏移量, 表示消息相对于 baseOffset的偏移量,占用4个字节,当前索引文件的文 件名为baseOffset的值。
position: 物理地址,也就是消息在日志分段文件中对应的物理位置,占用4个字节。
baseOffset: segment第一个message的Offset - 时间戳索引
则根据指定的时间戳(timestamp)来查找对应的偏移量信息,如下图: - 每个索引项占用12个字节,分为2个部分。
timestamp : 当前日志分段最大的时间戳
relativeOffset : 时间戳所对应的消息的相对于 baseOffset的偏移量
时间戳索引文件中包含若干时间戳索引项,每个追加的时间戳索引项中的timestamp 必须大于之前追加的 索引项的 timestamp, 否则不予追加。如果 broker 段参数 log.message.timestamp.type 设置为 LogAppendTime, 那么消息的时间戳必定能够保持单调递增,相反,如果是 CreateTime 类型则无法保证。
与偏移量索引文件相似,时间戳索引文件大小必须是索引项大小 12B的整数倍,如果不满足条件也会进行裁剪。同样假设 broker 端参数 log.index.size.max.bytes 配置为 67, 那么对应于时间戳索引文件,Kafka 会在内部将其转为 60.
4、日志查找
- 查找offset=350的消息,先二分查找确定消息在哪个segment中
- 到对应segment的.index文件中找到小于350的最大偏移量,即345,其对应的物理位置是 328
- 根据物理位置328,直接定位到.log文件的328文件位置
- 顺序读取每条消息的偏移量,但不读取消息内容
- 找到offset=350的消息,得到物理位置为448
- 开始真正读取offset=350的消息内容并返回
二、日志压缩(log compaction, 或者叫日志清洗更准确)
除segment删除规则之外,kafka还提供另一种数据清洗策略,对于有相同key不同value值的message,只保留最新版本在kafka中。
用于保存消费者offset的主题”__consumer_offsets”使用的就是这种策略
每个日志目录下,有名为”cleaner-offset-checkpoint”的文件,根据该文件可以将日志文件分成3部分:
- clean:偏移量是断续的,经过压缩的部分
- dirty:偏移量是连续的,未清理过的部分
- activeSegment:活跃的热点数据,不参与log compaction。默认情况下firstUncleanableOffset等于activeSegment的baseOffset
log compaction使用时应注意每个消息的key值不为null。
1、Log compaction步骤:
- a、污浊率:dirtyRatio = dirtyBytes / (cleanBytes + dirtyBytes),选择最高的合并,可通过log.cleaner.min.cleanable.ratio参数配置(默认0.5);
- b、创建一个名为”SkimpyOffsetMap”的哈希表来构建key与offset的映射表;
- c、首次遍历log文件,把每个key的hashCode和最后出现的offset都保存在SkimpyOffsetMap中
- d、第N次遍历log文件,检查每个消息是否符合保留条件,如果符合就保留下来,否则就会被清理掉。消息保留条件:假设一条消息的offset为O1,这条消息的key在SkimpyOffsetMap中所对应的offset为O2,如果O1>=O2即为满足保留条件
2、日志压缩举例:
1)、日志清理
- 第一次日志压缩,清理点为0,日志头部的范围从0到活动分段的基准偏移量13
- 第一次压缩后,清理点更新为13,第二次日志压缩时,日志头部范围从13到活动日志分段的基准偏移量20,日志尾部范围从0到清理点的位置13
- 第二次压缩后,清理点更新为20,第三次日志压缩时,日志头部范围从20到活动日志分段的基准偏移量28,日志尾部范围从2到清理点的位置20
2)、压缩合并:
- 第一次日志压缩,清理点等于0,没有尾部日志,日志头部从6:00到7:40,所有segment文件都是1GB.
- 第一次日志压缩后,清理点改为日志头部末尾即7:40,每个新日志分段的大小都小于1GB
- 第二次日志压缩时,清理点为7:40,日志头部从8:00到8:10,日志尾部从6:00到7:40,压缩操作会将多个小文件分成1组,每一组不超过1GB