RocksDB 远程压缩并发 MANIFEST 轮转导致的打开失败:OpenAndCompact 重试机制解析
2026/9/19 16:47:15 网站建设 项目流程

RocksDB 远程压缩并发 MANIFEST 轮转导致的打开失败:OpenAndCompact 重试机制解析

【免费下载链接】rocksdbA library that provides an embeddable, persistent key-value store for fast storage.项目地址: https://gitcode.com/gh_mirrors/ro/rocksdb

导读

本文讲解 RocksDB 远程压缩(CompactionService/DB::OpenAndCompact)在 worker 以只读方式打开 primary 目录时,因 primary 并发轮转CURRENT/MANIFEST 文件而偶发失败的问题,以及 RocksDB 为此引入的“secondary 打开有限次重试”修复方案。读完本文,你将理解该竞态产生的文件系统语义背景、Status::TryAgain/PathNotFound/NotFound三类瞬态错误的含义、OpenAndCompactOptions::max_secondary_open_retries参数的正确用法,以及这一修复与remote_compaction_manifest_floor加固机制的关系。本文以 unreleased_history/bug_fixes/remote_compaction_secondary_open_retry.md 为骨架,并结合 db/db_impl/db_impl_secondary.cc 与 include/rocksdb/options.h 等源码展开。

一、问题背景:远程压缩如何打开 primary 目录

1.1 远程压缩的基本流程

RocksDB 的CompactionService允许把压缩任务卸载到另一台主机或另一个进程执行,从而把 primary 上的后台负载转移出去。核心入口是DB::OpenAndCompact(),它“以只读方式打开数据库并执行压缩,且不修改原始 DB”,压缩结果输出到源数据库目录下的子目录而非安装回原 LSM 树。相关声明见 include/rocksdb/db.h。

从 db/db_impl/db_impl_secondary.cc 的实现看,OpenAndCompact()的执行分为几个明确步骤:

  1. 反序列化压缩输入CompactionServiceInput::Read()input中恢复 primary 序列化好的压缩任务信息(目标 CF、输入文件等);
  2. 加载选项:通过LoadOptionsFromFile()读取 primary 的 OPTIONS 文件(OptionsFileName(name, compaction_input.options_file_number)),再以CompactionServiceOptionsOverride覆盖相关配置;
  3. 确定输出目录RemoteCompactionJobDir()依据use_session_tmp_dir_for_remote_compaction决定输出路径是<name>/session_tmp/<output_directory>还是<name>/<output_directory>
  4. 过滤列族:只打开 default CF 与目标 CF,因为远程压缩只需 MANIFEST 中的 LSM 状态,无需 WAL 重放;
  5. 以 secondary 方式打开 primary 目录(跳过 WAL 恢复),随后在该视图上执行压缩。

1.2 secondary 打开与 MANIFEST 跟踪

关键在第 5 步:worker 调用DBImplSecondary::OpenAsSecondaryImpl()打开primary 的实时目录(只读)。secondary 依赖ReactiveVersionSet跟踪 primary 的 MANIFEST 演进,其中MaybeSwitchManifest()(见 db/version_set.cc)负责在发现CURRENT指向新 MANIFEST 时切换文件句柄。

二、竞态根源:无 read-after-unlink 语义的文件系统

2.1 primary 如何轮转 MANIFEST

每次 MANIFEST 轮转,primary 都会:

  • 写出新的 MANIFEST 文件;
  • 把新写入的CURRENT文件rename 覆盖到旧的CURRENT之上,完成指针切换;
  • 视清理策略删除旧 MANIFEST。

对运行中的 secondary 而言,它可能正处于“读CURRENT→ 打开对应 MANIFEST”的窗口期内,于是出现两种经典竞态:

  • 读取CURRENT时拿到了旧值,随后旧 MANIFEST 被 primary 删除,再打开文件即失败;
  • 读取CURRENT后 primary 立刻切换,旧 MANIFEST 被删除,打开时同样失败。

2.2 POSIX 与远程/对象存储的语义差异

在 POSIX 文件系统上,“打开之后删除/替换”是安全的:已打开的文件描述符仍可继续读取,未打开但刚被 rename 覆盖的文件路径在删除前也有一段可用窗口,这就是 “read-after-unlink” 语义。

但远程/对象存储(remote/object stores)不具备该语义:被替换的对象会立即被报告为“不存在”。于是 secondary 打开时得到的不再是“文件不可读但进程可继续”,而是明确的错误状态:

