Kafka 进阶学习(八)—— 日志清理

1,335 阅读7分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第7天,点击查看活动详情

前言

今天是我 Kafka 学习的第七天,今天学习的内容是日志清理策略,包括日志删除和日志压缩。

Kafka 将消息存储在磁盘中,为了控制磁盘占用空间的不断增加就需要对消息做一定的清理操作。Kafka 中每一个分区副本都对应一个 Log,而 Log 又可以分为多个日志分段,这样也便于日志的清理操作。

下面让我们一起进入今天的学习。

清理策略

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

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

清理策略配置为 broker 端参数 log.cleanup.policy,共三种设置方式,即:

  • delete:即采用日志删除的清理策略,该设置为默认设置;
  • compact:采用日志压缩的清理策略(还需要将 log.cleaner.enable(默认值为 true)设定为 true。);
  • delete,compact:同时支持日志删除和日志压缩两种策略;

日志清理的粒度可以控制到主题级别。

日志删除

在 Kafka 的日志管理器中会有一个专门的日志删除任务来周期性地检测和删除不符合保留条件的日志分段文件,这个周期可以通过 broker 端参数 log.retention.check.interval.ms 来配置,默认值为 300000,即 5 分钟。当前日志分段的保留策略有 3 种:基于时间的保留策略基于日志大小的保留策略 和 基于日志起始偏移量的保留策略

基于时间

日志删除任务会检查当前日志文件中是否有保留时间超过设定的阈值(retentionMs)的日志分段文件集合,retentionMs 可以通过 broker 端参数 log.retention.hours、log.retention.minutes 和 log.retention.ms 来配置,其中 log.retention.ms 的优先级最高,log.retention.minutes 次之,log.retention.hours 最低。默认情况下只配置了 log.retention.hours 参数,其值为 168,故默认情况下日志分段文件的保留时间为 7 天。

日志查找是根据 日志分段中最大的时间戳 largestTimeStamp 来计算。要获取日志分段中的最大时间戳 largestTimeStamp 的值,首先要查询该日志分段所对应的时间戳索引文件,查找时间戳索引文件中最后一条索引项,若最后一条索引项的时间戳字段值大于 0,则取其值,否则设置为最近修改时间 lastModifiedTime。

若待删除的日志分段的总数等于该日志文件中所有的日志分段的数量,那么说明所有的日志分段都已过期,但该日志文件中还要有一个日志分段用于接收消息的写入,即必须要保证有一个活跃的日志分段 activeSegment,在此种情况下,会先切分出一个新的日志分段作为 activeSegment,然后执行删除操作。

删除日志分段时,首先会从 Log 对象中所维护日志分段的跳跃表中移除待删除的日志分段,以保证没有线程对这些日志分段进行读取操作。然后将日志分段所对应的所有文件添加上 .deleted 的后缀(当然也包括对应的索引文件)。最后交由一个以“delete-file”命名的延迟任务来删除这些以 .deleted为后缀的文件,这个任务的延迟执行时间可以通过 file.delete.delay.ms 参数来调配,此参数的默认值为 60000,即 1 分钟。

基于日志大小

日志删除任务会检查当前日志的大小是否超过设定的阈值(retentionSize)来寻找可删除的日志分段的文件集合(deletableSegments),retentionSize 可以通过 broker 端参数 log.retention.bytes 来配置,默认值为 -1,表示无穷大。注意 log.retention.bytes 配置的是 Log 中所有日志文件的总大小,而不是单个日志分段(确切地说应该为 .log 日志文件)的大小。单个日志分段的大小由 broker 端参数 log.segment.bytes 来限制,默认值为 1073741824,即 1GB。

基于日志大小的保留策略与基于时间的保留策略类似,首先计算日志文件的总大小 size 和 retentionSize 的差值 diff,即计算需要删除的日志总大小,然后从日志文件中的第一个日志分段开始进行查找可删除的日志分段的文件集合 deletableSegments。查找出 deletableSegments 之后就执行删除操作,这个删除操作和基于时间的保留策略的删除操作相同。

基于日志起始偏移量

基于日志起始偏移量的保留策略的判断依据是某日志分段的下一个日志分段的起始偏移量 baseOffset 是否小于等于 logStartOffset,若是,则可以删除此日志分段。

日志的查找动作为从头开始遍历每个日志分段,直到找到所有的可删除的文件集合;删除操作同其他两个策略;

日志压缩

Kafka 中的 Log Compaction 是指在默认的日志删除(Log Retention)规则之外提供的一种清理过时数据的方式。Log Compaction 对于有相同 key 的不同 value 值,只保留最后一个版本。如果应用只关心 key 对应的最新 value 值,则可以开启 Kafka 的日志清理功能, Kafka 会定期将相同 key 的消息进行合并,只保留最新的 value 值。

Log Compaction 执行前后,日志分段中的每条消息的偏移量和写入时的偏移量保持一致。Log Compaction 会生成新的日志分段文件,日志分段中每条消息的物理位置会重新按照新文件来组织。Log Compaction 执行过后的偏移量不再是连续的,不过这并不影响日志的查询。

注意 Log Compaction 是针对 key 的,所以在使用时应注意每个消息的 key 值不为 null。

每个 broker 会启动 log.cleaner.thread(默认值为 1)个日志清理线程负责执行清理任务,这些线程会选择“污浊率”最高的日志文件进行清理。计算方式为:污浊率=清理量/(清理量+污浊量)

为了防止日志不必要的频繁清理操作,Kafka 还使用了参数 log.cleaner.min.cleanable.ratio(默认值为 0.5)来限定可进行清理操作的最小污浊率。

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

默认情况下,SkimpyOffsetMap 使用 MD5 来计算 key 的哈希值,占用空间大小为 16B,根据这个哈希值来从 SkimpyOffsetMap 中找到对应的槽位,如果发生冲突则用 线性探测法 处理。

参考文档

  • 《深入理解 Kafka:核心设计与实践原理》—— 朱忠华

往期文章