Kafka日志清理之LogCompaction

原 Kafka日志清理之Log Compactionhttps://blog.youkuaiyun.com/u013256816/article/details/80487758版权声明:本文为博主原创文章,未经博主朱小厮允许不得转载。 https://blog.youkuaiyun.com/u013256816/article/details/80487758
欢迎支持笔者新作:《深入理解Kafka:核心设计与实践原理》和《RabbitMQ实战指南》,同时欢迎关注笔者的微信公众号:朱小厮的博客。
在上一篇文章《Kafka日志清理之Log Deletion》中介绍了日志清理的方式之一——日志删除,本文承接上篇,主要来介绍Log Compaction。
Kafka中的Log Compaction是指在默认的日志删除(Log Deletion)规则之外提供的一种清理过时数据的方式。如下图所示,Log Compaction对于有相同key的的不同value值,只保留最后一个版本。如果应用只关心key对应的最新value值,可以开启Kafka的日志清理功能,Kafka会定期将相同key的消息进行合并,只保留最新的value值。

(图1)
有很多中文资料会把Log Compaction翻译为“日志压缩”,笔者认为不够妥帖,压缩应该是指Compression,在Kafka中消息可以采用GZip、Snappy、LZ4等压缩方式进行压缩,如果把Log Compaction翻译为日志压缩,容易让人和消息压缩(Message Compression)产生关联,而实则是两个不同的概念。英文“Compaction”可以直译为“压紧、压实”,如果这里将Log Compaction直译为“日志压紧”或者“日志压实”又未免太过生硬。考虑到“日志压缩”的说法已经广为接受,笔者这里勉强接受此种说法,不过文中尽量直接使用英文Log Compaction来表示日志压缩。读者在遇到类似“压缩”的字眼之时需格外注意这个压缩是具体指日志压缩(Log Compaction)还是指消息压缩(Message Compression)。
Log Compaction执行前后,日志分段中的每条消息的偏移量和写入时的保持一致。Log Compaction会生成新的日志分段文件,日志分段中每条消息的物理位置会重新按照新文件来组织。Log Compaction执行过后的偏移量不再是连续的,不过这并不影响日志的查询。
Kafka的Log Compaction可以类比于Redis的SNAPSHOTTING的持久化模式。试想一下,如果一个系统使用Kafka来保存状态,每次有状态变更都会将其写入Kafka中。在某一时刻此系统异常崩溃,进而在恢复时通过读取Kafka中的消息来恢复其应有的状态,那么此系统关心的是它原本的最新状态而不是历史时刻中的每一个状态。如果Kafka的日志保存策略是日志删除(Log Deletion),那么系统势必要一股脑的读取Kafka中的所有数据来恢复,而如果日志保存策略是Log Compaction,那么可以减少数据的加载量进而加快系统的恢复速度。Log Compaction在某些应用场景下可以简化技术栈,提高系统整体的质量。
我们知道可以通过配置log.dir或者log.dirs参数来设置Kafka日志的存放目录,而对于每一个日志目录下都有一个名为“cleaner-offset-checkpoint”的文件,这个文件就是清理检查点文件,用来记录每个主题的每个分区中已清理的偏移量。通过清理检查点文件可以将日志文件(Log)分成两个部分,参考下图,通过检查点cleaner checkpoint来划分出一个已经清理过的clean部分和一个还未清理过的dirty部分。在日志清理的同时,客户端也会读取日志。dirty部分的消息偏移量是逐一递增的,而clean部分的消息偏移量是断续的,如果客户端总能赶上dirty部分,它就能读取到日志的所有消息,反之,就不可能读到全部的消息。

