Paimon对比基于消息队列(如Kafka)的传统实时数仓方案的优势

发布于:2025-07-20 ⋅ 阅读:(22) ⋅ 点赞:(0)

弊端:数据重复 -> 优势:Paimon 主键表原生去重

原方案弊端 (Kafka)

  • 问题: 消息队列(Kafka)是仅支持追加(Append-Only)的日志流。当 Flink 作业发生故障恢复(Failover)或业务逻辑迭代重跑数据时,同样的数据会被再次写入消息队列,形成重复数据。
  • 影响: 下游应用(如DWS层、ADS层或直接对接的BI报表)必须自己实现复杂的去重逻辑,这不仅消耗大量计算资源(“资源消耗至少增加一倍”),而且可能导致作业不稳定。

Paimon 的优势

  • 解决方案: Paimon 提供了主键表(Primary Key Table)。这种表类型基于 LSM 树(Log-structured merge-tree)结构,原生支持 UPSERT (更新或插入) 和 DELETE 操作。
  • 工作原理: 当 Flink 作业将数据写入 Paimon 的主键表时:
    • 如果数据的主键已存在,Paimon 会更新该行记录。
    • 如果主键不存在,Paimon 会插入新行。
    • 因此,即使上游作业重发了数据,Paimon 也能通过主键自动去重,确保表里存储的始终是每个主键对应的最新状态。下游消费者不再需要关心去重问题。

正如文档中所描述的,主键表是 Paimon 的核心功能之一,用于支持大规模的实时更新。

# Overview

If you define a table with primary key, you can insert, update or delete records in the table.

Primary keys consist of a set of columns that contain unique values for each record. Paimon enforces data ordering by
sorting the primary key within each bucket, allowing users to achieve high performance by applying filtering conditions
on the primary key. See [CREATE TABLE]({{< ref "flink/sql-ddl#create-table" >}}).

在 Flink SQL 中创建这样的主键表非常简单:

// ... existing code ...
CREATE TABLE my_table (
    user_id BIGINT,
    item_id BIGINT,
    behavior STRING,
    dt STRING,
    hh STRING,
    PRIMARY KEY (dt, hh, user_id) NOT ENFORCED
);
// ... existing code ...

弊端:DWS 层缺失 -> 优势:Paimon 支持聚合与更新,构建 DWS 层

原方案弊端 (Kafka)

  • 问题: 由于 Kafka 不支持 UPSERT,无法对聚合结果进行原地更新。例如,一个按用户ID统计5分钟窗口消费额的 DWS 层,10:00-10:05 的聚合结果和 10:05-10:10 的聚合结果会作为两条独立的消息存在于 Kafka 中。
  • 影响: 无法构建一个持续更新状态的 DWS 层。下游要么消费这个中间结果流自己再做一次聚合,代价极高;要么跳过 DWS 层,直接将 DWD 数据写入在线存储(如 ClickHouse),把聚合压力和复杂性推给了下游系统。

Paimon 的优势

  • 解决方案: Paimon 的主键表能力完美解决了这个问题。我们可以轻松地在 Paimon 中构建 DWS 层。
  • 工作原理: Flink 聚合任务(例如,每5分钟统计一次用户消费总额)可以将聚合结果 UPSERT 到 Paimon 的 DWS 表中。这张表以用户ID为主键。每次新的聚合结果到来时,它会直接更新对应用户的消费总额,而不是插入一条新纪录。
  • Changelog 支持: 更重要的是,Paimon 可以为下游生成 changelog(变更日志)。下游系统(如 ClickHouse)可以直接消费这个 changelog 流,轻松地同步最新的聚合结果,而无需处理复杂的合并逻辑。

Paimon 的 merge-engine 机制甚至允许自定义更新逻辑,例如 partial-update(部分更新)或 aggregation(聚合),这为构建 DWS 层提供了极大的灵活性。


// ... existing code ...
- Realtime updates:
  - Primary key table supports writing of large-scale updates, has very high update performance, typically through Flink Streaming.
  - Support defining Merge Engines, update records however you like. Deduplicate to keep last row, or partial-update, or aggregate records, or first-row, you decide.
// ... existing code ...

弊端:No Schema -> 优势:Paimon 提供统一的、可查询的 Schema

