Kafka 的消息存储结构:索引文件与数据文件

Posted by 陈树义 on 2020-11-30

我们都知道 Kafka Server 会接收生产者的消息,那么 Kafka 接收到消息并刷到磁盘之后。消息文件是如何存储的呢?

Kafka 的消息存储结构

Kafka 有 Topic 和 Partition 两个概念,一个 Topic 可以有多个 Partition。在实际存储的时候,Topic + Partition 对应一个文件夹,这个文件夹对应的是这个 Partition 的数据。

在 Kafka 的数据文件目录下,一个 Partition 对应一个唯一的文件夹。如果有 4 个 Topic,每个 Topic 有 5 个 Partition,那么一共会有 4 * 5 = 20 个文件夹。而在文件夹下,Kafka 消息是采用 Segment File 的存储方式进行存储的。

Segment File 的大概意思是:将大文件拆分成小文件来存储,这样一个大文件就变成了一段一段(Segment 段)。这样的好处是 IO 加载速度快,不会有很长的 IO 加载时间。Kafka 的消息存储就采用了这种方式。

image.png

如上图所示,在一个文件夹下的数据会根据 Kafka 的配置拆分成多个小文件。拆分规则可以根据文件大小拆分,也可以根据消息条数拆分,这个是 Kafka 的一个配置,这里不细说。

在 Kafka 的数据文件夹下,分为两种类型的文件:索引文件(Index File)和数据文件(Data File)。索引文件存的是消息的索引信息,帮助快速定位到某条消息。数据文件存储的是具体的消息内容。

索引文件

索引文件的命名统一为数字格式,其名称表示 Kafka 消息的偏移量。我们假设索引文件的数字为 N,那么就代表该索引文件存储的第一条 Kafka 消息的偏移量为 N + 1,而上个文件存储的最后一条 Kafka 消息的偏移量为 N(因为 Kafka 是顺序存储的)。例如下图的 368769.index 索引文件,其表示文件存储的第一条 Kafka 消息的偏移量为 368770。而 368769 表示的是 0000.index 这个索引文件的最后一条消息。所以 368769.index 索引文件,其存储的 Kafka 消息偏移量范围为 368769-737337。

Kafka消息的索引文件

索引文件存储的是简单地索引数据,其格式为:「N,Position」。其中 N 表示索引文件里的第几条消息,而 Position 则表示该条消息在数据文件(Log File)中的物理偏移地址。例如下图中的「3,497」表示:索引文件里的第 3 条消息(即 offset 368772 的消息,368772 = 368769+3),其在数据文件中的物理偏移地址为 497。

Kafka消息的索引文件

其他的以此类推,例如:「8,1686」表示 offset 为 368777 的 Kafka 消息,其在数据文件中的物理偏移地址为 1686。

数据文件

数据文件的命名格式与索引文件的命名格式完全一样,这里就不再赘述了。

通过上面索引文件的分析,我们已经可以根据 offset 快速定位到某个数据文件了。那接着我们怎么读取到这条消息的内容呢?要读取到这条消息的内容,我们需要搞清楚数据文件的存储格式。

数据文件就是所有消息的一个列表,而每条消息都有一个固定的格式,如下图所示。

Kafka消息的数据文件

从上图可以看到 Kafka 消息的物理结构,其包含了 Kafka 消息的 offset 信息、Kafka 消息的大小信息、版本号等等。有了这些信息之后,我们就可以正确地读取到 Kafka 消息的实际内容。

实战:如何查找 message

前面我们分析了 Kafka 的整套文件存储机制,也讲了如何定位、读取到 Kafka 消息的内容。那么我们现在就来模拟一下如何根据 offset 寻找到 Kafka 消息内容。

例如我们要读取 Topic 为 Order、Partition 为 1,并且 offset 为 368775 的 Kafka 消息内容,我们应该怎么做呢?

假设我们的索引文件如下图所示

Kafka消息的索引文件

定位数据文件夹

首先我们需要定位到具体 Partition 的数据文件夹,直接就是 Kafka 的数据存储目录,然后是以 Topic + Partition 命名的文件夹。

定位索引文件

定位到数据文件夹后,我们可以将所有数据文件的文件名都列出来。

Kafka消息的索引文件

根据之前对索引文件内容的剖析,各个索引文件存储的 offset 范围为:

  • 000000.index -> 000000-368769
  • 368769.index -> 368770-737337
  • 737337.index -> 737338-1105814

我们要寻找的是 offset 为 368775 的消息,那其索引数据就存储在 368769.index 这个索引文件中。我们要读取的消息为 368775,那么这条消息在索引文件中是第 6 条消息。第 6 条在数据文件的物理位置为 1407。

读取消息内容

根据上面的分析,我们知道:offset 为 368775 的消息,其索引数据存储在 368769.index,消息内容存储在 368769.log 文件偏移位置为 1407 的地方。

那接下来结合偏移量以及消息的物理结构,直接读取到 offset 为 368775 的消息内容了。

稀疏索引

从下图的 Kafka 消息索引文件可以看出,索引文件中并不存储每条消息在数据文件的偏移地址。这是为什么呢?

Kafka消息的索引文件

但要理解这个问题,必须明白为什么 Kafka 要这么做。

因为 Kafka 的消息太多了,如果把所有消息的「offset,物理偏移量」信息都存入 index 文件的话,那么这个文件太大了,无法一次性存入内存。而如果无法一次性存入内容,就会导致需要多次去读取,但每次去读磁盘又会降低读取效率。

于是我们的计算机先烈们很天才地相出了一个办法 —— 能不能只存一部分的索引数据?

例如我本来要存储 1 万个消息,但现在我只存 5 千个。就像上图的 Kafka 索引文件一样跳着存储。如果我要寻找第 7 条消息,那我只需要找到第 6 条消息的物理偏移量。之后在读取数据文件的时候直接跳过 1 条消息就可以了。

不得不说,这种方法真的是很有效!减少索引文件的大小,让内存可以全部读取进去。所有索引数据都存在内存中,减少磁盘 IO 的次数,极大地提高查找效率。这种索引存储方法叫:稀疏索引存储方式。

但聪明的朋友们一定会想到一种极端情况,那就是如果我 1 万个消息,减少到了 10 个。那我去读取数据文件的时候,岂不是要遍历跳过很多条无效消息?这样效率岂不是也很低?!

没错!极端情况下确实会出现这样一种问题。

所以稀疏索引需要保证两个很重要的点,一个是索引的密度,一个是索引的均匀程度。

索引的密度。 虽然减少索引的信息可以减少文件大小,但是会导致后续查找的时间成本。所以为了后续的查找方便,你不能减少太多索引信息。例如原本有 1 万个消息 offset 索引,你现在减到了 100 个,那就太少了。

索引的均匀程度。 还是 1 万个消息的 offset 索引为例,如果你弄了 8000 个索引。这时候索引密度应该是没问题的。但是这 8000 个索引全都是第 1 - 8000 条消息的索引,最后一个索引是 8000 - 1 万的。那么当要读取的 offset 坐落于这个区间时,读取速度也会很慢。所以索引的分布也要均匀!

索引的多少与密度肯定有一个最佳平衡点,而我相信 Kafka 应该找到了这样一个平衡点。关于这块的内容,有机会再研究研究和大家分享。

参考资料