本文翻译自 StreamNative 博客《A Deep Dive into the Topic Data Lifecycle in Apache Pulsar》,作者陈航,Apache Pulsar PMC 成员,StreamNative 主管工程师。

译者信息

译者:何城波,就职于深信服 PaaS 平台部,负责 Pulsar、Kafka、Elasticsearch 相关运维开发工作

Apache Pulsar 中主题的生命周期由两个关键的保留策略管理:Broker 侧的主题保留策略以及 Bookie 侧的 Bookie 数据保留策略。所有的数据删除操作应该由 Broker 触发,而不应该直接从 Bookie 中删除 Ledger 文件。否则,数据将会丢失。

本篇博文聚焦于 Pulsar 中的三个话题:

  1. 1. 主题保留策略,我们主要讨论保留策略不起作用的场景。有关保留和过期策略的更多详细信息,请参考 Message retention and expiry。
  2. 2. Bookie 数据压实(Compaction)策略。
  3. 3. 如何检测和处理孤立 ledger,以修复无法删除的 Ledger 文件。

概述:主题数据的生命周期

在 Pulsar 中,当生产者向主题生产消息时,这些消息将由 Broker 中的 ManagedLedger 组件写入其管理的特定 Ledger 中。其中的元数据存储在 Pulsar 的元数据存储(例如 Apache ZooKeeper)中。Ledger 会根据所配置的副本策略(即 E、WQ 和 AQ 的值)写入特定的 Bookie 中。每个 Ledger 的元数据都存储在 BookKeeper 的元数据存储(例如 Apache ZooKeeper)中。

当需要根据所配置的保留策略删除 Ledger(例如图 1 中的 Ledger 3)时,会执行以下步骤。

技术探究|深入了解 Apache Pulsar 中的主题数据生命周期_apache

图 1

  1. 1. 从元数据存储中 ManagedLedger 的 Ledger 列表中删除 Ledger 3。
  2. 2. 如果第一步删除成功,Broker 会将 Ledger 3 的删除请求异步发送到 BookKeeper。然而,这并不能保证该 Ledger 可以被成功删除,Ledger 3 留在 BookKeeper 中作为孤立的 Ledger 的风险依然存在。有关如何解决此问题的更多信息,请参考 PIP-186。
  3. 3. 每个 Bookie 执行常规的压实检查,通过 ​​minorCompactionInterval​​ 和 ​​majorCompactionInterval​​ 可以进行相关配置。
  4. 4. 在压实检查中,Bookie 会检查每个 Ledger 的元数据是否存在于元数据存储中。如果不存在,则会从日志文件中删除该 Ledger 的数据。

从上面的最后两个步骤可以看出,存储在 BookKeeper 中的主题数据的删除由压实检查器触发,而不是由 Pulsar Broker 触发。这也就意味着从 ManagedLedger 的 Ledger 列表中删除 Ledger 时,不会立即删除 Ledger 数据。

主题保留策略

本节将简要讨论 Pulsar 中的主题保留策略,并聚焦当前 Ledger 删除逻辑的缺点以及保留策略不起作用的情况。

主题保留以及 TTL

当消息到达 Broker 时,它们会先被存储,直到它们在所有订阅上都得到确认时才会被标记成可删除。可以通过为特定的命名空间中的所有主题设置保留策略来保留已在所有订阅上确认的消息。如果消息没有被确认,Pulsar 默认会一直保存它们,这会占用大量的磁盘空间。这时可采用 TTL(生存时间),它可以控制未确认的消息多久被自动确认。有关详细消息请参考 Message retention and expiry[1]

当前 Ledger 删除逻辑的缺点

当前的 Ledger 删除逻辑需要两个单独的步骤。

  1. 1. 从 Ledger 列表中删除所有要删除的 Ledger 并更新元数据存储中最新的 Ledger 列表。
  2. 2. 在元数据存储更新回调操作中,异步从存储(例如 BookKeeper 或分层存储)中移除要删除的 Ledger,注意这并不能确保删除成功。