原方案弊端 (Kafka)

  • 问题: 存储在Kafka 中的数据(通常是 JSON 字符串)是半结构化的。虽然 Flink 作业在流处理时会解析它,但数据本身在消息队列中没有一个强定义的、可供外部查询的 Schema。
  • 影响: 其他数据消费者(如算法、BI 团队)无法直接查询 DWD 层数据。他们需要自己写程序消费 Kafka,然后重复解析和计算,造成了巨大的资源浪费和协作成本。

Paimon 的优势

  • 解决方案: Paimon 是一个湖仓格式,它将数据以结构化的、带 Schema 的表形式存储。
  • 工作原理: 数据一旦写入 Paimon 表,就拥有了明确的表结构(列名、数据类型等)。这个 Schema 信息会和数据一起被管理。
  • 生态兼容: Paimon 提供了强大的生态兼容性,支持 Flink, Spark, Hive, Trino, Doris 等多种查询引擎。这意味着 BI、算法等团队可以直接使用他们熟悉的 SQL 工具,像查询普通数据库一样查询 Paimon 中的 DWD 和 DWS 表,极大地提升了数据利用效率。

Paimon 的兼容性矩阵展示了其强大的生态整合能力:


// ... existing code ...
| Engine | Version | Batch Read | Batch Write | Create Table | Alter Table | Streaming Write | Streaming Read | Batch Overwrite | DELETE & UPDATE | MERGE INTO | Time Travel |
| :-------------------------------------------------------------------------------: | :-------------: | :-----------: | :-----------: | :-------------: | :-------------: | :----------------: | :----------------: | :---------------: | :---------------: | :----------: | :-----------: |
| Flink | 1.15 - 1.20 | ✅ | ✅ | ✅ | ✅(1.17+) | ✅ | ✅ | ✅ | ✅(1.17+) | ❌ | ✅ |
| Spark | 3.2 - 3.5 | ✅ | ✅ | ✅ | ✅ | ✅(3.3+) | ✅(3.3+) | ✅ | ✅ | ✅ | ✅(3.3+) |
| Hive | 2.1 - 3.1 | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| Trino | 420 - 440 | ✅ | ✅(427+) | ✅(427+) | ✅(427+) | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
// ... existing code ...

弊端:资源与效率 -> 优势:Paimon 统一流批,简化架构

原方案弊端 (Lambda 架构)

  • 问题: 为了保证数据的最终准确性(例如,通过离线反作弊修正数据),需要维护实时(Flink on Kafka)和离线(ETL on MaxCompute)两套独立的数据链路和代码。
  • 影响: 开发和运维成本翻倍,架构复杂,数据同步和一致性保障困难。

Paimon 的优势

  • 解决方案: Paimon 作为统一存储层,天然支持流式读写和批式读写,是构建流批一体(Unified Batch and Streaming)架构的理想选择。
  • 工作原理:
    • 流式写入: Flink 实时作业持续将数据写入 Paimon。
    • 批式读/写: 离线作业(如 Spark 或 Flink Batch)可以读取 Paimon 表的最新快照进行批量处理(如反作弊计算),并将修正后的数据写回到同一张 Paimon 表中。
    • 统一视图: 无论是实时查询还是离线查询,都访问的是同一份存储,同一张表。这彻底消除了两套系统、两份数据的问题。

在ODS层完成数据解析后,可以将数据反向写入到Paimon中,这正是 Paimon 流批一体能力的完美体现。

Paimon 的架构设计就是为了解决这个问题,提供一个统一的存储底座。

// ... existing code ...
Paimon provides table abstraction. It is used in a way that
does not differ from the traditional database:
- In `batch` execution mode, it acts like a Hive table and
  supports various operations of Batch SQL. Query it to see the
  latest snapshot.
- In `streaming` execution mode, it acts like a message queue.
  Query it acts like querying a stream changelog from a message queue
  where historical data never expires.
// ... existing code ...

总结

痛点 Kafka 方案 Paimon 解决方案
数据重复 Append-only,导致Failover后数据重复 主键表,通过 UPSERT 自动去重
DWS层构建 无 UPSERT,无法原地更新聚合结果 主键表,支持聚合结果的持续更新,可构建DWS层
数据共享 No Schema,下游需重复解析 统一Schema,多引擎可直接用SQL查询,提升协作效率
架构冗余 Lambda架构,流批两套代码和存储 流批一体,统一存储,简化架构,降低开发运维成本

常见优化策略

