深入Kafka - 日志存储

感情迁移 提交于 2020-02-26 02:03:59

Kafka中的消息是以主题为基本单位进行归类的,各个主题在逻辑上相互独立。每个主题又可以分为一个或多个分区。

不考虑多副本的情况,每个分区对应一个日志(Log),每个日志包含多个日志分段(LogSegment),对应到物理存储,可以理解为Log对应日志一个目录,每个LogSegment对应一个日志文件和两个索引文件,以及可能的其他可能文件(比如事务索引文件)。

举例说明,假设有名为topic-log的主题,此主题有4个分区,那么在实际物理存储上表现如下图。
在这里插入图片描述

1. __consumer_offsets-*

如图中绿色标记部分,用来保存客户端消费消息之后提交的消费位移,以便在机器重启或者宕机恢复之后正确得知客户端消费消息的位置。(不是本章重点,不再赘述)

2. Log目录

图中蓝色标记部分,可以理解为每个分区对应一个目录。本例中topic-log主题包含四个分区,所以有"topic-log-0",“topic-log-1”,“topic-log-2”,"topic-log-3"这4 个文件夹。

3. 检查点文件

与Log目录平级的,存在4个检查点文件,用于日志的compaction。
cleaner-offset-checkpoint文件是清理检查点文件,记录每个主题的每个分区中已经清理的偏移量,如下图(下文介绍日志压缩时会详细阐述)
在这里插入图片描述

4. 日志分段

日志分段是Log目录下的小文件,主要包括日志文件(.log)、偏移量索引文件(.index)、时间戳索引文件(.timeindex)。日志分段以baseOffset加后缀的方式命名,日志分段的作用将在下文日志索引详细阐述。

5. 日志索引

每个日志分段文件对应了两个索引文件,主要用来提高查找消息的效率。偏移量索引文件用来建立消息偏移量(offset )到物理地址之间的映射关系,方便快速定位消息所在的物理文件位置;时间戳索引文件则根据指定的时间戳(timestamp)来查找对应的偏移量信息。

5.1 偏移量索引文件

偏移量索引文件的格式如下图红色框注,每条记录包括relativeOffset和position两部分,各占4字节,分别表示相对偏移量和物理位置,满足baseOffset + relativeOffset = 真实偏移量。例如,一个日志分段的baseOffset为32,那么其文件名就是00000000000000000032.log,offset为35的消息在索引文件中的relativeOffset的值为35-32=3。
在这里插入图片描述我们知道偏移量索引文件的存在是为了提高查找效率,那么kafka具体是如何实现的呢?

其实,Kafka的每个日志对象中用ConcurrentSkipListMap(上图)的结构保存着各个日志分段,该结构使用跳表1保存各日志文件的baseOffset,这样查找baseOffset的时间复杂度便是O(logn);使用Map保存baseOffset到日志索引文件的映射,时间复杂度O(1)。如此便可快速定位的日志索引文件。

举个例子:
	当我们要查找一个偏移量为268的消息时
	首先,通过跳表找到第一个不大于268的baseOffset,定位的251的日志分段;
	其次,计算相对偏移量relativeOffset = 268-251=17;
	第三,再在对应的偏移量索引文件中找到第一个不大于17的relativeOffset,此处是14;
	最后,根据14对应的position(459)定位到具体日志分段文件位置开始顺序查找目标消息。
5.2 时间戳索引文件

时间戳索引文件的格式如下图红色框注,每条记录包括timestamp和taltiveOffset两部分,分别表示当前日志分段的最大时间戳(占8字节)和时间戳对应的相对偏移量(占4字节)。时间戳索引文件中的时间戳保持单调递增,追加的时间戳索引项中tiemstamp必须大于之前的tiemstamp,否则不予追加。
在这里插入图片描述
我们己经知道每当写入一定量的消息时,就会在偏移量索引文件和时间戳索引文件中分别增加一个偏移量索引项和时间戳索引项。两个文件增加索引项的操作是同时进行的,但并不意味着偏移量索引中的relativeOffset和时间戳索引项中的relativeOffset是同一个值。

