GreptimeDB Global GC Worker 设计解析:基于 Compaction 驱动的文件垃圾回收机制
2026/9/17 21:17:18 网站建设 项目流程

GreptimeDB Global GC Worker 设计解析:基于 Compaction 驱动的文件垃圾回收机制

【免费下载链接】greptimedbThe open-source observability database. One columnar engine for metrics, logs, and traces, on object storage.项目地址: https://gitcode.com/GitHub_Trending/gr/greptimedb

本文依据 docs/rfcs/2025-07-23-global-gc-worker.md 展开,并结合 GreptimeDB 当前仓库中 mito2 存储引擎的LocalGcWorker实现(src/mito2/src/gc.rs)、meta-srv 的 GC 调度器(src/meta-srv/src/gc/scheduler.rs)及对应配置项,深入讲解该 GC 机制的设计动机、工作流程、源码实现与配置方式。读者读完后可以理解 GreptimeDB 如何在列式存储引擎中安全回收废弃 Parquet/Puffin 文件、如何借助临时 Manifest 与"心跳"机制保护长查询所需文件,以及如何通过配置项调优 GC 行为。

背景与动机:为什么存储层需要全局 GC

GreptimeDB 是基于对象存储的时序/可观测性数据库,数据最终以 SST(Parquet 数据文件与 Puffin 索引文件)的形式落在对象存储上。随着功能的演进,存储目录中会不断积累"不再被任何组件使用"的陈旧文件,典型来源包括:

  • 表重新分区(repartition):分区规则变更后,旧分区对应的整批 SST 文件会被新文件替换,产生大量废弃文件;
  • Manifest 更新失败:新 SST 文件已成功写入对象存储,但随后的 manifest 更新操作失败,导致文件从未被任何 manifest 记录,成为无人引用的孤儿文件;
  • Compaction / Truncate / Delete:数据合并、清空与删除操作都会将旧文件标记为移除。

如果不做回收,这些文件会持续占用对象存储空间。因此,RFC 提出在 Compaction 流程内集成一个垃圾回收(GC)机制:当一个 region 的 Compaction 完成后,自动触发 GC worker,识别并删除超过保留期限的废弃文件。这一设计把 GC 的执行绑定在一个明确、可定义的操作边界——"一次 compaction 周期的成功完成"上,以正确性和安全性为优先。

术语定义

RFC 将待回收文件划分为两类核心概念:

术语含义典型场景
Unused File(未使用文件)存在于存储目录中、但从未被任何 manifest 正式记录的文件新 SST 成功写入存储,但随后的 manifest 更新失败,导致文件未被引用
Obsolete File(废弃文件)曾经被 manifest 记录、但现已明确标记为移除的文件数据 repartition 或 compaction 后产生的旧文件

仓库实现中进一步引入了两个与时间相关的精确概念(见 src/mito2/src/gc.rs 的模块注释):

  • expel time(驱逐时间):文件被视为"已移除"的时刻,即它从 manifest 中被移除的时间;
  • lingering time(滞留时间):文件从 manifest 移除后、到真正从对象存储删除之间的时间窗口。

此外,代码中还将"未在 manifest 中记录"的文件细分为unknown files(因为 checkpoint 可能已经推进、移除了 checkpoint 之前的移除记录,导致文件的 expel time 无法确定),并为此单独设计了滞留策略。

GC Worker 工作流程

触发与执行位置

RFC 明确:GC worker 作为 Compaction 流程的组成部分运行。某个 region 的 Compaction 完成后,GC worker 自动被触发;执行位置优先放在datanode上,从而避免 metasrv 侧需要额外配置对象存储(object storage)访问权限的负担。

在实际仓库实现中,GC 由两个层面协同驱动:

  1. datanode 侧(执行层):LocalGcWorker 是真正执行文件扫描与删除的 worker,它持有一个或多个待 GC 的 region(同一张表),并携带GcConfigFileRefsManifest等上下文;
  2. metasrv 侧(调度层):GcScheduler 周期性(默认每 5 分钟一个 tick,见 options.rs)向 datanode 下发 GC 请求,并汇总各 datanode 返回的GcReport。它还支持通过Event::Manually手动触发指定 region 的 GC。

完整处理流程

