在本文中,我将描述Kafka中日志压缩数据结构。然后,我会向大家展示Kafka如何在内部将这些topic的状态保存在文件系统中。
预备知识
我假设大家已经熟悉Apache Kafka的基本概念,比如代理(broker)、主题(topic)、分区(partition)、消费者(consumer)和生产者(producer)。此外,如果想运行示例命令,还必须运行Kafka broker和Zookeeper服务器。
什么是日志压缩主题
kafka官网上:
Log compaction is a mechanism to give finer-grained per-record retention, rather than the coarser-grained time-based retention. The idea is to selectively remove records where we have a more recent update with the same primary key. This way the log is guaranteed to have at least the last state for each key.
简言之,Kafka可以根据日志的时间或大小删除旧的记录。Kafka还支持记录键压缩的日志压缩。日志压缩意味着Kafka将保留记录的最新版本,并在日志压缩期间删除旧版本。压缩日志对于在崩溃或系统故障后恢复状态非常有用。它们在内存服务、持久化数据存储、重新加载缓存等方面非常有用。
Kafka日志压缩结构
压缩的日志只有日志有头(head)和尾(tail)。压缩日志头与传统的Kafka日志相同。新记录被追加到头的末尾。并且文件头是可以循环使用的。
![4e71ebd54b44f15dc3b95267d80a16f5.png](https://i-blog.csdnimg.cn/blog_migrate/655f39a31c3725811212228ce9a09fc1.jpeg)
案例
下面我们用一个小案例来阐述Log Compaction的作用。
![d1ec8cdeee35528fdc8a405102a9bb69.png](https://i-blog.csdnimg.cn/blog_migrate/b842e93d8fe63add3b9da7788aba2707.jpeg)
我们看到有两条记录使用了key p3。但是因为它是一个日志压缩的topic,Kafka在后台线程中删除了旧的记录(关于这一点的更多信息将在下一节中介绍)。现在假设有一个生产者向这个分区发送新记录。音乐制作人制作了3张唱片,key分别为p6,p5,p5:
![5a1be94c10587b03c2734573e2ccbab0.png](https://i-blog.csdnimg.cn/blog_migrate/2e63723e6e370224d2d398ed4b52c2dc.jpeg)
同样,Kafka broker中的后台线程删除了带有p5和p6 key的旧记录。注意,压缩日志由两部分组成尾和头。Kafka确保尾部内的所有记录都有一个唯一的key,因为在清洗过程的前一个循环中扫描尾部。但是head部分可以有重复的值。
现在就来使用kafka-topics工具创建它们。
创建一个压缩的日志主题
创建一个压缩topic
kafka-topics --create --zookeeper zookeeper:2181 --topic latest-product-price --replication-factor 1 --partitions 1 --config "cleanup.policy=compact" --config "delete.retention.ms=100" --config "segment.ms=100" --config "min.cleanable.dirty.ratio=0.01"
生成记录:
kafka-console-producer --broker-list localhost:9092 --topic latest-product-price --property parse.key=true --property key.separator=:>p3:10$>p5:7$>p3:11$>p6:25$>p6:12$>p5:14$>p5:17$
注意,在上面的命令中,我用:分隔key和value。现在使用topic:
kafka-console-consumer --bootstrap-server localhost:9092 --topic latest-product-price --property print.key=true --property key.separator=: --from-beginningp3:11$p6:12$p5:14$p5:17$
我们看到删除了具有重复key的记录。p5:14$记录没有被删除,我们将在描述清洗过程时看到原因。但我们必须首先看看Kafka如何在内部存储消息。
段(segment)
分区日志是一种抽象数据结构,它允许我们轻松地在分区内使用有序的消息,而不用担心Kafka的内部存储。然而,在实际中,分区日志被Kafka broker划分为多个段。段是存储在文件系统(数据目录和分区目录中)中的文件,它们的名称以.log结尾。在下图中,一个分区日志被划分为3段:
![cd1f42edc22dbe23afc74ee6aaf07751.png](https://i-blog.csdnimg.cn/blog_migrate/4247f9ed738e2dc84a72a33cefaf1cd5.jpeg)
可以看到,我们有一个分区日志,它包含3个单独的段文件中的7条记录。段的第一个偏移量称为段的基偏移量。段文件名总是等于它的基偏移量值。
分区中的最后一个段称为活动段。只有log的活动段才能接收新生成的消息。我们将看到Kafka在压缩日志的清理过程中如何处理活动段。
回到我们的示例,我们可以通过以下命令查看主题分区的段文件(假设你的Kafka数据目录是/var/lib/kafka/data):
ls /var/lib/kafka/data/latest-product-price-0/00000000000000000000.index 00000000000000000006.log00000000000000000000.log 00000000000000000006.snapshot00000000000000000000.timeindex 00000000000000000006.timeindex00000000000000000005.snapshot leader-epoch-checkpoint00000000000000000006.index136/5000
00000000000000000000.log和00000000000000000006.log是此分区的段,00000000000000000006.log是活动段。
kafka什么时候创建一个新的段?一个选择是通过设置段。topic创建期间的自动配置(默认为1GB)。当段大小超过这个值时,Kafka将创建一个新的段。另一个选项是通过segment.ms设置段的大小。 使用此选项,当Kafka收到一个generate请求时,它将活动段与segment.ms做比较。如果它是旧的,那么它将创建一个新的段。在我们的命令中,我们设置segment.ms=100,以确保每100毫秒创建一个新段。
有趣的是当你设置segment.ms=100,你可能会有更小的段。清理过程(参见下一节)之后,Kafka broker将合并非活动段并从中创建一个大段。
有关Kafka的段和内部存储的更多信息,读者可以百度、谷歌或者等我下一篇文章来解释。。。。 。
清洗过程
在启动期间,Kafka broker创建了一些cleaner threads,负责清理压缩的日志(这些线程的数量可以通过log.cleaner.threads来设置)。cleaner threads会不断地尝试在broker中找到最脏的日志,然后尝试清理它。对于每个日志,计算脏比如下
dirty ratio = the number of bytes in the head / total number of bytes in the log(tail + head)
然后,cleaner threads选择脏比率最高的日志。这个日志被称为最脏的日志,如果它的值大于min.cleanable.dirty.ratio的设置,它将被清洗。否则,cleaner threads将被阻塞数毫秒(可使用log.cleaner.backoff.ms来设置)。
在找到最脏的日志之后,我们希望找到日志中可清洗的部分。注意,日志的某些部分是不可清理的(为什么呢?提个小问题),不会被扫描:
· 活动段中的所有记录。这就是为什么我们仍然看到重复p5:14的记录在我们的消费者。
· 如果将min.compaction.lag.ms设置为大于0,那么任何具有时间戳小于该配置的记录的段都不会被清除。这些段不会被扫描以进行压缩。
现在我们知道要压缩哪些记录。从日志中的第一条记录到 第一条不可清理的记录。为了简单起见,在本文中,我们假设头部中的所有记录都是可清理的。
注意,我们知道日志尾部的每条记录都有一个惟一的键,因为重复项在上次清理中被删除了。我们只可能在head部分有一些记录,它们的键在日志中不是唯一的。为了更快地找到重复记录,Kafka在head部分为记录创建了一个映射。回到我们的例子,偏移映射(offset map)结构是这样的:
![cd0b91b2e2f5bd257440faccd30797ef.png](https://i-blog.csdnimg.cn/blog_migrate/6acbd14ae321d33aa300018553a5601a.jpeg)
我们看到,Kafka创建了一个名为偏移量映射的结构,对于head部分中的每个key,它都拥有相应的偏移量。如果我们有重复数据在头部,kafaka会使用最新的偏移量。在上图中,使用key p6记录偏移量为5,p5最新偏移量为7。现在clean thread检查日志中的每条记录,如果偏移量映射中有任何具有相同key的记录,并且偏移量与映射中的条目不同(我们不想删除最新的记录),则清除它。
在压缩日志的清理过程中,不仅会删除重复的消息(message),Kafka还会删除值为null的记录。这些记录被称为墓碑(tombstone)。您可以通过设置delete.retention.ms来延迟删除它们。 通过设置该参数,Kafka检查包含该记录的段的修改时间戳,如果修改时间小于配置值,该记录将被保留。
经过这个clean过程,我们有了一个新的log尾和一个新的log头!为清理而扫描的最后偏移量(在我们的示例中是旧头部中的最后一条记录)是新尾部的最后偏移量。
Kafka将新头部的开始偏移量保存在数据目录根目录中名为clean -offset-checkpoint的文件中。此文件用于日志的下一个清理周期。我们可以查看我们的topic检查点文件:
cat /var/lib/kafka/data/cleaner-offset-checkpoint01latest-product-price 0 6
如你所见有三行。第一行是文件的版本(我认为为了向后兼容的),第二行值1,显示多少行(这里只有一条记录),和最后一行包含压缩日志topic的名称,分区号,文件头偏移量。
结论
在本文中,向展示了什么是日志压缩、如何存储它们以及Kafka如何定期清理它们。最后,我想指出,日志压缩对于缓存场景非常有用,在缓存场景中,只想让每个记录的最新值保持在接近实时的状态。假设你希望在应用程序启动时构建缓存。您只需读取压缩后的topic并构建缓存,由于Kafka按顺序读取消息,这比使用SQL数据库来预热缓存要快得多。