日志压缩
日志压缩确保 Kafka 将始终保留单个主题分区的数据日志中每个消息键的至少最后一个已知值。它解决了一些用例和场景,如在应用崩溃或系统故障后的状态恢复,或在运行维护期间应用重启后重新加载缓存。让我们更详细地探讨这些用例,然后描述压缩的工作原理。
到目前为止,我们只描述了比较简单的数据保留方法,即在一段固定的时间后或当日志达到某种预定的大小时,旧的日志数据会被丢弃。这对于临时事件数据很有效,比如日志,每条记录都是单独存在。然而,一类重要的数据流类别是对关键的、易变的数据的更改日志(例如,对数据库表的更改)。
让我们讨论一个这样的数据流的具体例子。
Important
压缩的主题必须有带键的记录,以实现记录的保留。 Kafka 中的压缩并不保证在任何时候都只有一条具有相同键的记录。有可能存在多个具有相同键的记录,包括墓碑,因为压缩时间是不确定的。只有当主题分区满足某些少数条件时才会进行压缩,比如脏污染率、记录处于非活动段文件中等等。
假如我们有一个包含用户电子邮件地址的主题;每当一个用户更新他们的电子邮件地址时,我们用他们的用户 ID 作为主键向这个主题发送一条消息。现在假设我们在某个时间段内为一个 id 为 123 的用户发送以下消息,每条消息都对应于电子邮件地址的变化(其他 id 的消息被省略)。
123 => bill@microsoft.com
.
.
.
123 => bill@gatesfoundation.org
.
.
.
123 => bill@gmail.com
日志压缩给了我们一个更细化的保留机制,这样我们就能保证至少保留每个主键的最后一次更新(例如 bill@gmail.com)。通过这样做,我们保证日志包含了每个键的最终值的完整快照,而不仅仅是最近改变的键。这意味着下游的消费者可以从这个主题上恢复他们自己的状态,而我们不需要保留所有变化的完整日志。
让我们先看一下这一点很有用的几个用例,然后我们再看看如何使用它。
-
数据库变更订阅。通常有必要将一个数据集放在多个数据系统中,而这些系统中往往有一个是某种类型的数据库(要么是 RDBMS,要么可能是新式的键值存储)。例如,你可能有一个数据库、一个缓存、一个搜索集群和一个 Hadoop 集群。数据库的每一个变化都需要反映在缓存、搜索集群中,并最终反映在 Hadoop 中。在一个人只处理实时更新的情况下,你只需要最近的日志。但如果你想能够重新加载缓存或恢复一个失败的搜索节点,你可能需要一个完整的数据集。
-
事件源。这是一种应用程序的设计风格,它将查询处理与应用程序放在一起,并使用变日志作为应用程序的主要存储。
-
用于高可用性的日志。一个进行本地计算的进程可以通过记录它对基本地状态的改变来实现容错,这样如果它失败了,另一个进程可以重新加载这些改变并继续运行。这方面的一个具体例子是在一个流查询系统中处理计数、聚合和其他类似“分组”的处理。Samza,一个实时流处理框架,正是为了这个目的而使用这个功能。
在每一种情况下,我们主要需要处理实时的变化,但偶尔,当一台机器崩溃或数据需要重新加载或重新处理时,我们需要做一个完整的加载。日志压缩允许从统一支持主题中提供这两种使用情况。在这篇博客中,对日志的这种使用方式有更详细的描述。
一般的想法是简单的。如果我们有无限的日志保留,并且我们记录了上述情况下的每一个变化,那么我们就会捕捉到系统从最初开始的每个时间段的状态。使用这个完整的日志,我们可以通过回放日志中的前 N 条记录来恢复到任何时间点。这种假设的完整日志对于多次更新一条记录的系统来说并不实用,因为即使是稳定的数据集,日志也会无限制地增长。简单的日志保留机制会扔掉旧的更新,但日志不再是恢复当前状态的方法,现在从日志的开头恢复不再能重现当前状态,因为旧的更新可能根本就没有被捕获。
日志压缩是一种机制,提供更细粒度的每条记录的保留,而不是更粗粒度的基于时间的保留。我们的想法是有选择地删除哪些有相同逐渐的最近更新的记录。这样以来,日志就能保证每个键至少有最后的状态。
这种保留策略可以按主题设置,因此一个集群可以有一些主题,其保留是通过大小或时间来执行的,而其他主题的保留是通过压缩来执行的。
这一功能的灵感来自于 LinkedIn 最古老、最成功的基础设施之一 —— 名为 Databus 的数据库变更日志缓存服务。与大多数日志结构的存储系统不同,Kafka 是为订阅而建立的,并为快速线性读写组织数据。与 Databus 不同的是,Kafka 作为一个真实源存储,所以即使在上游数据源无法重放的情况下,它也很有用。
日志压缩基础
下面是一张高层次的图片,它显示了 Kafka 日志的逻辑结构和每个消息的偏移量。
日志的头部与传统的 Kafka 日志相同。它有密集的、连续的偏移,并保留了所有的消息。日志压缩增加了一个处理日志尾部的选项。上面的图片显示了一个有压缩尾部的日志。请注意,日志尾部的信息保留了它们第一次被写入时分配的原始偏移量 —— 这一点从未改变。还要注意的是,所有的偏移量仍然是日志中的有效位置,即使该偏移量的消息已经被压缩掉了;在这种情况下,这个位置与日志中出现的下一个最高偏移量是没有区别的。例如,在上图中,偏移量 36、37 和 38 都是相等的位置,从这些偏移量中的任何一个开始读,都会返回一个以 38 开始的信息集。
压缩也允许删除。一个嗲有键和空有效负载的消息将被视为从日志中删除。这个删除标记将导致任何带有该键的先前消息被删除(就像任何带有该键的新消息一样),但删除标记是特殊的,因为它们本身将在一段时间后从日志中清理出来以释放空间。在上图中,不再保留删除信息的时间点被标记为“删除保留点”。
压缩实在后台通过定期重新复制日志段来完成的。清理工作不会阻碍读取,并且可以被节制在不超过可配置的 I/O 吞吐量的范围内,以避免影响生产者和消费者。压缩日志段的实际过程看起来像这样:
日志压缩能提供什么保障?
日志压缩保证了以下几点:
-
任何停留在日志头部的消费者都会看到每个被写入的消息;这些消息会有顺序的偏移。该主题的 min.compaction.lag.ms 可以用来保证在一条消息被写入后必须经过的最小时间长度,然后才能被压缩。也就是说,它为每条信息在(未压缩的)头部停留的时间提供了一个下限。该主题的 max.compaction.lag.ms 可以用来保证从消息被写入到该消息有资格被压缩的最大延迟。
-
消息的顺序总是被保持。压缩永远不会重新排列消息,只是删除一些。
-
一个消息的偏移量永远不会改变。它是日志中一个位置的永久标识符。
-
任何从日志开始的消费者将至少看到所有记录的最终状态,并按照它们被写入的顺序。此外,只要消费者在小于主题 delete.retention.ms 设置的时间段(默认是 24 小时)内到达日志的头部,就会看到所有被删除的记录的删除标记。换句话说:由于删除标记的删除是与读取同时发生的,如果消费者滞后超过 delete.retention.ms ,就有可能错过删除标记。
日志压缩细节
日志压缩是日志清理器处理的,这是一个后台线程池,它重新复制日志段文件,删除哪些关键出现在日志头部的记录。每个压缩器线程的工作方式如下:
- 它选择日志头部与日志尾部比例最高的那份日志。
- 它为日志头部的每个键的最后偏移量创建一个简洁的摘要。
- 它从头到尾重新复制日志,删除在日志中较晚出现的键。新的、干净的片段会被立即交换到日志中,因此所需要的额外磁盘空间只是一个额外的日志片段(而不是日志的完整拷贝)。
- 日志头的摘要本质上只是一个空间紧凑的哈希表。它每个条目正好使用 24 个字节。因此用 8GB 的清洁器缓冲区,一个清洁器的迭代可以清洁大约 366GB 的日志头(假设是 1k 的消息)。
日志清理器配置
默认情况下,日志清理器是被启用的。这将启动清理器的线程池。要在一个特定的主题上弃用日志清理,请添加日志特定属性:
log.cleanup.policy=compact
log.cleanup.policy 属性是在 broker 的 server.properties 文件中定义的配置;它影响集群中所有没有配置覆盖的主题。日志清理器可以被配置为保留最小数量的未压缩的日志“头部”。这可以通过设置压缩的滞后时间来实现。
log.cleaner.min.compaction.lag.ms
这可以用来防止比最小消息年龄更新的消息被压缩。如果不设置,所有的日志段都有资格被压缩,除了最后一个段,也就是当前被写入的段。即使所有的消息逗比最小压缩时间滞后,活动段也不会被压缩。可以对日志清理器进行配置,以确保一个最大的延迟,在这个延迟之后,未压缩的日志“头部”才有资格进行日志压缩。
log.cleaner.max.compaction.lag.ms
这可以用来防止低生产率的日志在不受限制的时间内不符合压缩的要求。如果不设置,不超过 min.cleanable.dirty.ratio 的日志就不会被压缩。注意,这个压缩期限不是一个硬性的保证,因为它仍然受制于日志清理器县城的可用性和实际压缩时间。你要监控 uncleanable-partitions-count, max-clean-time-secs 和 max-compaction-delay-secs 指标。
更多清理器配置在 Kafka Broker Configurations 中有所描述。