RFC 给出的流程如下:

  1. 触发:region 的 Compaction 成功完成后,调用 GC worker;
  2. 读取 Manifest:读取 region 的主 manifest,获得所有被标记为废弃的文件列表;同时读取长查询产生的临时 manifest(temporary manifests),识别当前正在使用的文件,防止误删;
  3. 检查滞留时间(Obsolete Files):对每个废弃文件计算"滞留时间"(即从 manifest 移除后经过的时间);
  4. 标记删除(Obsolete Files):超过可配置的最大滞留时间、且未被任何活跃临时 manifest 引用的文件,标记为待删除;
  5. 滞留时间(Unused Files):从未被任何 manifest 记录的文件,同样需要等待可配置的最大滞留时间之后才可删除。

对应流程图(RFC 原文):

源码级实现:LocalGcWorker 的执行细节

对照 src/mito2/src/gc.rs,LocalGcWorker::run()按顺序执行以下步骤(与 RFC 流程一一对应):

  1. 读取临时引用read_tmp_ref_files()FileRefsManifest中收集所有 region 当前被引用的文件集合(gc.rs);
  2. 逐 region 执行do_region_gc()内先获取 region 的主 manifest,并校验临时 manifest 记录的 manifest version 与 region 当前 manifest version 是否一致。不一致(例如 leader 刚更新了 manifest version)时直接跳过该 region 的 GC,避免误删仍在使用的文件(gc.rs);
  3. 获取已移除文件及驱逐时间get_removed_files_expel_times()从 manifest 的removed_files记录中还原每个文件的 expel time(即包含移除动作的 delta manifest 的最后修改时间,见 gc.rs);
  4. 并发列出 region 目录文件list_from_object_store()将 region 目录按字典序边界分区,使用多个 lister 并发列出 Parquet 文件,并单独平铺列出region_dir/index/下的 Puffin 索引文件(gc.rs);
  5. 过滤可删除文件filter_deletable_files()结合"在 manifest 中""在临时引用中""处于滞留期""已达可删除条件"四类状态,逐文件判定是否可删;
  6. 物理删除与状态更新delete_files()调用delete_files/delete_indexes真正删除对象存储上的数据与索引;随后update_manifest_removed_files()调用clear_deleted_files清理 manifest 中对应的移除记录(gc.rs)。

需要特别说明的是,仓库实现中 GC 并不总是做"全量文件列表扫描",而是支持两种模式(由LocalGcWorker.full_file_listing字段控制,见 gc.rs):

  • 快速模式(fast):仅删除 manifest 的removed_files中记录的文件,避免昂贵的列表操作,性能最佳,作为常规 GC 使用;
  • 全量列表模式(full_file_listing):完整扫描 region 目录,额外发现并清理未被任何 manifest 跟踪的孤儿文件,代价高,周期性执行即可(metasrv 侧默认每 24 小时执行一次全量列表,见下文配置节)。

废弃文件(Obsolete Files)的回收条件

RFC 明确规定,一个废弃文件只有当以下两个条件同时满足时才会被永久删除

  1. 从 manifest 中被移除(其 obsolescence timestamp)所经过的时间超过可配置阈值;
  2. 当前未被任何活跃的临时 manifest 引用。

在源码中,这一逻辑浓缩在should_delete_file()函数中(gc.rs):

  • 文件若在 manifest 中is_in_manifest)或在临时引用中is_in_tmp_ref),一律不删除;
  • 文件若处于滞留期(is_linger)但未达删除条件(is_eligible_for_delete),不删除;
  • 仅当is_lingeris_eligible_for_delete同时为真时才删除;
  • 对于无法解析为 SST/索引文件名的条目,直接跳过并累加GC_SKIPPED_UNPARSABLE_FILES指标,绝不删除未知类型文件(gc.rs)。

对应地,src/mito2/src/gc/worker_test.rs 中提供了丰富的测试验证这些边界条件:

  • test_gc_worker_basic_compact:写入多批数据并 compaction 后,GC 删除的文件数量与 manifest 中记录的被移除文件数量一致;
  • test_gc_worker_compact_with_ref:当文件被临时引用(FileRefsManifest)保护时,GC 结果为空(0 个文件被删);
  • test_known_file_still_lingering_not_deleted/test_known_file_eligible_for_delete_deleted:分别验证滞留期未满不删、期满即删;
  • test_file_in_manifest_not_deleted/test_file_in_tmp_ref_not_deleted:验证 manifest 与临时引用对文件的强保护。

未使用文件(Unused Files)的处理

