我们都知道 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 的消息存储就采用了这种方式。
如上图所示,在一个文件夹下的数据会根据 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。
索引文件存储的是简单地索引数据,其格式为:「N,Position」。其中 N 表示索引文件里的第几条消息,而 Position 则表示该条消息在数据文件(Log File)中的物理偏移地址。例如下图中的「3,497」表示:索引文件里的第 3 条消息(即 offset 368772 的消息,368772 = 368769+3),其在数据文件中的物理偏移地址为 497。
其他的以此类推,例如:「8,1686」表示 offset 为 368777 的 Kafka 消息,其在数据文件中的物理偏移地址为 1686。
数据文件
数据文件的命名格式与索引文件的命名格式完全一样,这里就不再赘述了。
通过上面索引文件的分析,我们已经可以根据 offset 快速定位到某个数据文件了。那接着我们怎么读取到这条消息的内容呢?要读取到这条消息的内容,我们需要搞清楚数据文件的存储格式。
数据文件就是所有消息的一个列表,而每条消息都有一个固定的格式,如下图所示。
从上图可以看到 Kafka 消息的物理结构,其包含了 Kafka 消息的 offset 信息、Kafka 消息的大小信息、版本号等等。有了这些信息之后,我们就可以正确地读取到 Kafka 消息的实际内容。
实战:如何查找 message
前面我们分析了 Kafka 的整套文件存储机制,也讲了如何定位、读取到 Kafka 消息的内容。那么我们现在就来模拟一下如何根据 offset 寻找到 Kafka 消息内容。
例如我们要读取 Topic 为 Order、Partition 为 1,并且 offset 为 368775 的 Kafka 消息内容,我们应该怎么做呢?
假设我们的索引文件如下图所示
定位数据文件夹
首先我们需要定位到具体 Partition 的数据文件夹,直接就是 Kafka 的数据存储目录,然后是以 Topic + Partition 命名的文件夹。
定位索引文件
定位到数据文件夹后,我们可以将所有数据文件的文件名都列出来。
根据之前对索引文件内容的剖析,各个索引文件存储的 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 的消息太多了,如果把所有消息的「offset,物理偏移量」信息都存入 index 文件的话,那么这个文件太大了,无法一次性存入内存。而如果无法一次性存入内容,就会导致需要多次去读取,但每次去读磁盘又会降低读取效率。
于是我们的计算机先烈们很天才地相出了一个办法 —— 能不能只存一部分的索引数据?
例如我本来要存储 1 万个消息,但现在我只存 5 千个。就像上图的 Kafka 索引文件一样跳着存储。如果我要寻找第 7 条消息,那我只需要找到第 6 条消息的物理偏移量。之后在读取数据文件的时候直接跳过 1 条消息就可以了。
不得不说,这种方法真的是很有效!减少索引文件的大小,让内存可以全部读取进去。所有索引数据都存在内存中,减少磁盘 IO 的次数,极大地提高查找效率。这种索引存储方法叫:稀疏索引存储方式。
但聪明的朋友们一定会想到一种极端情况,那就是如果我 1 万个消息,减少到了 10 个。那我去读取数据文件的时候,岂不是要遍历跳过很多条无效消息?这样效率岂不是也很低?!
没错!极端情况下确实会出现这样一种问题。
所以稀疏索引需要保证两个很重要的点,一个是索引的密度,一个是索引的均匀程度。
索引的密度。 虽然减少索引的信息可以减少文件大小,但是会导致后续查找的时间成本。所以为了后续的查找方便,你不能减少太多索引信息。例如原本有 1 万个消息 offset 索引,你现在减到了 100 个,那就太少了。
索引的均匀程度。 还是 1 万个消息的 offset 索引为例,如果你弄了 8000 个索引。这时候索引密度应该是没问题的。但是这 8000 个索引全都是第 1 - 8000 条消息的索引,最后一个索引是 8000 - 1 万的。那么当要读取的 offset 坐落于这个区间时,读取速度也会很慢。所以索引的分布也要均匀!
索引的多少与密度肯定有一个最佳平衡点,而我相信 Kafka 应该找到了这样一个平衡点。关于这块的内容,有机会再研究研究和大家分享。