当我们查找指定时间开始的消息时,便会用到时间戳索引文件。那么,具体查找过程是怎样的呢?

举个例子:
	查找target=1526384728288开始的消息。
	首先,将target和每个日志分段的最大时间戳(计算方式见下文)逐一对比,找到第一个不小于target的日志分段;
	其次,在该日志分段对应的时间戳索引文件中二分查找不大于target的最大索引项,此处找到[1526384718283,28];
	第三,在偏移量索引文件中二分查找不大于28的最大索引项,此处找到[28,838];
	最后,在日志分段文件中的838位置开始查找不小于target的消息。

最大时间戳计算方式

每个日志分段的最大时间戳计算方式:
	从日志分段对应的时间戳索引文件取最后一条记录(因为时间戳单调递增),如果该记录中timestamp大于0,则其值便是最大时间戳;
	否则取该日志分段的最后修改时间,作为最大时间戳。

6. 日志清理

Kafka 提供了两种日志清理策略。

  1. 日志删除(LogRetention):按照一定的保留策略直接删除不符合条件的日志分段。
  2. 日志压缩(LogCompaction):针对每个消息的key进行整合,对于有相同key的不同value值,只保留最后一个版本。
6.1 日志删除

日志删除分为基于时间删除、基于日志大小删除和基于日志起始偏移量删除三种情况,比较简单,不再赘述。这里只简单介绍删除的步骤:

首先会从Log 对象中所维护日志分段的跳跃表(上文提到的ConcurrentSkipListMap)中移除待删除的日志分段,以保证没有线程对这些日志分段进行读取操作。然后将日志分段所对应的所有文件添加上".deleted"的后缀(当然也包括对应的索引文件)。最后交由一个以"delete-file"命名的延迟任务来删除这些以".deleted"为后缀的文件。
6.2 日志压缩

上文中提到 清理检查点文件,日志压缩时便是根据该文件计算污浊率,污浊率=未清理/(已清理+未清理)。

选择要清理的日志文件

broker会启动log.cleaner.thread(默认值为I )个日志清理线程负责执行清理任务,这些线程会选择“污浊率”最高的日志文件进行清理。为了防止日志不必要的频繁清理操作,Kafka还使用了参数log.cleaner.min.cleanable.ratio(默认值为0. 5 )来限定可进行清理操作的最小污浊率。

清理

Kafka中的每个日志清理线程会使用一个名为“SkimpyOffsetMap”的对象来构建key与offset的映射关系的哈希表。日志清理需要遍历两次日志文件,第一次遍历把每个key的哈希值和最后出现的offset都保存在SkimpyOffsetMap中;第二次遍历会检查每个消息是否符合保留条件,如果符合就保留下来,否则就会被清理。

7. 一次完整的LogCompaction

假设所有的参数配置都为默认值,在LogCompaction之前checkpoint 的初始值为0。执行第一次LogCompaction之后,每个非活跃的日志分段的大小都有所缩减,checkpoint的值也有所变化。执行第二次LogCompaction时会组队成[0.4GB,0.4GB]、[0.3GB,0.7GB]、[0.3GB、1GB]这4个分组,并且从第二次LogCompaction开始还会涉及墓碑消息的清除。同理,第三次LogCompaction过后的情形可参考图尾部。LogCompaction过程中会将每个日志分组中需要保留的消息复制到一个以".clean"为后缀的临时文件中,此临时文件以当前日志分组中第一个日志分段的文件名命名LogCompaction例如0000000000000000000.log.clean。LogCompaction过后将".clean"的文件修改为".swap"后缀的文件,例如:0 0 0000000000000000000.log.swap。然后删除原本的日志文件,最后才把文件的".swap"后缀去掉。整个过程中的索引文件的变换也是如此,至此一个完整LogCompaction操作才算完成。
在这里插入图片描述


  1. 跳表 – 参考《redis源码分析 - 内部数据结构》 ↩︎

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!