(图2)
上图中firstDirtyOffset(与cleaner checkpoint相等)表示dirty部分的起始偏移量,而firstUncleanableOffset为dirty部分的截止偏移量,整个dirty部分的偏移量范围为[firstDirtyOffset, firstUncleanableOffset),注意这里是左闭右开区间。为了避免当前活跃的日志分段activeSegment成为热点文件,activeSegment不会参与Log Compaction的操作。同时Kafka支持通过参数log.cleaner.min.compaction.lag.ms(默认值为0)来配置消息在被清理前的最小保留时间,默认情况下firstUncleanableOffset等于activeSegment的baseOffset。
注意Log Compaction是针对key的,所以在使用时应注意每个消息的key值不为null。每个broker会启动log.cleaner.thread(默认值为1)个日志清理线程负责执行清理任务,这些线程会选择“污浊率”最高的日志文件进行清理。用cleanBytes表示clean部分的日志占用大小,dirtyBytes表示dirty部分的日志占用大小,那么这个日志的污浊率(dirtyRatio)为:
dirtyRatio = dirtyBytes / (cleanBytes + dirtyBytes)
1
为了防止日志不必要的频繁清理操作,Kafka还使用了参数log.cleaner.min.cleanable.ratio(默认值为0.5)来限定可进行清理操作的最小污浊率。
Kafka中用于保存消费者消费位移的主题“__consumer_offsets”使用的就是Log Compaction策略。
这里我们已经知道了怎样选择合适的日志文件做清理操作,然而我们怎么对日志文件中消息的key进行筛选操作呢?Kafka中的每个日志清理线程会使用一个名为“SkimpyOffsetMap”的对象来构建key与offset的映射关系的哈希表。日志清理需要遍历两次日志文件,第一次遍历把每个key的哈希值和最后出现的offset都保存在SkimpyOffsetMap中,映射模型如下图所示。第二次遍历检查每个消息是否符合保留条件,如果符合就保留下来,否则就会被清理掉。假设一条消息的offset为O1,这条消息的key在SkimpyOffsetMap中所对应的offset为O2,如果O1>=O2即为满足保留条件。

(图3)
默认情况下SkimpyOffsetMap使用MD5来计算key的哈希值,占用空间大小为16B,根据这个哈希值来从SkimpyOffsetMap中找到对应的槽位,如果发生冲突则用线性探测法处理。为了防止哈希冲突过于频繁,我们也可以通过broker端参数log.cleaner.io.buffer.load.factor(默认值为0.9)来调整负载因子。偏移量占用空间大小为8B,故一个映射项占用大小为24B。每个日志清理线程的SkimpyOffsetMap的内存占用大小为log.cleaner.dedupe.buffer.size / log.cleaner.thread,默认值为 = 128MB/1 = 128MB。所以默认情况下SkimpyOffsetMap可以保存128MB * 0.9 /24B ≈ 5033164个key的记录。假设每条消息的大小为1KB,那么这个SkimpyOffsetMap可以用来映射4.8GB的日志文件,而如果有重复的key,那么这个数值还会增大,整体上来说SkimpyOffsetMap极大的节省了内存空间且非常高效。
“SkimpyOffsetMap”这个取名也很有意思,“Skimpy”可以直译为“不足的”,可以看出它最初的设计者也认为这种实现不够严谨。如果遇到两个不同的key但哈希值相同的情况,那么其中一个key所对应的消息就会丢失。虽然说MD5这类摘要算法的冲突概率非常小,但根据墨菲定律,任何一个事件,只要具有大于0的几率,就不能假设它不会发生,所以在使用Log Compaction策略时要注意这一点。
Log Compaction会为我们保留key相应的最新value值,那么当我们需要删除一个key怎么办?Kafka中提供了一个墓碑消息(tombstone)的概念,如果一条消息的key不为null,但是其value为null,那么此消息就是墓碑消息。日志清理线程发现墓碑消息时会先进行常规的清理,并保留墓碑消息一段时间。墓碑消息的保留条件是当前墓碑消息所在的日志分段的最近修改时间lastModifiedTime大于deleteHorizonMs,参考图2,这个deleteHorizonMs的计算方式为clean部分中最后一个日志分段的最近修改时间减去保留阈值deleteRetionMs(通过broker端参数log.cleaner.delete.retention.ms配置,默认值为86400000,即24小时)的大小,即:
deleteHorizonMs = clean部分中最后一个LogSegment的lastModifiedTime - deleteRetionMs
1
所以墓碑消息的保留条件为:
所在LogSegment的lastModifiedTime > deleteHorizonMs 
=> 所在LogSegment的lastModifiedTime > clean部分中最后一个LogSegment的lastModifiedTime - deleteRetionMs
=> 所在LogSegment的lastModifiedTime + deleteRetionMs > clean部分中最后一个LogSegment的lastModifiedTime
(可以对照图2中的deleteRetionMs所标记的位置去理解)
1
2
3
4
Log Compaction执行过后的日志分段的大小会比原先的日志分段的要小,为了防止出现太多的小文件,Kafka在实际清理过程中并不对单个的日志分段进行单独清理,而是会将日志文件中offset从0至firstUncleanableOffset的所有日志分段进行分组,每个日志分段只属于一组,分组策略为:按照日志分段的顺序遍历,每组中日志分段的占用空间大小之和不超过segmentSize(可以通过broker端参数log.segments.bytes设置,默认值为1GB),且对应的索引文件占用大小之和不超过maxIndexSize(可以通过broker端参数log.index.interval.bytes设置,默认值为10MB)。同一个组的多个日志分段清理过后,只会生成一个新的日志分段。