状态触发场景
Status::PathNotFound打开CURRENT或 MANIFEST 时目标对象已不存在(文件系统层的PathNotFound
Status::NotFound同样的“对象已消失”在另一条路径上的等价表现
Status::TryAgainMANIFEST 层面的专门映射,见下文MaybeSwitchManifest()

MaybeSwitchManifest()中专门做了这种映射:当FileExists返回IsNotFound,或打开 MANIFEST 返回IsPathNotFound时,统一构造Status::TryAgain("The primary may have switched to a new MANIFEST and deleted the old one.")(见 db/version_set.cc),把“primary 可能刚切换 MANIFEST 并删除了旧文件”这一瞬态竞态显式暴露出来。

2.3 为什么这是瞬态而不是损坏

关键在于:这只是读取时机撞上了轮转窗口,并不是数据库损坏。下一次尝试读取时,CURRENT已指向新 MANIFEST,打开即可成功。因此正确做法不是报错,而是在有限次数内重试

三、修复方案:max_secondary_open_retries有限重试

3.1 参数定义

修复为OpenAndCompactOptions新增了max_secondary_open_retries字段,声明位于 include/rocksdb/options.h:

struct OpenAndCompactOptions { // Allows cancellation of an in-progress compaction. std::atomic<bool>* canceled = nullptr; // Maximum number of times to retry opening the source DB as a secondary // after the initial attempt fails because CURRENT or MANIFEST was replaced // concurrently. A value of zero disables retries. // // Default: 2 uint32_t max_secondary_open_retries = 2; // ... allow_resumption 等其余字段 };

参数语义:

  • 默认值为 2,即初始尝试之外最多再重试 2 次,共最多 3 次尝试;
  • 设为 0 表示完全禁用重试,保持旧行为(一次失败即返回);
  • 仅对瞬态的打开失败生效,见下方状态过滤逻辑。

3.2 重试逻辑的源码实现

重试循环位于 db/db_impl/db_impl_secondary.cc:

for (uint32_t retry_count = 0;; ++retry_count) { s = DBImplSecondary::OpenAsSecondaryImpl( db_options, name, output_path, column_families, &handles, &db, /*recover_wal=*/false, /*trust_manifest_recovery=*/floor_provided); if (s.ok() || !(s.IsTryAgain() || s.IsPathNotFound() || s.IsNotFound()) || retry_count == options.max_secondary_open_retries) { break; } ROCKS_LOG_WARN(db_options.info_log, "OpenAndCompact: secondary open of %s failed with %s " "(retry %" PRIu32 " of %" PRIu32 "); the primary may have replaced " "CURRENT/MANIFEST concurrently, retrying", name.c_str(), s.ToString().c_str(), retry_count + 1, options.max_secondary_open_retries); }

逻辑要点:

  1. 每次循环调用OpenAsSecondaryImpl()执行一次 secondary 打开(跳过 WAL 恢复,因为远程压缩只需要 MANIFEST 里的 LSM 状态);
  2. 只有Status::TryAgainStatus::PathNotFoundStatus::NotFound三类瞬态状态才会触发重试;其他错误(如损坏、权限问题、配置错误)立即返回,不会被重试掩盖;
  3. 重试次数达到max_secondary_open_retries后停止,把最后一次的错误状态返回给调用方;
  4. 每次重试前输出一条ROCKS_LOG_WARN级别的日志,包含失败状态与“第几次/共几次重试”信息,便于在线上定位此类竞态发生的频率。

注意循环条件中retry_count == options.max_secondary_open_retries时也会 break,配合循环后对s.ok()的判断,保证重试次数严格有界。

3.3 配套计时统计

打开过程整体耗时被记录到OPEN_AND_COMPACT_DB_OPEN_MICROS直方图(db/db_impl/db_impl_secondary.cc),对应 include/rocksdb/statistics.h 中“Time spent opening the secondary DB insideDB::OpenAndCompact()”的说明。运维者可通过该指标观察重试对打开耗时的实际影响。

四、不重试的代价:primary 后台错误

4.1 失败如何传导回 primary

该修复之所以重要,是因为对不回退到本地压缩CompactionService实现而言,worker 侧的任何失败都会成为primary 侧的后台错误(background error)。也就是说,一次纯粹由文件系统时序造成的瞬态失败,可能被放大为 primary 上的持续错误状态,进而触发后续的错误处理路径(如 stop writes)。

4.2 回退语义由集成方决定

CompactionService::Start()的返回值CompactionServiceJobStatus控制失败后的行为。测试实现给出了两种典型选择(见 db/compaction/compaction_service_test.cc):

  • 返回kUseLocal:任务回退到 primary 本地执行,是“有本地兜底”的集成方式;
  • 返回kFailure:不本地兜底,失败直接暴露为后台错误,这是压力测试工具DbStressCompactionService的刻意选择。

对于后一种集成方式,max_secondary_open_retries的重试就是避免无谓后台错误的关键防线。

五、与 MANIFEST floor 加固的关系

5.1 两个不同层面的保护

本修复与既有的remote_compaction_manifest_floor机制(include/rocksdb/options.h)互为补充,解决的问题层面不同:

机制解决的问题失败处理
remote_compaction_manifest_floor(默认 true)primary 调度压缩时,把当时的 MANIFEST 位置(文件号与大小)随请求下发;worker 若发现自己基于更旧的MANIFEST 视图(如最终一致文件系统返回了过期CURRENT、或 MANIFEST 被截断)重建压缩,则拒绝执行回退到本地压缩,避免基于错误的 LSM 形状产出错误结果
max_secondary_open_retries(默认 2)打开瞬间撞上CURRENT/MANIFEST 替换的瞬态竞态有限次重试后成功继续;重试耗尽才返回错误

floor 参数还隐含开启了 worker 侧的 “trust the MANIFEST” 恢复模式(floor_provided传给OpenAsSecondaryImpltrust_manifest_recovery,见 db/db_impl/db_impl_secondary.cc),避免因某个并非压缩输入文件的文件瞬时不可用而失败整个任务。

5.2 本修复的定位

本文描述的重试逻辑处理的是floor 机制之前的打开阶段:先要成功打开并建立 secondary 视图(依赖ReactiveVersionSetMaybeSwitchManifest),之后才谈得上 floor 检查。因此两者是同一链路中不同节点的加固:重试消化“打开时撞上轮转”的瞬时窗口,floor 拒绝“基于过期视图压缩”的正确性风险。

六、集成与验证方式

6.1 如何设置该参数

worker 侧调用DB::OpenAndCompact()时传入自定义的OpenAndCompactOptions

OpenAndCompactOptions options; options.max_secondary_open_retries = 2; // 默认值,可显式指定 // options.canceled = &canceled_flag; // 可选:支持任务取消 DB::OpenAndCompact(options, db_path, scheduled_job_id, compaction_input, &result, options_override);

若希望关闭重试以复现/调试竞态,可显式设为 0。

6.2 测试覆盖

测试侧已在 db/compaction/compaction_service_test.cc 中覆盖该参数:测试压缩服务把max_secondary_open_retries_传入OpenAndCompactOptions(见 db/compaction/compaction_service_test.cc),并提供接口调整重试次数(max_secondary_open_retries_字段定义见同文件 db/compaction/compaction_service_test.cc,默认值与OpenAndCompactOptions一致)。通过构造 concurrent CURRENT/MANIFEST 替换的时序(利用TEST_SYNC_POINT,如DBImplSecondary::OpenAndCompact::BeforeLoadingOptions:0/1AfterOpenAsSecondary:0等同步点),可验证重试后成功与重试耗尽后返回错误的两种路径。

相关测试入口还包括 db/db_secondary_test.cc 与 db/compaction/compaction_job_test.cc(均引用了OpenAndCompact),可用于对照 secondary 打开与压缩任务行为。

七、小结与运维建议

  • 根因OpenAndCompact()以只读方式打开 primary 实时目录时,primary 通过 rename 覆盖CURRENT轮转 MANIFEST;在无 read-after-unlink 语义的远程/对象存储上,被替换对象立即报告为不存在,表现为PathNotFound/NotFound/TryAgain
  • 修复OpenAndCompactOptions::max_secondary_open_retries(默认 2)对这三类瞬态状态做有界重试,避免并发轮转导致压缩失败进而演变为 primary 后台错误。
  • 运维建议:线上若使用对象存储/远程文件系统承载 RocksDB 数据目录并启用CompactionService,遇到TryAgain/PathNotFound类的偶发打开失败属正常竞态,可关注OPEN_AND_COMPACT_DB_OPEN_MICROS指标与 INFO/WARN 日志中的 “retry N of M” 信息确认重试生效;如竞态频率过高,可评估适当增大max_secondary_open_retries,但需权衡每次重试的开销。

【免费下载链接】rocksdbA library that provides an embeddable, persistent key-value store for fast storage.项目地址: https://gitcode.com/gh_mirrors/ro/rocksdb

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

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

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

立即咨询