优化策略主要围绕性能存储稳定性三个方面展开,这是构建和维护高性能湖仓系统的核心。


一、 性能优化

性能优化的核心目标是提升数据写入和处理的吞杜量,同时降低延迟

1. 启用异步 Compaction
  • 分析:

    • Paimon 基于 LSM 树结构,写入时会产生多个有序的小文件(Sorted Runs)。查询时需要合并这些文件,文件过多会严重影响查询性能。
    • Compaction 就是将这些小文件合并成大文件的过程。默认情况下,Compaction 可能和数据写入操作在同一个 Flink Task 中同步或半同步执行,这会占用写入链路的资源,尤其是在写入高峰期,可能导致反压和延迟增加。
    • 异步 Compaction (Asynchronous Compaction) 将合并操作与写入操作解耦。写入操作可以快速完成,将合并任务交给独立的机制或在系统负载较低时执行。这极大地提升了写入的吞吐量和稳定性,正如您提到的“节点切换的平均耗时超过 50 秒,而开启后则缩短至 20 秒”。
  • 实现与扩展:

    • 完全异步化: 通过调整特定参数,可以让 Compaction 完全不阻塞写入,最大化写入吞吐量。这适用于对写入性能要求极高,但对数据查询的即时性要求稍低的场景。
       
      // ... existing code ...
      num-sorted-run.stop-trigger = 2147483647
      sort-spill-threshold = 10
      lookup-wait = false
      // ... existing code ...
      
    • 专用 Compaction 作业: 这是更彻底的解耦方式。可以将主写入作业的 Compaction 完全关闭 ('write-only' = 'true'),然后启动一个独立的、专用的 Flink 作业来负责 Compaction。这样可以为写入和 Compaction 分配独立的资源,互不干扰,实现更精细的资源管理和调优。
       
      // ... existing code ...
          public static final ConfigOption<Boolean> WRITE_ONLY =
                  key("write-only")
                          .booleanType()
                          .defaultValue(false)
                          .withFallbackKeys("write.compaction-skip")
                          .withDescription(
                                  "If set to true, compactions and snapshot expiration will be skipped. "
                                          + "This option is used along with dedicated compact jobs.");
      // ... existing code ...
      
      然后通过 Flink Action 启动专用作业:
       
      // ... existing code ...
      <FLINK_HOME>/bin/flink run \
          /path/to/paimon-flink-action-{{< version >}}.jar \
          compact \
      // ... existing code ...
      

2. 调整 Checkpoint Interval
  • 分析:

    • 在 Flink 流式写入 Paimon 的场景中,Checkpoint 是触发数据从内存刷写到文件系统并生成 Paimon Snapshot 的关键
    • 过于频繁的 Checkpoint (短间隔) 会导致生成大量的小文件,增加文件系统压力和后续 Compaction 的负担。同时,频繁的 Barrier 对齐和状态快照也会消耗大量网络和 CPU 资源,在高吞吐场景下极易引起反压。
    • 增大 Checkpoint Interval 可以让数据在内存的 write-buffer 中累积更多,单次刷写生成的文件更大,从而减少小文件数量和 Checkpoint 开销。
  • 实现与扩展:

    • 除了增大 execution.checkpointing.interval,还可以调整 execution.checkpointing.max-concurrent-checkpoints 来允许更多的 Checkpoint 并行,提高容错效率。
    • Buffer 优化: 配合增大 Checkpoint 间隔,应适当增大 Paimon 的写缓冲 write-buffer-size,并可以开启 write-buffer-spillable,当内存 Buffer 写满时,会先溢写到本地磁盘,而不是直接刷到远程存储,这样可以平滑 Checkpoint 峰值压力,生成更大的最终文件。
     
    // ... existing code ...
    1. Flink Configuration (`'flink-conf.yaml'/'config.yaml'` or `SET` in SQL): Increase the checkpoint interval
       (`'execution.checkpointing.interval'`), increase max concurrent checkpoints to 3
       (`'execution.checkpointing.max-concurrent-checkpoints'`), or just use batch mode.
    2. Increase `write-buffer-size`.
    3. Enable `write-buffer-spillable`.
    // ... existing code ...
    