由于这两个步骤是分开的,我们无法确认 Ledger 删除事务。如果第 1 步成功而第 2 步失败,则无法再从存储系统中删除 Ledger。在某些情况下(例如 Broker 重新启动),第 2 步可能会失败,从而导致存储系统中出现孤立的 Ledger。

我们可以将步骤 1 与步骤 2 交换顺序来解决此问题吗?不行,如果先从存储系统中删除 Ledger,然后从 ManagedLedger 的 Ledger 列表中删除 Ledger,仍然无法确认 Ledger 删除事务。如果从存储系统中成功删除 Ledger,而从 ManagedLedger 的 Ledger 列表中删除 Ledger 失败,消费者将无法从主题中读取数据。这是因为主题仍然认为已删除的 Ledger 是可读的。这个问题比孤立 Ledger 还要严重。

主题删除的另一个风险是,当在 Broker 端删除 Ledger(比如从 ManagedLedger 的 Ledger 列表中删除)时,如果在 BookKeeper 中删除 Ledger 失败,则主题元数据可能会保留。因此,当消费者根据主题元数据获取数据时会失败,因为实际数据在 Bookie 上不存在。

为了解决上述问题,我们正在制定一个 PIP[2],引入两阶段删除协议以确保从存储系统中删除 Ledger 的操作可重试。

为什么保留策略不生效

主题保留策略在以下两种情况下可能不会生效。

主题没有加载到 Broker 中

每个主题的保留策略检查器都属于它自己的 ManagedLedger。如果 ManagedLedger 未加载到 Broker 中,则保留策略检查器将不起作用。例如以下这个例子。

我们在 t0 时刻开始向 topic-a 生产 100GB 的数据,并在第 t0 + 3 小时完成了消息的生产。topic-a 的保留策略配置为 6 小时,即 t0 + 6 小时后数据才会过期。但是,由于 Broker 重启、负载均衡器卸载了它的 Bundle 或 ​​pulsar-admin​​ 命令触发的相关操作等原因,topic-a 可能会在 [t0 + 3, t0 +6] 之间被卸载。如果没有生产者、消费者或其他加载主题操作,则会一直保持卸载状态。当时间达到第 t0 + 6 小时时,topic-a 上的 100GB 数据根据保留策略应该过期。但是,由于没有将 topic-a 加载到任何 Broker 中,Broker 的保留策略检查器无法找到 topic-a。因此,保留策略不起作用。在这种情况下,直到 topic-a 再次加载到 Broker 中,这 100GB 的数据才会过期删除。

我们正在开发一个工具来解决这个问题。该工具将检查 BookKeeper 集群中长期存储的 Ledger,并解析出这些 Ledger 所属的主题名,之后会将这些主题加载到 Pulsar Broker 中,以便保留策略可以对其生效。

Ledger 处于 OPEN 状态

保留策略检查器会应用每个主题的保留策略,但是它仅检查处于 CLOSED 状态的 Ledger。如果 Ledger 是 OPEN 状态,即使 Ledger 应该过期,保留策略也不会生效。例如以下这个例子。

我们以 100MB/s 的速率向主题生产消息,Ledger 的最小翻转时间间隔设置为 10 分钟。它用于防止 Ledger 翻转频繁发生,Ledger 翻转之前必须达到最小翻转时间。

这意味着 Ledger 在前 10 分钟内都将保持在 OPEN 状态。10 分钟后,Ledger 大小约为 60GB。如果将保留时间设置为 5 分钟,则这些数据不会过期,因为 Ledger 处于 OPEN 状态。请注意,在达到最小翻转时间(managedLedgerMinLedgerRolloverTimeMinutes)并且满足以下条件之一后,才可以触发 Ledger 翻转:

  • • 已达到最大翻转时间(managedLedgerMaxLedgerRolloverTimeMinutes)
  • • 写入 Ledger 的条目数已达到最大值(managedLedgerMaxEntriesPerLedger)
  • • 写入 Ledger 的条目已达到最大字节大小(managedLedgerMaxSizePerLedgerMbytes)

