kafka中消息是通过主题来进行区分的,一个主题会有多个分区,每个分区会有多个副本,每个副本都会对应有一个log日志进行存储,每个log日志会对应有多个logSegment日志段,每个logSegment包含一个日志文件(.log后缀)和两个索引文件( .index和 .timeindex后缀);按照是否再有消息写入分为活跃日志分段(activeSegment)和非活跃日志分段logSegment;日志文件的命令,都是基于基准偏移量(baseOffset)进行命名的,名称固定为20位数字,没有达到的位数用0填充。
日志存储(log文件)
log文件中存放的是具体的每条消息,其每条消息的具体格式如下
各字段描述如下
- length:消息总长度。
- attributes:弃用,但还存在消息中,以备未来扩展使用。
- timestamp delta:时间戳增量。
- offset delta:位移偏移量。
- key length:表示key的长度。
- key:表示key的值。
- value length:表示value的长度。
- value:表示value的值
- headers count:消息中headers的数量。
- headers:用来支持应用级别的扩展。
如上所诉,一个日志文件,会包含多个日志分段(logSegment),那么日志的分段策略如下,只要满足其中一条,日志就会进行分段操作:
- 当前日志分段文件大小超过了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,即要追加的消息的偏移量不能转变为相对偏移量。
偏移量索引
偏移量索引:每个索引占用8个字节,分为两个部分。索引文件的命名及时baseOffset的值。
- relativeOffset:相对偏移量,表示消息相对于baseOffset的偏移量,占用4个字节。使用相对相对偏移量,只需要占用4个字节,如果使用的是绝对偏移量需要8个字节,这样可以节省空间大小,相对偏移量的计算公式为relativeOffset = offset - baseOffset。
- position:物理地址,也就是消息在日志分段中的对应的物理位置,占用4个字节。
日志查找步骤
- 首先通过二分法在偏移量索引文件中,查找不大于要查找的偏移量的最大偏移量(例如查找offset为7,那么查到的就是6);
- 从偏移量文件中取出offset对应的物理位置信息,去日志分段文件中,进行查找;
- 最后对应的物理位置开始顺序查找,直到找到对应的偏移量的信息。
时间戳索引
时间戳索引:每个索引项占用12字节,分为两个部分。
- timestamp:占用8字节,当前日志分段最大的时间戳。
- relativeOffset:占用4字节,时间戳所对应的消息的相对偏移量。
在时间戳索引中,每次增加的新的索引项所对应的timestamp必须大于之前的索引项的timestamp,否则是不合法的。timestamp对应有两个类型,分别是LogAppendTime和CreateTime,如果是LogAppendTime,那么在时间戳索引中,时间一定是单调递增的,如果是createTime,则无法保证,因为生成者Producer,产生的消息,可以指定对应的时间戳值,并且多个生产者的时区不同,也会导致这个问题。
日志查找步骤:
- 根据给定的时间戳找到不大于不大于该时间戳的最大偏移量。
- 根据查到的偏移量,去偏移量索引中,查到不大于该值的最大偏移量所对应的物理文件位置。
- 从指定的物理文件位置出顺序查找到对应的消息。
日志清理
kafka中提供了两种策略进行日志清理,通过配置项log.cleanup.policy来进行设置,默认值为“delete”,可以设置delete和compact,如果设置为compact,需要配置设置项log.cleaner,enable=true。
- 日志删除(Log Retention):按照一定的保留策略直接删除不符合要求的日志数据。
- 日志压缩(Log Compaction):按照每个消息的key进行处理,只保留相同key的最后一个版本消息,类似redis的RDB。
日志删除
kafka中有一个专门的日志删除任务来周期性的删除不符合要求的日志分段文件,这个周期时间可以通过配置log.retention.check.interval.ms来配置,默认为300000ms;对于日志删除,kafka中有三种方式进行日志删除。
- 基于时间策略:删除任务会检查日志文件中的保留时间是否超过设定的阈值(retentionMs)来进行日志的删除。阈值设置可以通过配置项log.retention.hours、log.retention.minutes、log.retention.ms来设置,设置的优先级为log.retention.ms > log.retention.minutes > log.retention.hours;kafka的默认配置为 log.retention.hours = 168,即保存日志7天。
- 基于日志大小策略:日志删除任务会检查当前日志的大小是否超过设定的阈值(retentionSize),其配置项为log.retention.bytes,-1表示无穷大,并且这个配置时总的log大小,而不是单个logSegment的大小;单个logSegment的大小由配置项log.segment.bytes决定,默认值为1GB
- 基于日志起始偏移量策略:删除策略为,判断某个日志分段的下一个日志分段的起始偏移量baseOffset是否小于等于logStartOffset,若满足,则可以删除该日志分段。
日志压缩
在kafka中会有日志清理线程,通过log.cleaner.thread参数设置线程数量,默认为1,来执行清理任务。在kafka中有一个专门用来存放key与offset对应关系的的哈希表(SkimpyOffsetMap),进行日志压缩时,首先将日志文件中的每条消息的key的哈希值和最后的偏移量offset存入SkimpyOffsetMap中,然后在遍历一边日志文件,将每个key对应的offset的大小与SkimpyOffsetMap中大小进行比较,小于SkimpyOffsetMap的偏移量就进行删除,大于等于这个偏移量就保留。
使用这种方式,当两个不同的key的哈希值相同时,那么其中一个key所对应的消息就会存在丢失情况。
当进行了日志压缩后,原来的大日志文件,就会变的很小,为了避免过多的小文件,kakfa会将多个小文件进行合并,重新生成一个日志分段文件。