(图4)
参考上图,假设所有的参数配置都为默认值,在Log Compaction之前checkpoint的初始值为0。执行第一次Log Compaction之后,每个非活跃的日志分段的大小都有所缩减,checkpoint的值也有所变化。执行第二次Log Compaction时会将组队成[0.4GB, 0.4GB]、[0.3GB, 0.7GB]、[0.3GB]、[1GB]这4个分组,并且从第二次Log Compaction开始还会涉及墓碑消息的清除。同理,第三次Log Compaction过后的情形可参考上图尾部。Log Compaction过程中会将对每个日志分组中需要保留的消息拷贝到一个以“.clean”为后缀的临时文件中,此临时文件以当前日志分组中第一个日志分段的文件名命名,例如:00000000000000000000.log.clean。Log Compaction过后将“.clean”的文件修改为以“.swap”后缀的文件,例如:00000000000000000000.log.swap,然后删除掉原本的日志文件,最后才把文件的“.swap”后缀去掉,整个过程中的索引文件的变换也是如此,至此一个完整Log Compaction操作才算完成。
以上是整个日志压缩(Log Compaction)过程的详解,读者需要注意将日志压缩和日志删除区分开,日志删除是指清除整个日志分段,而日志压缩是针对相同key的消息的合并清理。
欢迎支持笔者新作:《深入理解Kafka:核心设计与实践原理》和《RabbitMQ实战指南》,同时欢迎关注笔者的微信公众号:朱小厮的博客。