3. 调整 Writer 节点资源与并行度
  • 分析:

    • Writer 节点的并行度直接决定了数据写入的能力。这个并行度通常应该与 Paimon 表的 bucket 数量相匹配或成倍数关系,以确保数据均匀分布到各个 bucket,避免数据倾斜。
    • 在数据回刷或追赶历史数据时,上游数据源的读取速度会很快,此时必须相应地调大 Writer 的并行度来匹配处理能力。
    • Writer 节点的内存 (write-buffer-size) 也至关重要,它直接影响单次刷盘的文件大小和 Compaction 的效率。
  • 实现与扩展:

    • 动态调整: 虽然分区和 Bucket 数不建议频繁调整,但 Flink 作业的并行度是可以在停机重启时调整的。在数据回刷场景,可以临时调高并行度,完成后再恢复正常值。
    • 使用 Flink 托管内存: 为了避免手动管理内存导致 OOM,可以启用 'sink.use-managed-memory-allocator' = 'true'。这样 Paimon Writer 会使用 Flink 的托管内存,由 Flink TaskManager 统一管理和分配,可以提高资源利用率和稳定性。
     
    // ... existing code ...
    INSERT INTO paimon_table /*+ OPTIONS('sink.use-managed-memory-allocator'='true', 'sink.managed.writer-buffer-memory'='256M') */
    SELECT * FROM ....;
    
4. 合理设置分区与 Bucket
  • 分析:

    • 分区 (Partition) 是数据物理隔离的第一层,通常基于低基数的列(如日期 dt),用于数据管理和查询过滤。好的分区设计可以极大地提升查询性能,因为查询引擎可以直接跳过不相关的分区目录。
    • 桶 (Bucket) 是在分区内对数据进行哈希分桶,是数据读写并行的基本单位。bucket 的数量决定了写入的最大并行度和数据在分区内的分布。
    • Rescale 的代价: 修改分区键或 bucket 数需要 ALTER TABLE 后通过 INSERT OVERWRITE 重写数据,这是一个成本很高的操作。因此,在表设计之初就必须对未来的数据量和并发量有充分预估。
  • 实现与扩展:

    • 分区策略: 选择更新频率低、且常作为查询条件的列做分区键。例如,按天分区 dt 是最常见的策略。
    • Bucket 数估算: Bucket 数应根据分区内的数据量峰值来设定。一个经验法则是,保证每个 Bucket 内单个文件的大小在合理范围(例如 128MB ~ 1GB)。您的例子中“每个分区的bucket num为512”就是一个很好的实践,它为每小时高达 700GB 的数据量提供了足够的写入并行度和数据分布。

二、 存储优化

存储优化的核心是控制文件数量,回收无效数据,降低存储成本

1. 文件生命周期管理 (TTL)
  • 分析:

    • Paimon 的每一次提交都会生成一个新的快照 (Snapshot)。为了支持时间旅行 (Time Travel),旧的快照和对应的数据文件会保留一段时间。
    • snapshot.time-retained 和 snapshot.num-retained.min 控制了快照的保留策略。过长的保留时间会导致大量元数据和数据文件堆积。
    • changelog-producer 产生的 Changelog 文件也有独立的生命周期管理。
  • 实现与扩展:

    • 需要根据业务对数据回溯的需求来设定合理的 TTL。例如,如果业务只需要回溯 3 天的数据,那么 snapshot.time-retained 就不应设置得过长。
    • 定期检查和调整 TTL 策略,以平衡数据可恢复性和存储成本。
2. 小文件合并
  • 分析:

    • 流式写入不可避免地会产生小文件。除了前面提到的异步 Compaction 和专用 Compaction 作业,Paimon 还提供了其他机制。
    • precommit-compact: 在文件提交到快照之前进行一次合并,可以有效减少最终生成的 changelog 文件数量。
    • full-compaction: 全量合并,可以将一个分区/桶内的所有文件合并成一个或少数几个文件,对查询性能提升最大。可以通过 'full-compaction.delta-commits' 定期触发。
  • 实现与扩展:

    • Sort Compact: 在执行 Compaction 时,可以指定按某些列 (Z-Order 或普通排序) 对数据进行排序。这可以极大地优化基于这些列的范围查询或点查性能,因为数据在物理上是连续存储的,可以最大化数据跳过的效果。
       
      // ... existing code ...
      CALL sys.compact(
         `table` => 'database_name.table_name', 
         partitions => 'partition_name', 
         order_strategy => 'z-order',
         order_by => 'col1,col2'
      );
      // ... existing code ...
      
    • 外部治理服务: 对于大型湖仓平台,可以引入如 Apache Amoro 这样的外部治理服务,它能提供更智能、自动化的表维护(Self-Optimizing),包括小文件合并、数据过期等。