RFC 指出,由于 GC worker 与 Compaction 深度集成,"Unused Files"这类最容易被误删的文件类别风险已被大幅化解——新写入的 SST 文件几乎总是紧接着伴随 manifest 更新,因此"写完文件但尚未记入 manifest"的时间窗口极短。任何真正"未使用"(未被任何 manifest 引用)的文件,在超过可配置的最大滞留时间后即可安全删除。

不过,源码实现比 RFC 更为保守,将"unknown files"(无法确定 expel time 的文件)单独对待:

  • 对于活跃/打开的 region:仅当对象文件的last_modified时间超过unknown_file_lingering_time(默认 1 天)阈值时才删除,且采用严格小于(strict less-than)比较;如果对象存储不提供last_modified元数据,则保守地保留该文件(gc.rs);
  • 对于已删除的 region(dropped):unknown 文件可以立即删除,因为这类 region 已无活跃读写,且 meta 侧在发起 dropped-region GC 前会先收集相关活跃 region 的FileRefsManifest以做交叉保护。

对应测试见 worker_test.rs 中的test_unknown_file_within_ttl_not_deletedtest_unknown_file_exceeded_ttl_deletedtest_unknown_file_dropped_region_deletedtest_missing_last_modified_unknown_kepttest_unknown_file_at_cutoff_not_deleted等用例。

同时,为便于调试与审计,GC 会维护近期已删除文件的完整列表(GcReport)。源码中GcReport结构体记录了每个 region 删除的数据文件与索引文件、需要下轮重试的 region、以及成功处理的 region 集合(src/store-api/src/storage/file.rs)。

读一致性保障:临时 Manifest 与"心跳"机制

为什么需要保护机制

GC 最危险的场景是误删仍在被长查询使用的文件。为此,RFC 设计了一套依赖临时 manifest 的保护机制:

  1. 创建临时 manifest:当检测到长查询(例如通过 slow query recorder)时,查询进程向 region 的 manifest 目录写入一个临时 manifest,其中列出该查询所需的全部文件;
  2. 周期性心跳:仅仅创建文件不够——如果查询进程崩溃,临时 manifest 会变成孤儿文件,导致 GC 永远无法回收相关文件。因此执行长查询的进程必须周期性更新临时 manifest 的修改时间戳(touch 文件),作为"查询仍存活"的心跳信号;
  3. GC 校验:GC worker 运行时扫描临时 manifest,检查其最后修改时间;若超过可配置阈值,则判定该临时 manifest 已陈旧(来自崩溃或已终止的查询),将其删除,受其保护的文件随即不再受屏蔽;
  4. 动态生命周期:临时 manifest 在查询启动时创建、由查询周期性保活、查询正常结束时主动删除、异常终止时由 GC worker 自动清理。

仓库中的落地方案:FileRefsManifest

在 GreptimeDB 当前实现中,这一概念落地为FileRefsManifest(临时文件引用清单,见 src/store-api/src/storage/file.rs),包含三部分:

  • file_refs:每个 region 当前被引用(正在被查询使用)的文件集合FileRef
  • manifest_version:生成这些临时引用时对应 region 的 manifest 版本,用于判断临时引用是否过期;
  • cross_region_refs:跨 region 文件归属映射,记录 repartition 后新 region 持有的、原属于源 region 的文件,防止 GC 误删迁移中的文件。

GC worker 在do_region_gc()中会做版本一致性校验:如果FileRefsManifest中记录的 manifest version 与 region 当前 manifest version 不一致,则跳过该 region 的 GC(gc.rs),并累加GC_ERRORS_TOTAL{manifest_mismatch}指标。这对应 RFC 中"读副本的 manifest 版本落后时间不能超过废弃文件的 lingering time,否则可能引用已被 GC 删除的文件"的约束。

两阶段实施建议

RFC 同时指出,临时 manifest + 心跳机制一次性实现复杂度较高,建议分两阶段推进:

  • Phase 1(基于时间的简单删除):先实现只依赖可配置 lingering time 的简单 GC 策略,为空间回收提供基线能力,暂不引入临时 manifest;
  • Phase 2(一致性感知 GC):根据 Phase 1 的实际效果与观测到的问题,再决定是否实现完整的临时 manifest 与心跳机制,以处理长查询场景。

配置参数详解

datanode 侧:region_engine.mito.gc

GC worker 的滞留时间等参数在 datanode 的 mito2 引擎配置中设置,完整示例见 config/datanode.example.toml,对应GcConfig结构体(src/mito2/src/gc.rs):