这些参数可以在 broker.conf 中配置。

在这个示例中,如果主题在第 t0 + 9 分钟时刻被卸载并保持卸载状态,则无论配置的保留策略是什么,都至少有大约 54GB 的数据不会过期删除。

Bookie 数据压实策略

BookKeeper 集群根据存储在元数据存储(例如 ZooKeeper)中的 Ledger 元数据来判定该 Ledger 是否存在。如果 Pulsar 已经从元数据存储中删除了该 Ledger 的元数据,则意味着需要从所有存储该 Ledger 副本的 Bookie 中删除 Ledger 数据。

当需要根据主题保留策略删除 Ledger 时,Pulsar 仅删除 Ledger 的元数据,而不是存储在 Bookie 上的实际副本数据。实际数据是否被删除取决于每个 Bookie 的垃圾回收线程。

每个 Bookie 的垃圾回收在以下三种情况下被触发。

1.Minor 压实,可以通过 ​​minorCompactionThreshold=0.2​​​ 和 ​​minorCompactionInterval=3600​​ 进行配置。默认情况下,Minor Compaction 每小时触发一次。如果 entryLogFile 的剩余数据大小小于总大小的 20%,则 entryLogFile 将被压实。

2.Major 压实,可以通过 ​​majorCompactionThreshold=0.5​​​ 和 ​​majorCompactionInterval=86400​​ 进行配置。默认情况下,Major Compaction 每天都会触发,如果 entryLogFile 的剩余数据大小小于总大小的 50%,则 entryLogFile 将被压实。