3. 清理废弃/孤立文件
  • 分析:
    • 作业异常终止或旧版本 Paimon 的 Bug 可能会导致产生一些不被任何快照引用的孤立文件。这些文件占用了存储空间且不会被自动清理。
  • 实现与扩展:
    • Paimon 提供了 expire_snapshots 和 drop_partition 等 Action,可以用来清理快照和分区。
    • 社区也提供了相应的工具或讨论来识别和清理孤立文件。编写定时脚本,定期执行 Paimon 提供的清理命令,是一种有效的运维实践。

三、 稳定性优化

稳定性优化的核心是保障作业在各种异常情况下(尤其是高负载时)的健壮性和可恢复性

1. 启用 Consumer
  • 分析:

    • 当 Flink 作业从 Paimon 消费数据时,它会从某个快照开始读取。如果这个快照因为 TTL 过期而被删除了,作业就无法从该点恢复。
    • Consumer 机制允许为某个消费作业(由 consumer-id 标识)“锁定”一个快照。这个被锁定的快照及其之后的所有快照都不会被 TTL 机制自动删除,直到 Consumer 前进到新的快照。
    • 这极大地增强了下游消费作业的可恢复性,但代价是可能会保留更多的快照和数据文件,增加了存储成本。
  • 实现与扩展:

    • 这是一个典型的恢复能力与存储成本之间的权衡。对于关键的下游应用,启用 Consumer 是必要的。对于非核心应用,可以不启用,或定期手动重置消费位点。
2. 调整 TM (Task Manager) 和 Committer 资源
  • 分析:

    • TM 资源: Paimon Writer 的内存需求与数据记录大小、更新频率、Bucket 数量等密切相关。内存不足是导致 OOM 和作业不稳定的主要原因。
    • Committer 节点: 这是 Flink 写入 Paimon 的最后一步,负责将所有 Task 生成的 manifest 文件合并,并生成最终的 snapshot 文件。当一次 Checkpoint 写入的分区和文件非常多时,Committer 会成为瓶颈,需要大量的内存来持有这些元数据信息,也需要足够的 CPU 来完成合并。
  • 实现与扩展:

    • 精细化资源管理: Flink 1.18 之后默认开启了细粒度资源管理。可以利用这个特性为 Paimon 的 Committer 算子单独配置更高的内存和 CPU,而无需增加整个 TaskManager 的资源,从而实现更高效的资源利用。
       
      // ... existing code ...
      You can use fine-grained-resource-management of Flink to increase committer heap memory only:
      1. Configure Flink Configuration `cluster.fine-grained-resource-management.enabled: true`. (This is default after Flink 1.18)
      2. Configure Paimon Table Options: `sink.committer-memory`, for example 300 MB, depends on your `TaskManager`.
         (`sink.committer-cpu` is also supported)
      // ... existing code ...
      
    • 经验公式与监控: 结合 Paimon 社区提供的经验公式和实际的监控数据(如内存使用率、GC 时间、反压情况),持续迭代和优化资源配置。

总结

结合 Paimon 的文档和特性,我们可以看到这些策略背后都有其深刻的技术原理支撑。核心思想可以归纳为:

  1. 解耦与异步: 将耗时的 Compaction 操作与主写入链路解耦,是提升写入性能和稳定性的关键。
  2. 批处理思想: 在流处理中引入批处理的思想,通过增大 Checkpoint 间隔和 Buffer,将多次小操作合并为一次大操作,以摊销固定开销。
  3. 预估与规划: 在表设计阶段充分预估未来数据量,合理规划分区和 Bucket,避免后期高昂的调整成本。
  4. 权衡与取舍: 在性能、成本、稳定性、数据时效性之间做出权衡。例如,Consumer 提升了恢复能力但增加了存储成本;高压缩率降低了存储但增加了 CPU 开销。
  5. 精细化运维: 利用专用作业、细粒度资源管理等高级特性,对不同组件进行针对性优化,实现对整个系统的精细化控制。

这些策略共同构成了一套行之有效的 Paimon 湖仓优化方法论。

补充Paimon快照存储占用

快照(Snapshot)的本质 是一个元数据文件。