<think>我们正在讨论Kafka日志配置的详细解析。根据之前的对话历史和引用内容,用户已经了解了Kafka日志存储时间设置,现在想深入了解日志配置的详细解析。 引用[1]提到了Kafka的动态配置,引用[2]和[3]则涉及日志清理机制和日志存储结构。因此,我们可以结合这些引用,从以下几个方面展开: 1. Kafka日志的基本存储结构(分区、副本、日志段) 2. 日志清理策略(包括基于时间、基于大小和压缩) 3. 动态配置日志参数的方法(无需重启) 另外,用户之前的问题中已经涉及了存储时间,所以这里需要更全面地讲解日志配置,包括存储结构、清理策略、配置方法等。 根据引用[3]:Kafka日志存储是以主题分区为单位的,每个分区由多个日志段(LogSegment)组成,每个日志段包含.log(消息)、.index(位移索引)和.timeindex(时间戳索引)文件。这种分段机制便于管理。 根据引用[2]:Kafka日志清理有两种策略:删除(delete)和压缩(compact)。删除策略又分为基于时间(log.retention.hours等)和基于大小(log.retention.bytes)两种。清理过程通过后台线程定期执行。 根据引用[1]:Kafka支持动态配置,因此日志相关的参数(如保留时间)可以在运行时修改,无需重启Broker。 因此,回答结构如下: 1. 日志存储结构 2. 日志清理策略(删除:时间/大小;压缩) 3. 日志清理机制(源码层面简要提及,如clean()方法) 4. 动态配置方法(使用kafka-configs.sh) 最后,生成相关问题。 注意:用户上一次的问题和回答中已经详细说明了基于时间的清理,所以这里需要补充其他清理策略和动态配置。 回答格式要求:使用LaTeX格式的数学表达式(如有),但日志配置中可能数学表达式不多,主要用文字描述。但如果有数值示例,可以用行内公式(如$log.retention.bytes=1GB$)。独立公式单独成段。 另外,在引用处添加引用标识,如[^1], [^2], [^3]。 </think>### Kafka日志配置详细解析 Kafka日志配置是其核心存储机制的关键,决定了消息的存储方式、生命周期和资源利用率。以下从存储结构、清理策略和动态配置三方面详细解析: #### 1. **日志存储结构** Kafka采用**分段存储机制**管理日志文件,每个主题分区对应独立的物理存储单元: - **基本单元**:日志段(LogSegment)是基础存储单位,当单个日志文件达到阈值(默认1GB)时自动创建新段[^3] - **文件组成**:每个日志段包含三组文件: - `.log`:存储实际消息二进制数据 - `.index`:位移索引文件(快速定位消息) - `.timeindex`:时间戳索引文件(用于基于时间的清理) - **存储路径**:遵循`<topic>-<partition>`目录结构(如`orders-topic-0/`)[^3] - **分段优势**: - 避免单文件过大(支持水平扩展) - 提升数据删除效率(以段为单位清理) - 加速消息检索(索引文件与日志分离) #### 2. **日志清理策略** Kafka提供两种互补的清理机制,通过`log.cleanup.policy`配置: ##### (1) **删除策略(delete)** 基于时间或空间阈值删除旧数据: - **时间驱动**: - 核心参数:`log.retention.ms`(毫秒级控制,优先级最高) - 备选参数:`log.retention.minutes`/`log.retention.hours` - 执行周期:由`log.retention.check.interval.ms`控制(默认5分钟)[^2] - **空间驱动**: - `log.retention.bytes`:分区最大字节数(如$10 \times 1024^3$=10GB) - `log.segment.bytes`:单个日志段大小(默认$1 \times 1024^3$=1GB) - **删除逻辑**:后台线程扫描日志段,当满足以下任一条件即删除: - 段内所有消息时间戳 <(当前时间 - 保留阈值) - 分区总大小 > `log.retention.bytes` ##### (2) **压缩策略(compact)** 保留每个Key的最新值(适用于状态更新场景): - 触发条件:`log.cleaner.min.compaction.lag.ms`(消息最小保留时间) - 压缩过程: - 创建新的干净段(仅保留Key的最新版本) - 删除旧段(`clean()`方法处理脏数据)[^2] - 适用场景:如用户配置更新、库存状态同步等 #### 3. **动态配置机制** Kafka支持**运行时调整参数**无需重启集群: ```bash # 修改主题级保留时间(示例) bin/kafka-configs.sh --bootstrap-server localhost:9092 \ --entity-type topics --entity-name orders \ --alter --add-config retention.ms=43200000 # 12小时 # 查看当前配置 bin/kafka-configs.sh --describe --topic orders ``` - **覆盖规则**:主题级配置 > Broker级默认配置 - **生效场景**: - 临时扩展存储空间(增大`log.retention.bytes`) - 应对流量高峰(缩短清理周期) - 变更数据保留策略(切换delete/compact)[^1] - **注意事项**:动态修改仅影响新创建的日志段 #### 4. **最佳实践建议** - **监控指标**:关注`LogCleanerManager`指标组,尤其`cleaner-time-ms`和`uncleanable-bytes` - **参数调优**: - 高吞吐场景:增大`log.segment.bytes`降低文件切换开销 - 敏感数据:缩短`log.retention.check.interval.ms`至1分钟 - **灾难防护**:设置`log.retention.bytes`时预留20%磁盘缓冲 > Kafka日志管理是吞吐量和存储效率的平衡艺术。理解分段存储与清理策略的协同,配合动态配置能力,可构建高可靠的消息系统[^1][^2][^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值