3.通过 REST API 触发的压实(​​curl -XPUT <http://localhost:8000/api/v1/bookie/gc>​​​)。对于 REST API,可以先通过设置 ​​httpServerEnabled=true​​ 来启用。

Bookie GC 如何工作

当 Bookie 触发压实时,压实检查器会检查每个 Ledger 的元数据以获取 Ledger 列表。对于 Ledger 列表中的每个 Ledger,它会检查 Ledger 的元数据是否仍然存在于元数据存储中,例如图 2 中的 Ledger 2(图中 Ledger 2 有 3 个副本分别存在于 Bookie1、Bookie3 和 Bookie8 中)。

技术探究|深入了解 Apache Pulsar 中的主题数据生命周期_数据_02

图 2

之后,压实检查器过滤出仍然存在的 Ledger,并计算 entryLogFile 的剩余数据大小百分比。如果百分比低于阈值(默认情况下,​​minorCompactionThreshold=0.2​​​ 和 ​​majorCompactionThreshold=0.5​​),它将开始对 entryLogFile 进行压实。具体来说,它从旧的 entryLogFile 中读取剩余的 Ledger 数据,并将它们写入当前的 entryLogFile。在所有剩余的 Ledger 成功压实后,它会删除旧的 entryLogFile,以释放存储空间。

技术探究|深入了解 Apache Pulsar 中的主题数据生命周期_java_03

图 3

如何减少 GC IO 影响

压实检查器会从旧的 entryLogFile 中读取数据并将它们写入当前文件,因此可能会导致磁盘的混合读写 IO。如果我们不引入限流策略,将会影响 Ledger 磁盘的性能。

压实限流

在 Bookie 中,有两种压实限流策略,即按字节或按条目数。相关配置如下。

# 按字节或条目数限流压实
isThrottleByBytes=false

# 设置压实过程中重新添加条目的速率。单位是每秒增加条目数。
compactionRateByEntries=1000

# 设置压实过程中重新添加条目的速率。单位是每秒添加字节数。
compactionRateByBytes=1000000

默认情况下,Bookie 使用按条目数限流的策略。但是由于每个条目的数据大小不一样,我们无法控制压实过程中的读写吞吐量,对 Ledger 磁盘的性能会有很大的影响。因此,我们建议使用按字节限流的策略。

PageCache 预读

对于 entryLogFile,如果 90% 以上的条目已经被删除,Compactor 会一一扫描条目的 Header 元数据。当读取一个条目的元数据时,它会错过 BufferedChannel 读取缓冲区缓存从而触发从磁盘预取。对于接下来的条目,Header 元数据读取也会错过 BufferedChannel 读取缓冲区缓存,并且会继续从磁盘预取而不会受到限制。这将导致 Ledger 磁盘 IO 利用率攀高。有关更多信息,请参考 PR-3192[3] 以修复此错误。

此外,从磁盘进行的每个预取操作也会触发 OS PageCache 预取。对于压实模型,OS PageCache 预取会导致 PageCache 污染,也可能会影响 Journal Sync 的延迟。为了解决这个问题,我们可以使用 Direct IO 来降低 PageCache 的效果。有关详细信息,请参考 Issue #2943[4]

为什么 Bookie GC 不生效

当一个 Ledger 磁盘达到最大使用阈值时,它会暂停 Minor 和 Major 压实。当使用 ​​curl -XPUT http://localhost:8000/api/v1/bookie/gc​​​ 触发压实时,会被 ​​suspendMajor​​​ 和 ​​suspendMinor​​ 标志过滤。所以导致了:

1.Bookie 不会清理已删除的 Ledger。

2.无法释放磁盘空间。

3.Bookie 无法从只读状态恢复到可写状态。

在这种情况下,只能通过以下步骤来触发压实。

1.增加最大磁盘使用阈值。

2.重新启动 Bookie。

3.再次使用 ​​curl -XPUT http://localhost:8000/api/v1/bookie/gc​​​ 触发压实。PR-3205 为 REST API 添加了 ​​forceAllowCompaction=true​​​ 标志以忽略 ​​suspendMajor​​​ 和 ​​suspendMinor​​ 标志并强制触发压实。

移除无法被删除的日志文件

当 Pulsar 集群持续运行几个月后,Bookie 上的一些旧 entryLogFile 可能无法删除,主要原因如下:

1.Ledger 删除逻辑错误导致孤立 Ledger。

2.非活跃的主题不会加载到 Broker 中。因此,主题保留策略无法对其生效。

3.集群中仍然存在非活跃的游标,并且无法删除其对应的游标 Ledger。

我们需要一个工具来检测和修复上述场景中的 Ledger。

对于场景 1,使用 PIP-186 的方案[5]即可解决。但是,仍然无法删除现有的孤立 Ledger。我们需要扫描整个 BookKeeper 集群的元数据,并检查每个 Ledger 的元数据。如果 Ledger 相关主题的 Ledger 列表中不包含该 Ledger,则表示该 Ledger 已被删除。可以使用 ​​bookkeeper​​ 命令直接安全地删除这些 Ledger。有关详细信息,请参考此工具[6]

对于场景 2,我们将开发一个检查器来检测仍然保存着 Ledger 数据的非活跃主题。检测到这些主题后,将触发操作将它们加载到 Broker 中,并为它们应用保留策略。此功能仍在开发中。

对于场景 3,我们考虑直接删除长时间不活跃的游标,例如 7 天。

总结

本博文解释了 Apache Pulsar 中的主题数据生命周期,包括主题保留策略和 BookKeeper 垃圾收集逻辑。同时,还讨论了主题数据无法删除的情况,并给出了一些解决方案。

引用链接

​[1]​​ Message retention and expiry: ​https://pulsar.apache.org/docs/cookbooks-retention-expiry/​
​​​[2]​​ PIP: ​https://github.com/apache/pulsar/issues/16569​
​​​[3]​​ 3192: ​https://github.com/apache/bookkeeper/pull/3192​
​​​[4]​​ #2943: ​https://github.com/apache/bookkeeper/issues/2943​
​​​[5]​​ 方案: ​https://github.com/apache/pulsar/issues/16569​
​​​[6]​​ 工具: ​https://docs.streamnative.io/platform/latest/operator-guides/configure/sn-pulsar-tool/pck-tutorial​