Paimon 的数据组织是一个清晰的层级结构,正如文档中图示的那样: Snapshot -> Manifest List -> Manifest -> Data File

  • Snapshot 文件: 是表的某个时间点版本的入口。它本身很小,是一个 JSON 文件,记录了这个版本包含哪些 Manifest List 文件,以及其他元数据。您正在查看的 Snapshot.java 文件就定义了它的结构。
  • Manifest List / Manifest 文件: 也是元数据文件,它们像目录一样,逐层记录了哪些数据文件(Data File)属于这个快照版本,以及这些数据文件的状态(是新增的还是被删除的)。
  • Data File: 这才是真正存储着表数据的物理文件(例如 Parquet 文件)。

所以,一个快照通过层层指向,最终“引用”了一批数据文件。

逻辑删除 vs 物理删除

当对表进行更新、删除或执行 Compaction(合并)操作时,Paimon 并不会立即去物理删除旧的数据文件。它会执行一个逻辑删除

  1. 生成新的数据文件。
  2. 创建一个新的 Snapshot
  3. 在这个新 Snapshot 的 Manifest 文件中,将旧的数据文件标记为 DELETE,将新的数据文件标记为 ADD

此时,旧的 Snapshot 依然存在,并且它仍然指向那些被“逻辑删除”的旧数据文件。这就是 Paimon 实现时间旅行(Time Travel) 的基础——只要旧快照还在,就可以随时回到过去的数据版本。

文档 docs/content/learn-paimon/understand-files.md 中对此有清晰的描述:

Paimon maintains multiple versions of files, compaction and deletion of files are logical and do not actually delete files. Files are only really deleted when Snapshot is expired.

简单来说:Compaction 等操作只做标记,不做真删除。真正的删除由快照过期来触发。

禁止删除有快照指向的文件

只要有一个活跃的、未过期的快照还在引用某个数据文件,这个数据文件就是安全的,绝对不会被删除。物理删除操作只会发生在那些“无主”的文件上——即所有引用它的快照都已经过期并被清除了。

这个机制确保了数据安全性和时间旅行能力,同时通过 TTL 自动回收不再需要的历史数据,从而控制存储成本。

这个过程的实现主要在 SnapshotDeletion.java 这个类中,它负责具体的清理逻辑。

// ... existing code ...
public class SnapshotDeletion extends FileDeletionBase<Snapshot> {

// ... existing code ...
    @Override
    public void cleanUnusedDataFiles(Snapshot snapshot, Predicate<ExpireFileEntry> skipper) {
        if (changelogDecoupled && !produceChangelog) {
            // Skip clean the 'APPEND' data files.If we do not have the file source information
            // eg: the old version table file, we just skip clean this here, let it done by
            // ExpireChangelogImpl
            Predicate<ExpireFileEntry> enriched =
                    manifestEntry ->
                            skipper.test(manifestEntry)
                                    || (manifestEntry.fileSource().orElse(FileSource.APPEND)
                                            == FileSource.APPEND);
            cleanUnusedDataFiles(snapshot.deltaManifestList(), enriched);
        } else {
            cleanUnusedDataFiles(snapshot.deltaManifestList(), skipper);
        }
        cleanUnusedDataFiles(snapshot.baseManifestList(), skipper);
    }
// ... existing code ...

这个类中的方法会遍历过期快照的 deltaManifestList 和 baseManifestList,收集文件列表,然后执行清理。

快照 TTL 如何触发物理删除

快照 TTL(Time-To-Live,生命周期)是如何处理多版本数据删除的?

  1. 定义过期策略: 可以配置快照的保留策略,比如 snapshot.time-retained (保留时长) 和 snapshot.num-retained.min (最小保留数量)。
  2. 识别过期快照: 当一个快照的存活时间超过了您设定的 TTL,它就会被 Paimon 的过期机制(Expire)识别为“已过期”。
  3. 清理过程:
    • 过期程序(ExpireSnapshots)会启动,它首先会删除这些过期的 snapshot JSON 文件本身。
    • 接着,它会读取这些过期快照所引用的 Manifest 文件,列出所有被这些过期快照“逻辑删除”的数据文件。
    • 最关键的一步:程序会检查这个列表中的每一个数据文件,确认它是否还被任何一个“未过期”的(即活跃的)快照所引用
    • 只有当一个数据文件不再被任何活跃快照引用时,它才会被物理删除