配置项类型默认值说明
region_engine.mito.gc.enableBoolfalse是否启用 GC。必须与 metasrv 的gc.enable保持一致,否则会产生意外行为
region_engine.mito.gc.lingering_timeString1h删除文件前的滞留时间。应足够长以让长查询完成;设为None时未使用文件会被立即删除
region_engine.mito.gc.unknown_file_lingering_timeString1d删除无法确定 expel time 的 unknown 文件前的滞留时间。仅在全量列表 GC 时生效;基于对象last_modified时间戳做严格小于比较。生产环境不宜配得过小,避免误删 compaction/flush 进行中的 pre-manifest 文件;对象存储不提供last_modified时保守保留;dropped region 的 unknown 文件立即删除

此外,源码中GcConfig还包含两个并发控制参数(未在示例配置文件中直接暴露,使用默认值):

  • max_concurrent_lister_per_gc_job(默认 32):单个 GC job 内并发列表操作的上限;
  • max_concurrent_gc_job(默认 4):datanode 上并发 GC job 的上限,防止过多 GC 任务压垮 datanode。

这两个参数由GcLimiter通过 tokio 信号量实施:每次新建LocalGcWorker时需获取一个信号量许可(permit()),无可用许可时返回TooManyGcJobs错误(gc.rs)。

metasrv 侧:gc

GC 调度器(何时、对哪些 region 触发 GC)由 metasrv 配置控制,完整示例见 config/metasrv.example.toml,对应GcSchedulerOptions(src/meta-srv/src/gc/options.rs):

配置项默认值说明
gc.enablefalse是否启用 GC。必须与 datanode 的mito.gc.enable保持一致;关闭时 datanode 上的部分文件可能永远不会被删除
gc.gc_cooldown_period5m同一 region 两次 GC 之间的冷却时间
max_concurrent_tables10并发处理的表数量上限
max_retries_per_region3单个 region GC 失败时的最大重试次数
region_gc_concurrency16表内 region GC 的并发度
retry_backoff_duration5s重试之间的退避时间
min_region_size_threshold100MBGC 候选 region 的最小尺寸阈值
sst_count_weight0.5GC 打分中 SST 文件数量的权重(SST 越多潜在可删文件越多,中等优先级)
file_removed_count_weight1.0GC 打分中待删文件数量的权重(可删文件越多优先级越高)
regions_per_table_threshold20每张表最多选出参与 GC 的 region 数(按打分取前 20)
mailbox_timeout60s与 datanode 的 mailbox 通信超时
full_file_listing_interval24h全量文件列表 GC 的执行间隔。全量列表开销大但能清理孤儿文件,每 N 个 GC 周期执行一次(N = 该值 / ticker 间隔)
tracker_cleanup_interval6h清理 GC tracker 中陈旧 region 条目的间隔(仅用于内存清理)

metasrv 侧 ticker 默认每 5 分钟触发一次 GC tick(options.rs),且所有参数在GcSchedulerOptions::validate()中做了合法性校验(例如max_concurrent_tablesretry_backoff_duration等必须大于 0,见 options.rs)。另外注意gc.experimental_soft_drop(软删除表的自动清理)为Enterprise Edition 专属功能,社区版配置为启用会直接校验失败。

已知缺陷与竞态条件

RFC 坦诚列出了该设计的固有缺陷,仓库实现与调度策略也都围绕这些风险做了针对性设计:

  • 依赖 Compaction 频率:GC 周期与 Compaction 频率直接绑定。Compaction 不频繁的环境中,废弃文件会累积较长时间才被回收,存储消耗可能增加。为此 metasrv 侧引入了独立的周期性 ticker 调度(而非仅靠 compaction 触发),缓解了这一依赖;
  • 与长查询的竞态:若长查询已开始但尚未写入临时 manifest,而 Compaction 恰好同时开始并将该查询所需文件标记为废弃,就可能造成误删。缓解手段是:临时 manifest 的写入阈值时间应显著小于废弃文件的 lingering time,从而保证下一次 GC 运行时查询已注册引用;同时,读副本的 manifest 版本落后时间不能超过废弃文件的 lingering time;
  • 与 region 迁移(region-migration)的竞态:RFC 用一张时序图说明了风险——GC 读取 Region 1 的 manifest 后、Region 迁移将 Leader 切换到 Region 2、Region 2 写入新文件,此时 GC 继续扫描目录会发现"不在 Region 1 manifest 中"的新文件并将其误标为孤儿并删除。根本解法是对同一 region 的 GC 与 region 迁移/重分区操作加互斥锁,避免二者并发执行:

