日志索引
每个日志分段文件对应了两个索引文件,主要用来提高查找消息的效率。偏移量索引文件用来建立消息偏移量( offset )到物理地址之间的映射关系,方便快速定位消息所在的物理文件位置;时间戳索引文件则根据指定的时间戳( timestamp )来查找对应的偏移量信息。
索引文件以稀疏索引( sparse index )的方式构造消息的索引,它并不保证每个消息在索引文件中都有对应的索引项,每当写入一定量(log.index.interval.bytes
指定,默认值为 4096 ,即 4KB )的消息时,偏移量索引文件和时间戳索引文件分别增加一个偏移量索引项和时间戳索引项,增大或减小 log.index.interval.bytes
的值,对应地可以增加或缩小索引项的密度。
稀疏索引通过MappedByteBuffer将索引文件映射到内存中,以加快索引的查询速度。偏移量索引文件中的偏移量是单调递增的。查询指定偏移量时:1、使用二分查找法来快速定位偏移量的位置,2、如果指定的偏移量不在索引文件中,则会返回小于指定偏移量的最大偏移量。时间戳索引文件中的时间戳也保持严格的单调递增,查询指定时间戳时,也根据二分查找法来查找不大于该时间戳的最大偏移量,至于要找到对应的物理文件位置还需要根据偏移量索引文件来进行再次定位。稀疏索引的方式是在磁盘空间、内存空间、查找时间等多方面之间的一个折中。
日志分段文件达到一定的条件时需要进行切分,那么其对应的索引文件也需要进行切分。日志分段文件切分条件,满足以下其一即可:
- 当前日志分段文件的大小超过了 broker 端参数
log.segment.bytes
的值。log.segment.bytes
参数的默认值为 1073741824 ,即 lGB。 - 当前日志分段中消息的最大时间戳与当前系统的时间戳的差值大于
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)。
偏移量索引
偏移量索引项的格式如图所示。每个索引项占用8个字节,分为两个部分。
- relativeOffset:相对偏移量,表示消息相对于baseOffset的偏移量,占用4字节,当前索引文件的文件名即为baseOffset。
- position:物理地址,也就是消息在日志分段文件中对应的物理位置,占用4个字节。
消息的偏移量( offset )占用8个字节,也可以称为绝对偏移量。
偏移量索引示意图
如果我们要查找偏移量为233的消息,那么应该怎么做呢? 首先通过二分法在偏移量索引文件中找到不大于 23最大索引项,即【22,656】,然后从日志分段文件中的物理位置656开始顺序查找偏移量23的消息.
以上是最简单的一种情况。参考下图如果要查找偏移 268 的消息,那么应该怎么办呢?首先肯定是定位到 baseOffset为251的日志分段,然后计算相对偏移量 relativeOffset = 268 - 251=17 ,之后再在对应的索引文件中找到不大于17的索引项,最后根据索引项中的 position定位到具体的日志分段文件位置开始查找目标消息。那么又是如何查找 baseOffset 251的日志分段的呢?这里并不是顺序查找,而是用了跳跃表的结构 Kafka 的每个日志对象中使用了ConcurrentSkipListMap来保存各个日志分段,每个日志分段的 baseOffset 作为 key ,这样可以根据指定偏移量来快速定位到消息所在的日志分段。
时间戳索引
时间戳索引格式图
每个索引项占用12个字节,分为两个部分。
- timestamp:当前日志分段最大的时间戳。
- relativeOffset:时间戳所对应的消息的相对偏移量。
时间戳索引文件中包含若干时间戳索引项,每个追加的时间戳索引项中的timestamp
必须大于之前追加的索引项的 timestamp
,否则不予追加,如果broker端参数 log.message.timestamp.type
设置为 LogAppendTime ,那么消息的时间戳必定能够保持单调递增;相反,如果是CreateTime类型则无法保证。生产者可以使用类似
ProducerRecord(String topic, Integer partition, Long timestamp, K key, V value)
的方法来指定时间戳的值,即使生产者客户端采用自动插入的时间戳也无法保证时间戳能够单调递增,如果两个不同时钟的生产者同时往一个分区中插入消息,那么也会造成当前分区的时间戳乱序。
我们己经知道每当写入一定量的消息时, 就会在偏移量索引文件和时间戳索引文件中分别增加一个偏移量索引项和时间戳索引项。两个文件增加索引项的操作是同时进行的,但并不意味着偏移量索引中的relativeOffset 和时间戳索引项中的relativeOffset是同一个值。
时间戳示意图
如果要查找指定时间戳 targetTimeStamp = 1526384718288开始的消息,首先是找到不小于指定时间戳的日志分段。这里就无法使用跳跃表来快速定位到相应的日志分段 了, 需要分以下几个步骤来完成。
- 将targetTimeStamp和每个日志分段中的最大时间戳largestTimeStamp逐一对比,直到找到不小于 targetTimeStamp的largestTimeStamp所对应的日志分段。日志分段中的largestTimeStamp的计算是先查询该日志分段所对应的时间戳索引文件,找到最后一条索引项,若最后一条索引项的时间戳字段值大于0,则取其值,否则取该日志分段的最近修改时间。
- 找到相应的日志分段之后,在时间戳索引文件中使用二分查找算法查找到不大于targetTimeStamp 最大索引项,即【1526384718283, 28】,如此便找到了一个相对偏移量28。
- 在偏移量索引文件中使用二分算法查找到不大于 28 的最大索引项,即【26, 838】。
- 从步骤1中找到日志分段文件中的 838 的物理位置开始查找不小于targetTimeStamp的消息。