源码层面,LocalGcWorker对 region 的 Follower 状态有明确防护:run()中若发现 region 处于RegionRoleState::Follower,会直接报错拒绝执行 GC(gc.rs),且 GC 对 manifest version 的一致性校验同样降低了此类风险;

  • 临时 manifest 上传到对象存储的额外开销:会增加复杂度与潜在性能开销,但长查询通常不频繁,影响预期很小。实现中还通过cross_region_refs处理 repartition 带来的跨 region 文件归属问题,进一步降低误删风险;
  • 与 repartition 的竞态:RFC 建议在 GC 期间同时对 region-migration 与 repartition 加锁,作为当前阶段的简单可靠方案。

结论与权衡

RFC 汇总了该集成式 GC 方案的核心权衡:

方面当前方案(集成式 GC)
实现复杂度中等。需要与 Compaction 流程及 slow query recorder(用于临时 manifest 管理)做细致集成
可靠性。与 Compaction 集成 + 利用长查询的临时 manifest,显著降低误删风险;精确管理废弃文件滞留时间、避免误删新建 SST,增强了数据安全性
性能开销低到中。GC 在 compaction 之后运行,对写入路径的直接冲击最小;slow query recorder 管理临时 manifest 的开销对长查询可接受
对其他组件的影响中等。需要修改 Compaction 流程以触发 GC、修改 slow query recorder 以管理临时 manifest,引入一定耦合,但整体提升了数据安全性
删除策略基于状态与时间。废弃文件按可配置滞留时间删除,若被临时 manifest 引用则暂停;未使用文件(从未进入 manifest)同样受滞留时间约束

未决问题与未来工作

RFC 留下两个需要进一步讨论与落地的方向:

  • Slow Query Recorder 实现细节:需要给出 slow query recorder 的具体修改规范,以及它与临时 manifest 之间精确的交互机制;
  • 可配置滞留时间:需要为废弃文件与未使用文件分别确立并开放具体的滞留时间配置,以在存储回收与数据可用性之间取得最优平衡。

替代方案对比

方案一:独立 GC 服务(Standalone GC Service)

不把 GC worker 集成进 Compaction,而是实现一个独立服务,周期性基于 manifest 信息与预定义保留策略扫描并清理废弃/未使用文件。

  • 优点:与 Compaction 解耦,可独立扩缩容部署;GC 频率与策略可独立于 Compaction 配置;
  • 缺点:需要单独的服务进行管理、监控与组件协调;可能与 Compaction 已有的文件扫描逻辑重复;保证读一致性需要更复杂的协调机制(如分布式锁管理器或更精细的临时 manifest 系统)。

若集成式 GC worker 被证明不够用,或未来需要更高级的 GC 策略,可考虑在将来实施此方案。

方案二:Manifest 驱动的立即删除(无滞留时间)

文件一旦从 manifest 移除就立即删除,不设置任何滞留时间。

  • 优点:GC 逻辑最简单,空间即时回收;
  • 缺点:若未做到完全同步,极易删除仍被长查询或其他进程使用的文件;需要极其健壮且即时的机制来保证无活跃查询引用待删文件,可能导致性能瓶颈或复杂的错误处理;由于删除的即时性,误删问题极难排查调试。

总结

Global GC Worker 是 GreptimeDB 存储引擎中保障对象存储空间可持续回收的关键设计。它以"Compaction 完成"为执行边界,通过"manifest 追踪 + 滞留时间窗口 + 临时文件引用清单"三层防护,在回收 repartition、compaction、manifest 更新失败等场景产生的废弃文件的同时,最大程度避免误删活跃查询正在使用的数据。当前仓库中 LocalGcWorker 与 GcScheduler 的协同实现、FileRefsManifest 的跨 region 引用追踪,以及 datanode/metasrv 两侧的 GC 配置项,共同构成了这一机制的完整工程落地,为后续更细粒度的滞留时间调优与 slow query recorder 集成预留了清晰的演进路径。

【免费下载链接】greptimedbThe open-source observability database. One columnar engine for metrics, logs, and traces, on object storage.项目地址: https://gitcode.com/GitHub_Trending/gr/greptimedb

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询